socket之tcp如何维护长连接
1.TCP长连接与⼼跳保活
======
长连接
TCP经过三次握⼿建⽴连接,长连接是指不管有⽆数据包的发送都长期保持建⽴的连接;
有长连接⾃然也有短连接,短连接是指双⽅有数据发送时,就建⽴连接,发送⼏次请求后,就主动或者被动断开连接。
⼼跳
⼼跳是⽤来检测⼀个系统是否存活或者⽹络链路是否通畅的⼀种⽅式,做法是定时向被检测系统发送⼼跳包,被检测系统收到⼼跳包进⾏回复,收到回复说明对⽅存活。⼼跳能够给长连接提供保活功能,能够检
测长连接是否正常,⼀旦链路死了,不可⽤了,能够尽快知道,然后做些其他的⾼可⽤措施,来保证系统的正常运⾏。
长连接的优势
减少连接建⽴过程的耗时
TCP连接建⽴需要三次握⼿,三次握⼿也就说需要三次交互才能建⽴⼀个连接通道,同城的机器之间的⼤概是ms级别的延时,影响还不⼤,如果是北京和上海两地机房,⾛专线⼀来⼀回⼤概需要30ms,如果使⽤长连接,这个优化还是⼗分可观的。
⽅便实现push数据
数据交互-推模式实现的前提是⽹络长连接,有了长连接,连接两端很⽅便的互相push数据,来进⾏交互。
长连接保活
操作系统实现:
TCP的KeepAlive机制(此机制并不是TCP协议规范中的内容,由操作系统去实现)KeepAlive机制开启后,在⼀定时间内(⼀般时间为7200s,参数tcp_keepalive_time)在链路上没有数据传送的情况下,TCP层将发送相应的KeepAlive探针以确定连接可⽤性,探测失败后重试10(参数tcp_keepalive_probes)次,每次间隔时间75s(参数tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可⽤。这些参数是机器级别,可以调整。KeepAlive的保活机制只在链路空闲的情况下才会起到作⽤。
⼀个可靠的系统,长连接的保活肯定是要依赖应⽤层的⼼跳来保证的。
应⽤层实现:
如果客户端已经消失⽽连接未断开,则会使得服务器上保留⼀个半开放的连接,⽽服务器⼜在等待来⾃客户端的数据,此时服务器将永远等待客户端的数据。保活功能就是试图在服务端器端检测到这种半开放的连接。
如果⼀个给定的连接在两⼩时内没有任何动作,服务器就向客户发送⼀个探测报⽂段,根据客户端主机响应探测4个客户端状态:
客户主机依然正常运⾏,且服务器可达。此时客户的TCP响应正常,服务器将保活定时器复位。
客户主机已经崩溃,并且关闭或者正在重新启动。上述情况下客户端都不能响应TCP。服务端将⽆法收到客户端对探测的响应。服务器总共发送10个这样的探测,每个间隔75秒。若服务器没有收到任何⼀个响应,它就认为客户端已经关闭并终⽌连接。
客户端崩溃并已经重新启动。服务器将收到⼀个对其保活探测的响应,这个响应是⼀个复位,使得服务器终⽌这个连接。
客户机正常运⾏,但是服务器不可达。这种情况与第⼆种状态类似。
⼼跳包使⽤
⽅案⼀
最简单的策略当然是客户端定时n秒发送⼼跳包,服务端收到⼼跳包后,回复客户端的⼼跳,如果客户端连续m秒没有收到⼼跳包,则主动断开连接,然后重连,将正常的业务请求暂时不发送的该台服务器上。
⽅案⼆
这样传送⼀些⽆效的数据包有点多,可以做些优化。因为⼼跳就是⼀种探测请求,业务上的正常请求除
了做业务处理外,还可以⽤作探测的功能,⽐如此时有请求需要发送到服务端,这个请求就可以当作是⼀次⼼跳,服务端收到请求,处理后回复,只要服务端有回复,就表明链路还是通的,如果客户端请求⽐较空闲的时候,服务端⼀直没有数据回复,就使⽤⼼跳进⾏探测,这样就有效利⽤了正常的请求来作为⼼跳的功能,减少⽆效的数据传输。
----
1.TCP长连接与⼼跳保活
可能很多 Java 程序员对 TCP 的理解只有⼀个三次握⼿,四次握⼿的认识,我觉得这样的原因主要在于 TCP 协议本⾝稍微有点抽象(相⽐较于应⽤层的 HTTP 协议)。
前⾔
可能很多 Java 程序员对 TCP 的理解只有⼀个三次握⼿,四次握⼿的认识,我觉得这样的原因主要在于 TCP 协议本⾝稍微有点抽象(相⽐较于应⽤层的 HTTP 协议);其次,⾮框架开发者不太需要接触到 TCP 的⼀些细节。其实我个⼈对 TCP 的很多细节也并没有完全理解,这篇⽂章主要针对交流⾥有⼈提出的长连接,⼼跳的问题,做⼀个统⼀的整理。
在 Java 中,使⽤ TCP 通信,⼤概率会涉及到 Socket、Netty,本⽂会借⽤它们的⼀些 API 和设置参数
来辅助介绍。
长连接与短连接
TCP 本⾝并没有长短连接的区别,长短与否,完全取决于我们怎么⽤它。
短连接:每次通信时,创建 Socket;⼀次通信结束,调⽤ socket.close()。这就是⼀般意义上的短连接,短连接的好处是管理起来⽐较简单,存在的连接都是可⽤的连接,不需要额外的控制⼿段。
长连接:每次通信完毕后,不会关闭连接,这样就可以做到连接的复⽤。长连接的好处便是省去了创建连接的耗时。
短连接和长连接的优势,分别是对⽅的劣势。想要图简单,不追求⾼性能,使⽤短连接合适,这样我们就不需要操⼼连接状态的管理;想要追求性能,使⽤长连接,我们就需要担⼼各种问题:⽐如端对端连接的维护,连接的保活。
长连接还常常被⽤来做数据的推送,我们⼤多数时候对通信的认知还是 request/response 模型,但 TCP 双⼯通信的性质决定了它还可以被⽤来做双向通信。在长连接之下,可以很⽅便的实现 push 模型。
短连接没有太多东西可以讲,所以下⽂我们将⽬光聚焦在长连接的⼀些问题上。纯讲理论未免有些过于单调,所以下⽂我借助 Dubbo 这个RPC 框架的⼀些实践来展开 TCP 的相关讨论。
服务治理框架中的长连接
前⾯已经提到过,追求性能的时候,必然会选择使⽤长连接,所以借助 Dubbo 可以很好的来理解 TCP。我们开启两个 Dubbo 应⽤,⼀个server 负责监听本地 20880(众所周知,这是 Dubbo 协议默认的端⼝),⼀个 client 负责循环发送请求。执⾏lsof -i:20880命令可以查看端⼝的相关使⽤情况:
image.png
*:20880 (LISTEN)说明了 Dubbo 正在监听本地的 20880 端⼝,处理发送到本地 20880 端⼝的请求
后两条信息说明请求的发送情况,验证了 TCP 是⼀个双向的通信过程,由于我是在同⼀个机器开启了两个 Dubbo 应⽤,所以你能够看到是本地的 53078 端⼝与 20880 端⼝在通信。我们并没有⼿动设置 53078 这个客户端端⼝,他是随机的,但也阐释了⼀个道理:即使是发送请求的⼀⽅,也需要占⽤⼀个端⼝。
稍微说⼀下 FD 这个参数,他代表了⽂件句柄,每新增⼀条连接都会占⽤新的⽂件句柄,如果你在使⽤ TCP 通信的过程中出现了open too many files的异常,那就应该检查⼀下,你是不是创建了太多的连接,
⽽没有关闭。细⼼的读者也会联想到长连接的另⼀个好处,那就是会占⽤较少的⽂件句柄。
长连接的维护
因为客户端请求的服务可能分布在多个服务器上,客户端端⾃然需要跟对端创建多条长连接,使⽤长连接,我们遇到的第⼀个问题就是要如何维护长连接。
@Sharable
public class NettyHandler extends SimpleChannelHandler {
private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>(); // <ip:port, channel>
}
public class NettyServer extends AbstractServer implements Server {
private Map<String, Channel> channels; // <ip:port, channel>
}
在 Dubbo 中,客户端和服务端都使⽤ip:port维护了端对端的长连接,Channel 便是对连接的抽象。我们主要关注 NettyHandler 中的长连接,服务端同时维护⼀个长连接的集合是 Dubbo 的设计,我们将在后⾯提到。
连接的保活
这个话题就有的聊了,会牵扯到⽐较多的知识点。⾸先需要明确⼀点,为什么需要连接的报活?当双⽅已经建⽴了连接,但因为⽹络问题,
链路不通,这样长连接就不能使⽤了。需要明确的⼀点是,通过 netstat,lsof 等指令查看到连接的状态处于ESTABLISHED状态并不是⼀件⾮常靠谱的事,因为连接可能已死,但没有被系统感知到,更不⽤提假死这种疑难杂症了。如果保证长连接可⽤是⼀件技术活。
连接的保活:KeepAlive
⾸先想到的是 TCP 中的 KeepAlive 机制。KeepAlive 并不是 TCP 协议的⼀部分,但是⼤多数操作系统都实现了这个机制。KeepAlive 机制开启后,在⼀定时间内(⼀般时间为 7200s,参数tcp_keepalive_time)在链路上没有数据传送的情况下,TCP 层将发送相应的KeepAlive探针以确定连接
可⽤性,探测失败后重试 10(参数tcp_keepalive_probes)次,每次间隔时间 75s(参数tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可⽤。
在 Netty 中开启 KeepAlive:
bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
Linux 操作系统中设置 KeepAlive 相关参数,修改/f⽂件:
p_keepalive_time=90
p_keepalive_intvl=15
p_keepalive_probes=2
KeepAlive 机制是在⽹络层⾯保证了连接的可⽤性,但站在应⽤框架层⾯我们认为这还不够。主要体现在两个⽅⾯:KeepAlive 的开关是在应⽤层开启的,但是具体参数(如重试测试,重试间隔时间)的设置却是操作系统级别的,位于操作系统
的/f配置中,这对于应⽤来说不够灵活。
KeepAlive 的保活机制只在链路空闲的情况下才会起到作⽤,假如此时有数据发送,且物理链路已经不通,操作系统这边的链路状态还是 ESTABLISHED,这时会发⽣什么?⾃然会⾛ TCP 重传机制,要知道默认的 TCP 超时重传,指数退避算法也是⼀个相当长的过程。
KeepAlive 本⾝是⾯向⽹络的,并不是⾯向于应⽤的,当连接不可⽤时,可能是由于应⽤本⾝ GC 问题,系统 load ⾼等情况,但⽹络仍然是通的,此时,应⽤已经失去了活性,所以连接⾃然应该认为是不可⽤的。
看来,应⽤层⾯的连接保活还是必须要做的。
连接的保活:应⽤层⼼跳
终于点题了,⽂题中提到的⼼跳便是⼀个本⽂想要重点强调的另⼀个 TCP 相关的知识点。上⼀节我们已经解释过了,⽹络层⾯的KeepAlive 不⾜以⽀撑应⽤级别的连接可⽤性,本节就来聊聊应⽤层的⼼跳机制是实现连接保活的。
如何理解应⽤层的⼼跳?简单来说,就是客户端会开启⼀个定时任务,定时对已经建⽴连接的对端应⽤发送请求(这⾥的请求是特殊的⼼跳请求),服务端则需要特殊处理该请求,返回响应。如果⼼跳持续多
次没有收到响应,客户端会认为连接不可⽤,主动断开连接。不同的服务治理框架对⼼跳,建连,断连,拉⿊的机制有不同的策略,但⼤多数的服务治理框架都会在应⽤层做⼼跳,Dubbo 也不例外。
应⽤层⼼跳的设计细节
以 Dubbo 为例,⽀持应⽤层的⼼跳,客户端和服务端都会开启⼀个HeartBeatTask,客户端在HeaderExchangeClient中开启,服务端将在HeaderExchangeServer开启。⽂章开头埋了⼀个坑:Dubbo 为什么在服务端同时维护Map呢?主要就是为了给⼼跳做贡献,⼼跳定时任务在发现连接不可⽤时,会根据当前是客户端还是服务端⾛不同的分⽀,客户端发现不可⽤,是重连;服务端发现不可⽤,是直接 close。
// HeartBeatTask
if (channel instanceof Client) {
((Client) channel).reconnect();
} else {
channel.close();
}
熟悉其他 RPC 框架的同学会发现,不同框架的⼼跳机制真的是差距⾮常⼤。⼼跳设计还跟连接创建,重连机制,⿊名单连接相关,还需要具体框架具体分析。
除了定时任务的设计,还需要在协议层⾯⽀持⼼跳。最简单的例⼦可以参考 nginx 的健康检查,⽽针对 Dubbo 协议,⾃然也需要做⼼跳的⽀持,如果将⼼跳请求识别为正常流量,会造成服务端的压⼒问题,⼲扰限流等诸多问题。
image.png
socket通信报文格式dubbo protocol
其中 Flag 代表了 Dubbo 协议的标志位,⼀共 8 个地址位。低四位⽤来表⽰消息体数据⽤的序列化⼯具的类型(默认 hessian),⾼四位中,
第⼀位为1表⽰是 request 请求,第⼆位为 1 表⽰双向传输(即有返回response),第三位为 1 表⽰是⼼跳事件。
⼼跳请求应当和普通请求区别对待。
注意和 HTTP 的 KeepAlive 区别对待
HTTP 协议的 KeepAlive 意图在于连接复⽤,同⼀个连接上串⾏⽅式传递请求-响应数据
TCP 的 KeepAlive 机制意图在于保活、⼼跳,检测连接错误。
这压根是两个概念。
KeepAlive 常见错误
启⽤ TCP KeepAlive 的应⽤程序,⼀般可以捕获到下⾯⼏种类型错误
1. ETIMEOUT 超时错误,在发送⼀个探测保护包经过 (tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间后仍然没
有接收到 ACK 确认情况下触发的异常,套接字被关闭
2. java.io.IOException: Connection timed out
3. EHOSTUNREACH host unreachable(主机不可达)错误,这个应该是 ICMP 汇报给上层应⽤的。
4. java.io.IOException: No route to host
5. 链接被重置,终端可能崩溃死机重启之后,接收到来⾃服务器的报⽂,然物是⼈⾮,前朝往事,只能报以⽆奈重置宣告之。
6. java.io.IOException: Connection reset by peer
总结
有三种使⽤ KeepAlive 的实践⽅案:
默认情况下使⽤ KeepAlive 周期为 2 个⼩时,如不选择更改,属于误⽤范畴,造成资源浪费:内核会为每⼀个连接都打开⼀个保活计时器,N 个连接会打开 N 个保活计时器。优势很明显:
TCP 协议层⾯保活探测机制,系统内核完全替上层应⽤⾃动给做好了
内核层⾯计时器相⽐上层应⽤,更为⾼效
上层应⽤只需要处理数据收发、连接异常通知即可
数据包将更为紧凑
1. 关闭 TCP 的 KeepAlive,完全使⽤应⽤层⼼跳保活机制。由应⽤掌管⼼跳,更灵活可控,⽐如可以在应⽤级别设置⼼跳周期,适配私
有协议。
2. 业务⼼跳 + TCP KeepAlive ⼀起使⽤,互相作为补充,但 TCP 保活探测周期和应⽤的⼼跳周期要协调,以互补⽅可,不能够差距过
⼤,否则将达不到设想的效果。
各个框架的设计都有所不同,例如 Dubbo 使⽤的是⽅案三,但阿⾥内部的 HSF 框架则没有设置 TCP 的 KeepAlive,仅仅由应⽤⼼跳保活。和⼼跳策略⼀样,这和框架整体的设计相关。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论