持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外,减少开销的那部分时间,使请求和响应能够更早地结束。HTTP的KeepAlive和TCP的KeepAlive其实并不是一个概念,这一点很多人理解都是有误的。HTTP 协议的 KeepAlive 意图在于连接复用,同一个连接上串行方式传递请求-响应数据,在 HTTP/1.1 中,所有的连接默认都是持久连接,但在 HTTP/1.0 内并未标准化,虽然有一部分服务器通过非标准的手段实现了持久连接,但服务器端不一定能够支持持久连接。。TCP 的 KeepAlive 机制意图在于长连接保活、心跳,检测连接错误。
注:建立 TCP 连接到数量是限制的,Chrome 最多允许对同一个 Host 建立六个 TCP 连接(参阅资料)
管线化
RFC 2616 中规定
A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received. 一个支持持久连接的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应)。收到请求的服务器必须按照请求收到的顺序发送响应。
持久连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应,才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待
响应了。
HTTP/1.1 规范中规定了 Pipelining用于解决多个HTTP 发生问题,但现代浏览器默认是不开启Pipelining。为何呢?实践中会出现许多问题:
- 一些代理服务器不能正确的处理 HTTP Pipelining
- 正确的流水线实现是复杂的
-
Head-of-line Blocking 连接头阻塞:在建立起一个 TCP 连接之后,假设客户端在这个连接连续向服务器发送了几个请求。按照标准,服务器应该按照收到请求的顺序返回结果,假设服务器在处理首个请求时花费了大量时间,那么后面所有的请求都需要等着首个请求结束才能响应
但是,HTTP2 提供了 Multiplexing 多路传输特性(非文本),可以在一个 TCP 连接中同时完成多个 HTTP 请求。至于 Multiplexing 具体怎么实现的可以看我的另一篇HTTP2。
启用 TCP KeepAlive 的应用程序,一般可以捕获到下面几种类型错误
- ETIMEOUT 超时错误,java.io.IOException: Connection timed out,在发送一个探测保护包经过 (tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间后仍然没有接收到 ACK 确认情况下触发的异常,套接字被关闭。
- EHOSTUNREACH host unreachable(主机不可达)错误,java.io.IOException: No route to host,这个应该是 ICMP 汇报给上层应用的。
- 链接被重置,java.io.IOException: Connection reset by peer,终端可能崩溃死机重启之后,接收到来自服务器的报文,然物是人非,前朝往事,只能报以无奈重置宣告之。
三种使用 KeepAlive 的实践方案:
1. 操作系统级KeepAlive
KeepAlive 并不是 TCP 协议的一部分,但是大多数操作系统都实现了这个机制(所以需要在操作系统层面设置 KeepAlive 的相关参数)。
默认情况下使用 KeepAlive 周期为 2 个小时(参数 tcp_keepalive_time
),在链路上没有数据传送的情况下,TCP 层将发送相应的 KeepAlive 探针以确定连接可用性,探测失败后重试 10(参数 tcp_keepalive_probes)次,每次间隔时间 75s(参数 tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用。
如不选择更改,属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N 个连接会打开 N 个保活计时器。 优势很明显:
- TCP 协议层面保活探测机制,系统内核完全替上层应用自动给做好了
- 内核层面计时器相比上层应用,更为高效
- 上层应用只需要处理数据收发、连接异常通知即可
- 数据包将更为紧凑
Netty 中开启 KeepAlive:bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
Linux 操作系统中设置 KeepAlive 相关参数,修改 /etc/sysctl.conf
文件
net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2
2. 完全使用应用层心跳保活
KeepAlive 机制是在网络层面保证了连接的可用性,它会发生什么呢?
只在链路空闲的情况下才会起到作用的话,假如此时有数据发送,且物理链路已经不通,操作系统这边的链路状态还是 ESTABLISHED
,这时会发生什么?自然会走 TCP 重传机制,要知道默认的 TCP 超时重传,指数退避算法也是一个相当长的过程。
因此,KeepAlive 本身是面向网络的,并不面向于应用,当连接不可用,可能是由于应用本身的 GC 频繁,系统 load 高等情况。关闭 TCP 的 KeepAlive,由应用掌管心跳,更灵活可控,比如可以在应用级别设置心跳周期,适配私有协议。
如何理解应用层的心跳?
简单来说,就是客户端会开启一个定时任务,定时对已经建立连接的对端应用发送请求(这里的请求是特殊的心跳请求),服务端则需要特殊处理该请求,返回响应。如果心跳持续多次没有收到响应,客户端会认为连接不可用,主动断开连接。不同的服务治理框架对心跳,建连,断连,拉黑的机制有不同的策略,但大多数的服务治理框架都会在应用层做心跳,Dubbo/HSF 也不例外。
以 Dubbo 为例,支持应用层的心跳,客户端和服务端都会开启一个 HeartBeatTask
,客户端在 HeaderExchangeClient
中开启,服务端将在 HeaderExchangeServer
开启。
很多时候心跳请求应当和普通请求区别对待
最简单的例子可以参考 nginx 的健康检查,而针对 Dubbo 协议(需要在Dubbo 协议的标志位中设计心跳事件),自然也需要做心跳的支持,如果将心跳请求识别为正常流量,会造成服务端的压力问题,干扰限流等诸多问题。
3. 业务心跳 + TCP KeepAlive 一起使用
业务心跳 + TCP KeepAlive 一起使用,互相作为补充,但 TCP 保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果。
熟悉其他 RPC 框架的同学会发现,各个框架的设计都有所不同,不同框架的心跳机制真的是差距非常大。心跳设计还跟连接创建,重连机制,黑名单连接相关。例如 Dubbo 使用的是方案三,但阿里内部的 HSF 框架则没有设置 TCP 的 KeepAlive,仅仅由应用心跳保活。和心跳策略一样,这和框架整体的设计相关。