又见 KeepAlive
我为什么要谈 KeepAlive
最近工作中遇到一个问题,想把它记录下来,场景是这样的:
从上图可以看出,用户通过 Client 访问的是 LVS 的 VIP, VIP 后端挂载的 RealServer 是 Nginx 服务器。 Client 可以是浏览器也可以是一个客户端程序。一般情况下,这种架构不会出现问题,但是如果 Client 端把请求发送给 Nginx,Nginx 的后端需要一段时间才能返回结果,超过 1 分 30 秒就会有问题,使用 LVS 作为负载均衡设备看到的现象就是 1 分 30 秒之后, Client 和 Nginx 链接被断开,没有数据返回。原因是 LVS 默认保持 TCP 的 Session 为 90s,超过 90s 没有 TCP 报文在链接上传输,LVS 就会给两端发送 RESET 报文断开链接。LVS 这么做的原因相信大家都知道一二,我所知道的原因主要有两点:
1.节省负载均衡设备资源,每一个 TCP/UDP 的链接都会在负载均衡设备上创建一个 Session 的结构, 链接如果一直不断开,这种 Session 结构信息最终会消耗掉所有的资源,所以必须释放掉。
2.另外释放掉能保护后端的资源,如果攻击者通过空链接,链接到 Nginx 上,如果 Nginx 没有做合适 的保护,Nginx 会因为链接数过多而无法提供服务。
这种问题不只是在 LVS 上有,之前在商用负载均衡设备 F5 上遇到过同样的问题,F5 的 Session 断开方式和 LVS 有点区别,F5 不会主动发送 RESET 给链接的两端,Session 消失之后,当链接中一方再次发送报文时会接收到 F5 的 RESET, 之后的现象是再次发送报文的一端 TCP 链接状态已经断开,而另外一端却还是 ESTABLISH 状态。
知道是负载均衡设备原因之后,第一反应就是通过开启 KeepAlive 来解决。到此这个问题应该是结束了,但是我发现过一段时间总又有人提起 KeepAlive 的问题,甚至发现由于 KeepAlive 的理解不正确浪费了很多资源,原本能使用 LVS 的应用放在了公网下沉区,或者换成了商用 F5 设备(F5 设备的 Session 断开时间要长一点,默认应该是 5 分钟)。所以我决定把我知道的 KeepAlive 知识点写篇博客分享出来。
为什么要有 KeepAlive?
在谈 KeepAlive 之前,我们先来了解下简单 TCP 知识(知识很简单,高手直接忽略)。首先要明确的是在 TCP 层是没有“请求”一说的,经常听到在 TCP 层发送一个请求,这种说法是错误的。TCP 是一种通信的方式,“请求”一词是事务上的概念,HTTP 协议是一种事务协议,如果说发送一个 HTTP 请求,这种说法就没有问题。也经常听到面试官反馈有些面试运维的同学,基本的 TCP 三次握手的概念不清楚,面试官问 TCP 是如何建立链接,面试者上来就说,假如我是客户端我发送一个请求给服务端,服务端发送一个请求给我。。。这种一听就知道对 TCP 基本概念不清楚。下面是我通过 wireshark 抓取的一个 TCP 建立握手的过程。(命令行基本上用 TCPdump,后面我们还会用这张图说明问题):
现在我看只要看前 3 行,这就是 TCP 三次握手的完整建立过程,第一个报文 SYN 从发起方发出,第二个报文 SYN,ACK 是从被连接方发出,第三个报文 ACK 确认对方的 SYN,ACK 已经收到,如下图:
但是数据实际上并没有传输,请求是有数据的,第四个报文才是数据传输开始的过程,细心的读者应该能够发现 wireshark 把第四个报文解析成 HTTP 协议,HTTP 协议的 GET 方法和 URI 也解析出来,所以说 TCP 层是没有请求的概念,HTTP 协议是事务性协议才有请求的概念,TCP 报文承载 HTTP 协议的请求(Request) 和响应(Response)。
现在才是开始说明为什么要有 KeepAlive。链接建立之后,如果应用程序或者上层协议一直不发送数据,或者隔很长时间才发送一次数据,当链接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,链接还需不需要保持,这种情况在 TCP 协议设计中是需要考虑到的。TCP 协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP 自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,链接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持链接。
如何开启 KeepAlive
KeepAlive 并不是默认开启的,在 Linux 系统上没有一个全局的选项去开启 TCP 的 KeepAlive。需要开启 KeepAlive 的应用必须在 TCP 的 socket 中单独开启。Linux Kernel 有三个选项影响到 KeepAlive 的行为:
- net.ipv4.tcpkeepaliveintvl = 75
- net.ipv4.tcpkeepaliveprobes = 9
- net.ipv4.tcpkeepalivetime = 7200
tcpkeepalivetime 的单位是秒,表示 TCP 链接在多少秒之后没有数据报文传输启动探测报文; tcpkeepaliveintvl 单位是也秒,表示前一个探测报文和后一个探测报文之间的时间间隔,tcpkeepaliveprobes 表示探测的次数。
TCP socket 也有三个选项和内核对应,通过 setsockopt 系统调用针对单独的 socket 进行设置:
- TCPKEEPCNT: 覆盖 tcpkeepaliveprobes
- TCPKEEPIDLE: 覆盖 tcpkeepalivetime
- TCPKEEPINTVL: 覆盖 tcpkeepalive_intvl
举个例子,以我的系统默认设置为例,kernel 默认设置的 tcpkeepalivetime 是 7200s, 如果我在应用程序中针对 socket 开启了 KeepAlive,然后设置的 TCP_KEEPIDLE 为 60,那么 TCP 协议栈在发现 TCP 链接空闲了 60s 没有数据传输的时候就会发送第一个探测报文。
TCP KeepAlive 和 HTTP 的 Keep-Alive 是一样的吗?
估计很多人乍看下这个问题才发现其实经常说的 KeepAlive 不是这么回事,实际上在没有特指是 TCP 还是 HTTP 层的 KeepAlive,不能混为一谈。TCP 的 KeepAlive 和 HTTP 的 Keep-Alive 是完全不同的概念。TCP 层的 KeepAlive 上面已经解释过了。 HTTP 层的 Keep-Alive 是什么概念呢? 在讲述 TCP 链接建立的时候,我画了一张三次握手的示意图,TCP 在建立链接之后, HTTP 协议使用 TCP 传输 HTTP 协议的请求(Request) 和响应(Response) 数据,一次完整的 HTTP 事务如下图:
各位看官请注意,这张图我简化了 HTTP(Req) 和 HTTP(Resp),实际上的请求和响应需要多个 TCP 报文。从图中可以发现一个完整的 HTTP 事务,有链接的建立,请求的发送,响应接收,断开链接这四个过程,早期通过 HTTP 协议传输的数据以文本为主,一个请求可能就把所有要返回的数据取到,但是,现在要展现一张完整的页面需要很多个请求才能完成,如图片,JS,CSS 等,如果每一个 HTTP 请求都需要新建并断开一个 TCP,这个开销是完全没有必要的,开启 HTTP Keep-Alive 之后,能复用已有的 TCP 链接,当前一个请求已经响应完毕,服务器端没有立即关闭 TCP 链接,而是等待一段时间接收浏览器端可能发送过来的第二个请求,通常浏览器在第一个请求返回之后会立即发送第二个请求,如果某一时刻只能有一个链接,同一个 TCP 链接处理的请求越多,开启 KeepAlive 能节省的 TCP 建立和关闭的消耗就越多。当然通常会启用多个链接去从服务器器上请求资源,但是开启了 Keep-Alive 之后,仍然能加快资源的加载速度。HTTP/1.1 之后默认开启 Keep-Alive, 在 HTTP 的头域中增加 Connection 选项。当设置为 Connection:keep-alive 表示开启,设置为 Connection:close 表示关闭。实际上 HTTP 的 KeepAlive 写法是 Keep-Alive,跟 TCP 的 KeepAlive 写法上也有不同。所以 TCP KeepAlive 和 HTTP 的 Keep-Alive 不是同一回事情。
Nginx 的 TCP KeepAlive 如何设置
开篇提到我最近遇到的问题,Client 发送一个请求到 Nginx 服务端,服务端需要经过一段时间的计算才会返回, 时间超过了 LVS Session 保持的 90s,在服务端使用 Tcpdump 抓包,本地通过 wireshark 分析显示的结果如第二副图所示,第 5 条报文和最后一条报文之间的时间戳大概差了 90s。在确定是 LVS 的 Session 保持时间到期的问题之后,我开始在寻找 Nginx 的 TCP KeepAlive 如何设置,最先找到的选项是 keepalivetimeout,从同事那里得知 keepalivetimeout 的用法是当 keepalivetimeout 的值为 0 时表示关闭 keepalive,当 keepalivetimeout 的值为一个正整数值时表示链接保持多少秒,于是把 keepalivetimeout 设置成 75s,但是实际的测试结果表明并不生效。显然 keepalivetimeout 不能解决 TCP 层面的 KeepAlive 问题,实际上 Nginx 涉及到 keepalive 的选项还不少,Nginx 通常的使用方式如下:
从 TCP 层面 Nginx 不仅要和 Client 关心 KeepAlive,而且还要和 Upstream 关心 KeepAlive, 同时从 HTTP 协议层面,Nginx 需要和 Client 关心 Keep-Alive,如果 Upstream 使用的 HTTP 协议,还要关心和 Upstream 的 Keep-Alive,总而言之,还比较复杂。所以搞清楚 TCP 层的 KeepAlive 和 HTTP 的 Keep-Alive 之后,就不会对于 Nginx 的 KeepAlive 设置错。我当时解决这个问题时候不确定 Nginx 有配置 TCP keepAlive 的选项,于是我打开 Ngnix 的源代码,在源代码里面搜索 TCP_KEEPIDLE,相关的代码如下:
从代码的上下文我发现 TCP KeepAlive 可以配置,所以我接着查找通过哪个选项配置,最后发现 listen 指令的 so_keepalive 选项能对 TCP socket 进行 KeepAlive 的配置。
以上三个参数只能使用一个,不能同时使用, 比如 so_keepalive=on, so_keepalive=off 或者 so_keepalive=30s::(表示等待 30s 没有数据报文发送探测报文)。通过设置 listen 80,so_keepalive=60s::之后成功解决 Nginx 在 LVS 保持长链接的问题,避免了使用其他高成本的方案。在商用负载设备上如果遇到类似的问题同样也可以通过这种方式解决。
参考资料
- 《TCP/IP 协议详解 VOL1》-- 强烈建议对于网络基本知识不清楚同学有空去看下。
- http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/
- http://nginx.org/en/docs/http/ngx_http_core_module.html
- Nginx Source code: https://github.com/alibaba/tengine
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Linux 释放 cache 内存
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论