在 http 早期,每个 http 请求都要求打开一个 tpc socket 连接,并且使用一次之后就断开这个 tcp 连接。
使用 keep-alive 可以改善这种状态,即在一次 TCP 连接中可以持续发送多份数据而不会断开连接。通过使用 keep-alive 机制,可以减少 tcp 连接建立次数,也意味着可以减少 TIME_WAIT 状态连接,以此提高性能和提高 httpd 服务器的吞吐率 (更少的 tcp 连接意味着更少的系统内核调用,socket 的 accept() 和 close()调用)。
但是,keep-alive 并不是免费的午餐, 长时间的 tcp 连接容易导致系统资源无效占用。配置不当的 keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置 keep-alive timeout 时间非常重要。
Httpd 守护进程,一般都提供了 keep-alive timeout 时间设置参数。比如 nginx 的 keepalive_timeout,和 Apache 的 KeepAliveTimeout。这个 keepalive_timout 时间值意味着:一个 http 产生的 tcp 连接在传送完最后一个响应后,还需要 hold 住 keepalive_timeout 秒后,才开始关闭这个连接。
当 httpd 守护进程发送完一个响应后,理应马上主动关闭相应的 tcp 连接,设置 keepalive_timeout 后,httpd 守护进程会想说:”再等等吧,看看浏览器还有没有请求过来”,这一等,便是 keepalive_timeout 时间。如果守护进程在这个等待的时间里,一直没有收到浏览发过来 http 请求,则关闭这个 http 连接。
下面写一个脚本,方便测试:
2 |
echo "www.example.com" ; |
1. 当 keepalive_timeout 时间为 0 时,即不启用 Keep-Alive 时,一个 tcp 连接的生命周期:
01 |
#tcpdump -n host 218.1.57.236 and port 80 |
02 |
20:36:50.792731 IP 218.1.57.236.43052 > 222.73.211.215.http: S 1520902589:1520902589(0) win 65535 |
03 |
20:36:50.792798 IP 222.73.211.215.http > 218.1.57.236.43052: S 290378256:290378256(0) ack 1520902590 win 5840 |
04 |
20:36:50.801629 IP 218.1.57.236.43052 > 222.73.211.215.http: . ack 1 win 32768 |
06 |
20:36:50.801838 IP 218.1.57.236.43052 > 222.73.211.215.http: P 1:797(796) ack 1 win 32768 |
07 |
20:36:50.801843 IP 222.73.211.215.http > 218.1.57.236.43052: . ack 797 win 59 |
09 |
20:37:50.803230 IP 222.73.211.215.http > 218.1.57.236.43052: P 1:287(286) ack 797 win 59 |
10 |
20:37:50.803289 IP 222.73.211.215.http > 218.1.57.236.43052: F 287:287(0) ack 797 win 59 |
11 |
20:37:50.893396 IP 218.1.57.236.43052 > 222.73.211.215.http: . ack 288 win 32625 |
12 |
20:37:50.894249 IP 218.1.57.236.43052 > 222.73.211.215.http: F 797:797(0) ack 288 win 32625 |
13 |
20:37:50.894252 IP 222.73.211.215.http > 218.1.57.236.43052: . ack 798 win 59 |
- 第 1~3 行建立 tcp 三次握手,建立连接。用时 8898μs
- 第 4~5 行通过建立的连接发送第一个 http 请求,服务端确认收到请求。用时 5μs
- 第 5~6 行,可以知道脚本执行用时 60s1387μs, 与 php 脚本相符。
- 第 6、8 行服务端发送 http 响应。发送响应用时 90166μs。
- 第 7 行,表明由服务端守护进程主动关闭连接。结合第 6、8 行,说明 http 响应一旦发送完毕,服务端马上关闭这个 tcp 连接
- 第 7、9、10 说明 tcp 连接顺序关闭, 用时 90963μs。需要注意, 这里 socket 资源并没有立即释放,需要等待 2MSL 时间(60s)后才被真正释放。
由此可见,在没有设置 keepalive_timeout 情况下,一个 socket 资源从建立到真正释放需要经过的时间是: 建立 tcp 连接 + 传送 http 请求 + php 脚本执行 + 传送 http 响应 + 关闭 tcp 连接 + 2MSL。(注: 这里的时间只能做参考,具体的时间主要由网络带宽,和响应大小而定)
2. 当 keepalive_timeout 时间大于 0 时,即启用 Keep-Alive 时,一个 tcp 连接的生命周期。为了便于分析,我们将 keepalive_timeout 设置为 300s
01 |
#tcpdump -n host 218.1.57.236 and port 80 |
02 |
21:38:05.471129 IP 218.1.57.236.54049 > 222.73.211.215.http: S 1669618600:1669618600(0) win 65535 |
03 |
21:38:05.471140 IP 222.73.211.215.http > 218.1.57.236.54049: S 4166993862:4166993862(0) ack 1669618601 win 5840 |
04 |
21:38:05.481731 IP 218.1.57.236.54049 > 222.73.211.215.http: . ack 1 win 32768 |
05 |
21:38:05.481976 IP 218.1.57.236.54049 > 222.73.211.215.http: P 1:797(796) ack 1 win 32768 |
06 |
21:38:05.481985 IP 222.73.211.215.http > 218.1.57.236.54049: . ack 797 win 59 |
08 |
21:38:07.483626 IP 222.73.211.215.http > 218.1.57.236.54049: P 1:326(325) ack 797 win 59 |
09 |
21:38:07.747614 IP 218.1.57.236.54049 > 222.73.211.215.http: . ack 326 win 32605 |
10 |
21:43:07.448454 IP 222.73.211.215.http > 218.1.57.236.54049: F 326:326(0) ack 797 win 59 |
11 |
21:43:07.560316 IP 218.1.57.236.54049 > 222.73.211.215.http: . ack 327 win 32605 |
12 |
21:43:11.759102 IP 218.1.57.236.54049 > 222.73.211.215.http: F 797:797(0) ack 327 win 32605 |
13 |
21:43:11.759111 IP 222.73.211.215.http > 218.1.57.236.54049: . ack 798 win 59 |
- 我们先看一下,第 6~8 行,跟上次示例不一样的是,服务端 httpd 守护进程发完响应后,没有立即主动关闭 tcp 连接。
- 第 8 行,结合第 6 行,我们可以看到,5 分钟 (300s) 后,服务端主动关闭这个 tcp 连接。这个时间,正是我们设置的 keepalive_timeout 的时间。
- 由此可见,设置了 keepalive_timout 时间情况下,一个 socket 建立到释放需要的时间是多了 keepalive_timeout 时间。
3. 当 keepalive_timeout 时间大于 0,并且在同一个 tcp 连接发送多个 http 响应。这里为了便于分析,我们将 keepalive_timeout 设置为 180s
通过这个测试,我们想弄清楚,keepalive_timeout 是从第一个响应结束开启计时,还是最后一个响应结束开启计时。测试结果证实是后者,这里,我们每隔 120s 发一次请求,通过一个 tcp 连接发送了 3 个请求。
01 |
# tcpdump -n host 218.1.57.236 and port 80 |
02 |
22:43:57.102448 IP 218.1.57.236.49955 > 222.73.211.215.http: S 4009392741:4009392741(0) win 65535 |
03 |
22:43:57.102527 IP 222.73.211.215.http > 218.1.57.236.49955: S 4036426778:4036426778(0) ack 4009392742 win 5840 |
04 |
22:43:57.111337 IP 218.1.57.236.49955 > 222.73.211.215.http: . ack 1 win 32768 |
06 |
22:43:57.111522 IP 218.1.57.236.49955 > 222.73.211.215.http: P 1:797(796) ack 1 win 32768 |
07 |
22:43:57.111530 IP 222.73.211.215.http > 218.1.57.236.49955: . ack 797 win 59 |
08 |
22:43:59.114663 IP 222.73.211.215.http > 218.1.57.236.49955: P 1:326(325) ack 797 win 59 |
09 |
22:43:59.350143 IP 218.1.57.236.49955 > 222.73.211.215.http: . ack 326 win 32605 |
11 |
22:45:59.226102 IP 218.1.57.236.49955 > 222.73.211.215.http: P 1593:2389(796) ack 650 win 32443 |
12 |
22:45:59.226109 IP 222.73.211.215.http > 218.1.57.236.49955: . ack 2389 win 83 |
13 |
22:46:01.227187 IP 222.73.211.215.http > 218.1.57.236.49955: P 650:974(324) ack 2389 win 83 |
14 |
22:46:01.450364 IP 218.1.57.236.49955 > 222.73.211.215.http: . ack 974 win 32281 |
16 |
22:47:57.377707 IP 218.1.57.236.49955 > 222.73.211.215.http: P 3185:3981(796) ack 1298 win 32119 |
17 |
22:47:57.377714 IP 222.73.211.215.http > 218.1.57.236.49955: . ack 3981 win 108 |
18 |
22:47:59.379496 IP 222.73.211.215.http > 218.1.57.236.49955: P 1298:1622(324) ack 3981 win 108 |
19 |
22:47:59.628964 IP 218.1.57.236.49955 > 222.73.211.215.http: . ack 1622 win 32768 |
21 |
22:50:59.358537 IP 222.73.211.215.http > 218.1.57.236.49955: F 1622:1622(0) ack 3981 win 108 |
22 |
22:50:59.367911 IP 218.1.57.236.49955 > 222.73.211.215.http: . ack 1623 win 32768 |
23 |
22:50:59.686527 IP 218.1.57.236.49955 > 222.73.211.215.http: F 3981:3981(0) ack 1623 win 32768 |
24 |
22:50:59.686531 IP 222.73.211.215.http > 218.1.57.236.49955: . ack 3982 win 108 |
- 第一组,三个 ip 包表示 tcp 三次握手建立连接,由浏览器建立。
- 第二组,发送第一次 http 请求并且得到响应,服务端守护进程输出响应之后,并没马上主动关闭 tcp 连接。而是启动 keepalive_timout 计时。
- 第三组,2 分钟后,发送第二次 http 请求并且得到响应,同样服务端守护进程也没有马上主动关闭 tcp 连接,重新启动 keepalive_timout 计时。
- 第四组,又 2 分钟后,发送了第三次 http 请求并且得到响应。服务器守护进程依然没有主动关地闭 tcp 连接(距第一次 http 响应有 4 分钟了, 大于 keepalive_timeout 值),而是重新启动了 keepalive_timout 计时。
- 第五组,跟最后一个响应 keepalive_timeout(180s)内,守护进程再没有收到请求。计时结束,服务端守护进程主动关闭连接。4 次挥手后,服务端进入 TIME_WAIT 状态。
这说明,当设定了 keepalive_timeout,一个 socket 由建立到释放,需要时间是:tcp 建立 + (最后一个响应时间 – 第一个请求时间) + tcp 关闭 + 2MSL。红色加粗表示每一次请求发送时间、每一次请求脚本执行时间、每一次响应发送时间,还有两两请求相隔时间。进一步测试,正在关闭或者 TIME_WAIT 状态的 tcp 连接,不能传输 http 请求和响应。即,当一个连接结束 keepalive_timeout 计时,服务端守护进程发送第一个 FIN 标志 ip 包后,该连接不能再使用了。
http keep-alive 与 tcp keep-alive,不是同一回事,意图不一样。http keep-alive 是为了让 tcp 活得更久一点,以便在同一个连接上传送多个 http,提高 socket 的效率。而 tcp keep-alive 是 TCP 的一种检测 TCP 连接状况的保鲜机制。tcp keep-alive 保鲜定时器,支持三个系统内核配置参数:
1 |
echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time |
2 |
echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl |
3 |
echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes |
keepalive 是 TCP 保鲜定时器,当网络两端建立了 TCP 连接之后,闲置 idle(双方没有任何数据流发送往来)了 tcp_keepalive_time 后,服务器内核就会尝试向客户端发送侦测包,来判断 TCP 连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的回答(ack 包),则会在 tcp_keepalive_intvl 后再次尝试发送侦测包,直到收到对对方的 ack, 如果一直没有收到对方的 ack, 一共会尝试 tcp_keepalive_probes 次,每次的间隔时间在这里分别是 15s, 30s, 45s, 60s, 75s。如果尝试 tcp_keepalive_probes, 依然没有收到对方的 ack 包,则会丢弃该 TCP 连接。TCP 连接默认闲置时间是 2 小时,一般设置为 30 分钟足够了。
也就是说,仅当 nginx 的 keepalive_timeout 值设置高于 tcp_keepalive_time,并且距此 tcp 连接传输的最后一个 http 响应,经过了 tcp_keepalive_time 时间之后,操作系统才会发送侦测包来决定是否要丢弃这个 TCP 连接。一般不会出现这种情况,除非你需要这样做。
使用 http keep-alvie,可以减少服务端 TIME_WAIT 数量(因为由服务端 httpd 守护进程主动关闭连接)。道理很简单,相较而言,启用 keep-alive,建立的 tcp 连接更少了,自然要被关闭的 tcp 连接也相应更少了。
我想用一张示意图片来说明使用启用 keepalive 的不同。另外,http keepalive 是客户端浏览器与服务端 httpd 守护进程协作的结果,所以,我们另外安排篇幅介绍不同浏览器的各种情况对 keepalive 的利用。
![HTTP Keep-Alive 是什么?如何工作? HTTP Keep-Alive 是什么?如何工作?](https://www.bomx.cn/wp-content/uploads/2023/08/Snipaste_2023-08-29_23-38-34.jpg)