蛮荆

为什么应用层心跳检测是必要的

2020-10-17

TCP 心跳

TCP Keepalive 是一种用于检测 TCP 连接是否活跃的机制,通过定期发送探测数据包来确定连接的状态,主要用于检测空闲 (僵尸) 连接、保持 NAT 映射 (NAT 设备、防火墙设备) 等。

原理简述

  1. 要启用 TCP Keepalive 自动检测机制,需要通信双方都开启 Keepalive 选项
  2. 如果在一定时间(默认 2 小时)内没有数据传输,TCP 会发送一个 Keepalive 探测数据包
  3. 如果通信的对方仍然活跃,就会对该探测数据包进行响应,如果对方没有响应,TCP 将重试发送探测数据包
  4. 在达到最大重试次数(默认 10 次)后,如果仍然未收到响应,TCP 将认为连接已断开,关闭连接

下面是根据 TCP Keepalive 工作原理,转换后的逻辑伪代码 (针对单个 TCP 连接)。

# TCP Keepalive 控制参数
KEEPALIVE_INTERVAL = 7200  # 默认 2 小时
KEEPALIVE_PROBES = 10   # 默认 10 次
KEEPALIVE_TIMEOUT = 75  # 默认 75 秒

# 开启 TCP Keepalive 机制
# 初始化各项控制参数
def enable_keepalive(socket):
    socket.setsockopt(..., socket.SO_KEEPALIVE, 1)

    socket.setsockopt(..., KEEPALIVE_INTERVAL)
    socket.setsockopt(..., KEEPALIVE_PROBES)
    socket.setsockopt(... KEEPALIVE_TIMEOUT)

# 如果连接在 Keepalive 间隔时间内处于空闲状态
# 发送 Keepalive 探测包并启动探测计时器
def send_keepalive_probe(socket):
    if is_idle(socket, KEEPALIVE_INTERVAL):
        send_probe_packet(socket)
        start_probe_timer(socket)

# 处理 Keepalive 响应
# 如果收到探测包的 ACK 确认,则重置空闲计时器
# 否则增加探测次数
#   如果超过最大探测次数,则关闭连接
# Function to handle keepalive response
def handle_keepalive_response(socket):
    if received_probe_ack(socket):
        reset_idle_timer(socket)
    else:
        increment_probe_count(socket)
        
        if probe_count(socket) > KEEPALIVE_PROBES:
            close_connection(socket)

# 检查Keepalive超时
# 如果探测计时器过期,则处理 Keepalive 响应
def check_keepalive_timeout(socket):
    if probe_timer_expired(socket):
        handle_keepalive_response(socket)

# 核心主循环管理 Keepalive
# 1. 启用 Keepalive 选项
# 2. 在连接打开时定期发送探测包和检查超时
# Main loop to manage keepalive
def manage_keepalive(socket):
    enable_keepalive(socket)
    
    while is_open(socket):
        send_keepalive_probe(socket)
        check_keepalive_timeout(socket)
        time.sleep(KEEPALIVE_TIMEOUT) 

...
...

相关参数

Linux 内核中和 TCP Keepalive 机制相关的几个参数如下:

  • tcp_keepalive_time:首次探测之前的空闲时间(默认 2 小时)
  • tcp_keepalive_intvl:重试探测的时间间隔(默认 75 秒)
  • tcp_keepalive_probes:最大重试次数(默认 10 次)

当然,这些参数都可以通过修改系统配置文件进行修改,尤其在优化高并发场景和移动场景为主的后端服务器时,这几个参数需要着重优化一下:

# 设置首次探测之前的空闲时间为 10 分钟
echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time

# 设置重试探测的时间间隔为 15 秒
echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl

# 设置最大重试次数为 3 次
echo 3 > /proc/sys/net/ipv4/tcp_keepalive_probes

运行 sysctl -p 命令生效,重启之后仍然有效。

局限性

TCP Keepalive 机制由内核 (操作系统) 负责执行,当进程退出后,内核会针对进程中未关闭的连接逐个进行关闭 (向连接的通信对方发送 FIN 报文),这样就保证了每个连接的通信双方都可以知道通信的状态,并根据状态来完成不同的具体业务逻辑。

表面上看,不论进程是运行还是退出,TCP Keepalive 机制都可以通过内核很好地完成,但是在一些极端场景中,内核无法保证 TCP 协议栈正常工作,例如:

  • 操作系统异常导致重启,TCP 协议栈没有机会发送 FIN 报文
  • 服务器硬件故障、基础设置故障 (如断电、断网、地理不可抗力因素),TCP 协议栈同样没有机会发送 FIN 报文
  • 海量并发连接数,操作系统或进程重启时,TCP 协议栈可能无法断开所有连接,也就是 FIN 报文出现丢包后,没有更多的时间进行重试
  • 网络链路故障,只能等到 TCP Keepalive 检测超时,通信双方才能确认这种情况,此时距离发生故障可能已经过去了一段时间

应用层心跳

必要性

前文中讲到了 TCP Keepalive 机制 (内核实现) 的局限性,除此之外,结合到应用层一起来看的话,TCP Keepalive 机制无法确认应用层的心跳检测目标:应用程序还在正常工作。具体来说,TCP Keepalive 检测结果正常,只能说明两件事情:

  1. 应用程序 (进程) 还存在
  2. 网络链路正常

但是 当应用程序进程运行中发生异常时,例如死锁、Bug 导致的无限循环、无限阻塞 等,虽然此时操作系统依然可以正常执行 TCP Keepalive 机制,但是对于应用程序的异常情况,通信对方是无法得知的。

此外,应用层心跳检测具有更好的灵活性,例如可以控制检测时间、间隔、异常处理机制、附加额外数据等。

综上所述,应用层心跳检测是必须实现的。

实现方式

常见的应用层心跳实现方式有:

  • HTTP: 访问指定 URL, 根据响应码或者响应数据来判定应用是否正常
  • Exec: 执行指定 (Shell) 命令 (例如文件检查、网络检查),并检查命令的退出状态码,如果状态码为 0,说明应用正常运行
  • WebSocket: 和 HTTP 检测方式类似
  • 其他自定义检测方式

其中业界主流的检测方式是 HTTP (长连接方式), 主要是因为:

  1. HTTP 实现简单,基于长连接的方式避免了连接的建立和释放带来的开销
  2. HTTP 对于 (异构) 环境的要求很低,而且大多数应用中都使用 HTTP 作为 API 主要通信协议,心跳检测并不会带来多少额外的工作量

实现细节

1. 不要单独实现 “心跳线程”

使用单独的线程来实现 “心跳检测”,虽然可以将心跳检测应用代码和具体的业务逻辑代码隔离,但是当 “业务线程” 发生死锁或者 Bug 崩溃时,心跳线程检测不到。

所以应该将心跳检测直接实现在 “业务线程” 中。

2. 不要单独实现 “心跳连接”

对于网络 (例如 TCP) 编程的场景,心跳检测应该在 “业务连接” 直接实现,而不是使用单独的连接,这样当业务连接出现异常时,通信对方可以第一时间感知到 (没有及时收到心跳响应)。

此外,大多数网络防火墙会定时监测空闲 (僵尸) 连接并清除,如果心跳检测使用额外的连接,那么当 “业务连接” 长时间没有要发送的数据时,就已经被防火墙断开了,但是此时心跳检测连接还在正常工作,这会影响通信对方的判断,以为 “业务连接” 还在正常工作。

所以应该将心跳检测直接实现在 “业务连接” 中。

扩展阅读

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。