TCP Keepalive 学习
TCP keepalive 意味着能够检测连接的 socket, 并且能够确定连接是 running 还是 broken。
0x00 什么是 TCP keepalive
keepalive 的概念十分简单:当你建立 TCP 连接时候,可以将一些定时器与之关联起来。其中一些定时器处理 keepalive 过程。当 keepalive 的定时器到 0 时,会向对端发送一个没有数据的 keepalive 探测包,并打开 ACK 标志。可以这么做是因为 TCP / IP 规范有一种重复确认(duplicate ACK),并且因为 TCP 是一种基于流的协议,对端也是没有参数的。另一方面,将收到对端主机(对端主机并不需要支持 keepalive,只要 TCP / IP)的应答消息,该消息没有数据和 ACK。
如果收到了 keepalive 探测包的应答消息,那么可以断言该连接仍在运行并且不用担心用户层的实现。事实上,TCP 允许处理一条数据流而不是数据包,所以零长度的数据包对用户程序也是没有危险的。
这个过程是非常有用的,因为如果对端断开了连接(比如重启),即使没有流量我们也会注意到连接已经断开。如果对端没有回应 keepalive 探测包,那么可以断言连接已经不是有效的了, 应该采取相应措施。
0x01 为什么要使用 TCP keepalive
Keepalive 是非入侵性的。在大多数情况下,即使有疑问,打开它也是没有风险的。但是它会产生额外的网络流量,可能对路由器和防火墙产生影响。
检测对端的死连接
keepalive 能够在对端告诉我们自己挂掉之前就通知我们。这可能由于几个原因造成的。比如 kernel panic 又或者对端进程被杀死。
对端进程虽然是正常的,但是网络链路却出了故障。这种场景下,如果网络链路不恢复正常的话,对我们来说,对端依旧是挂掉的。在这两种场景下,对端在挂掉之前都是无法通知我们的。
这些情况下,正常的TCP操作无法检查连接状态。
_____ _____
| | | |
| A | | B |
|_____| |_____|
^ ^
|--->--->--->-------------- SYN -------------->--->--->---|
|---<---<---<------------ SYN/ACK ------------<---<---<---|
|--->--->--->-------------- ACK -------------->--->--->---|
| |
| system crash ---> X
|
| system restart ---> ^
| |
|--->--->--->-------------- PSH -------------->--->--->---|
|---<---<---<-------------- RST --------------<---<---<---|
| |
假设 A 和 B 的 TCP 连接场景: 已经通过三次握手建立了 TCP 连接,这时候我们认为已经建立了稳定的连接,可以正常在这条链路上发送数据。这是突然发送意外:B 突然断电关机了,还没有来得及通知 A 连接断了。此时 A 端准备接收数据,然而并不知道 B 已经 crash 了。此时恢复 B 端的电源等待系统重启。此时的状态就是 A 和 B 都正常运行,并且 A 知道它和 B 之间有一条已经建立好的连接,但是 B 却不知道。这个时候,如果 A 试图通过这条连接向 B 发送数据,B 将回复一个 RST 数据包(在一个已关闭的 socket 上收到数据时,将发送 RST 数据包,要求对端关闭异常连接且对端不需要回复 ACK ),这样将导致 A 最终关闭这个连接。至此,这个死连接才算清理掉。
Keepalive 能够告诉我们对端不可达而不会误报。事实上如果两端网络之间出现问题,keepalive 会等待一段时间,多次重试失败才会将连接标记为不可用。
防止由于网络不活动而导致的断开连接
keepalive 的另外一个目标就是防止因为网络不活动而断开网络连接。当我们在 NAT 代理或者使用防火墙的时候,经常会出现这种问题。这是由 NAT 代理和防火墙内部的实现导致的:NAT 代理和防火墙一般会记录所有通过他们的连接。但由于机器的物理资源限制,它们只能在内存中保存有限数量的连接。最常见的策略就是保持最新的连接,丢弃掉老的或者不活动的连接。
_____ _____ _____
| | | | | |
| A | | NAT | | B |
|_____| |_____| |_____|
^ ^ ^
|--->--->--->---|----------- SYN ------------->--->--->---|
|---<---<---<---|--------- SYN/ACK -----------<---<---<---|
|--->--->--->---|----------- ACK ------------->--->--->---|
| | |
| | <--- connection deleted from table |
| | |
|--->- PSH ->---| <--- invalid connection |
| | |
回到 A 和 B。通道一旦打开,等待事件发生,然后将此通知给其他对等方。但是在较长时间间隔之后,A 和 B 会实际向对方发送数据。此时, A 和 B 的连接是有效的(建立连接后,如果不断开,则该连接一直保持),但是代理或者防火墙却并不知道(该连接已经在他们的内存中被新的连接淘汰掉了)。当发出数据后,代理将不能正确处理我们的数据,最终导致连接断开。因为正常的实现将连接放在列表顶部,当它的一个数据包到达时,并且当它需要消除一个条目时选择队列中的最后一个连接,通过网络周期性地发送数据包是始终处于极小的位置,并有轻微的删除风险。
0x02 Linux 下的 TCP keepalive
Linux 已经内建支持 keepalive。需要 procfs
和 sysctl
的支持来配置内核参数。
tcp_keepalive_time
最后一次数据包(这里简单的 ack 不被认为是数据)发送到第一次 keepalive 探测包发送的间隔;在连接被标记为需要 keepalive 之后,这个计数器不再被使用。
tcp_keepalive_intvl
keepalive 探测包发送的间隔,不管这是连接上发送了什么数据交换。
tcp_keepalive_probes
在认为连接失效并通知应用层时未确认的探测包个数,即探测次数。
配置内核
Procfs
可以通过查看 /proc/sys/net/ipv4/
目录下的文件来查看实际的值
# cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
# cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
前面两个参数的单位是秒,最后一个参数的是一个纯数。这意味着 keepalive 在发送第一个探测包之前等到 7200 秒,然后每 75 秒重新发送一次。如果连续 9 次没有收到 ACK,则将此连接标记为中断。
修改值的方式很简单,只要将新的值写入文件。假设要配置为连接闲置 10 分钟后启动 keepalive,然后以一分钟的间隔发送探测,将探测次数增加到20,可以这样配置:
# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
# echo 60 > /proc/sys/net/ipv4/tcp_keepalive_intvl
# echo 20 > /proc/sys/net/ipv4/tcp_keepalive_probes
为了确保一切成功,重新检查这些文件,并确认正在显示是这些新值而不是旧的值。
请记住,procfs
处理特殊文件,并且不能对它们执行任何操作,因为它们只是内核空间中的一个接口,而不是真实文件,所以在使用它们之前先测试脚本。
可以通过 sysctl(8
工具,指定要读取或写入的内容:
# sysctl \
> net.ipv4.tcp_keepalive_time \
> net.ipv4.tcp_keepalive_intvl \
> net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
使用 sysctl -w
来写入:
# sysctl -w \
> net.ipv4.tcp_keepalive_time=600 \
> net.ipv4.tcp_keepalive_intvl=60 \
> net.ipv4.tcp_keepalive_probes=20
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 60
net.ipv4.tcp_keepalive_probes = 20
Sysctl
另外一个访问内核参数的方法是 sysctl (2)
系统调用。当不可用 procfs 时会很有用,因为与内核的通信是直接通过系统调用而不是通过 procfs 子树来执行的。目前没有包含此系统调用的程序(请记住 sysctl(8)不使用它)
配置持久化
我们可以在任何时候更改这几个参数的值,因为keepalive会在每次用到这几个参数的时候重新读取他们的值。所以,如果我们在连接还正常使用的时候修改这几个参数的值,内核便会在下一次使用这个新值。但一般我们会选择在三个地方去设置这几个参数的值:(1)第一次配置网络的时候(2)rc.local脚本里面。一般的发行版里面都包含该脚本,一般认为该脚本执行后用户的设置就算完成了。(3)/etc/sysctl.conf配置文件里面。sysctl工具就是读取和设置该配置文件。
并不是所有的网络应用都需要keepalive的支持,只有TCP支持keepalive,所以我们也只能在TCP套接字中使用keepalive。
0x03 编程中使用 Keepalive
具体可见TCP Keepalive HOWTO Programming applications
0x04 RFC 1122
TCP Keepalive虽不是标准规范,但操作系统一旦实现,默认情况下须为关闭,可以被上层应用开启和关闭。
TCP Keepalive必须在没有任何数据(包括ACK包)接收之后的周期内才会被发送,允许配置,默认值不能够小于2个小时。
不包含数据的ACK段在被TCP发送时没有可靠性保证,即一旦发送,不确保一定发送成功。
规范建议 keepalive 保活包不应该包含数据,但也可以配置为发送包含一个字节的垃圾数据的 keepalive 段,以便与错误的TCP实现兼容。
SEG.SEQ = SND.NXT-1,即TCP保活探测报文序列号将前一个 TCP 报文序列号减 1。SND.NXT = RCV.NXT,即下一次发送正常报文序号等于 ACK 序列号。注意图中 keepalive 的长度和序列号。
不幸的是,除非片段包含数据,否则一些错误的TCP实现无法响应 SEG.SEQ = SND.NXT-1。可能会要求 keepalive 报文必须携带有1个字节的 payload。
TCP Keepalive应该在服务器端启用,客户端不做任何改动;若单独在客户端启用,若客户端异常崩溃或出现连接故障,存在服务器无限期的为已打开的但已失效的文件描述符消耗资源的严重问题。
0x05 参考
https://tools.ietf.org/html/rfc1122#page-101
http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html