4、心跳检测机制:TCP Keep-Alive、应用层心跳、超时重试策略

说到交易系统的容错,心跳检测是我最常被问到的话题之一。

很多人觉得心跳嘛,不就是定时发个包确认对方活着?其实没那么简单。我见过不少系统,心跳机制写得挺漂亮,结果一上线就出问题——要么误判断连导致频繁切换,要么心跳本身成了性能瓶颈。

今天咱们就把这块掰开揉碎了讲清楚。

4.1 为什么需要心跳检测?

交易系统里,网络连接是不可靠的。TCP 连接看起来还连着,实际上对端可能已经挂了。这种现象叫「半开连接」(Half-Open Connection)。

举个例子:你下单的网关进程突然崩溃了,但操作系统还没来得及发 FIN 包。你的交易客户端还傻傻地以为连接正常,继续发单——结果单子全丢了。

心跳检测就是为了解决这个问题。它定期确认对端是否存活,一旦发现异常,立即触发重连或切换。

核心目标: 在可接受的时间内,检测出连接失效,并触发恢复动作。

4.2 TCP Keep-Alive:系统自带的心跳

TCP 协议本身提供了一个 Keep-Alive 机制。说白了,就是操作系统帮你定期发一个空数据包,看看对方有没有回应。

我个人的习惯是:不依赖 TCP Keep-Alive 做核心检测。为什么?

  • 检测周期太长:默认情况下,Linux 要等 2 小时才发第一个探测包。就算你调参数,也很难低于 10 秒。
  • 粒度太粗:它只能检测 TCP 层是否通,应用层卡死了它不知道。
  • 跨平台不一致:Windows、Linux、macOS 的参数设置完全不同。

不过,TCP Keep-Alive 也不是完全没用。我一般把它当作最后一道防线——应用层心跳失效时,它能兜底清理死连接。

实用建议: 如果你用 Linux,可以这样调整 Keep-Alive 参数:
sysctl -w net.ipv4.tcp_keepalive_time=30 (30秒后开始探测)
sysctl -w net.ipv4.tcp_keepalive_intvl=5 (每次间隔5秒)
sysctl -w net.ipv4.tcp_keepalive_probes=3 (连续3次无响应则断开)

4.3 应用层心跳:真正的核心

交易系统里,我们几乎都用应用层心跳。说白了,就是在业务协议里加一个专门的 Ping/Pong 消息。

我曾经在一个期货高频交易项目里,就因为心跳设计不合理,导致频繁误切。后来我总结了一套经验:

4.3.1 心跳协议设计

最简单的做法:客户端定时发 Ping,服务端回复 Pong。但这里有几个坑:

  • Ping 和 Pong 要带序列号:防止历史心跳包干扰判断
  • 服务端也要主动发心跳:防止客户端单方面认为连接正常
  • 心跳包要轻量:别带业务数据,就一个消息头加时间戳
// 一个典型的心跳消息结构
struct HeartbeatMessage {
    uint8_t  type;       // 0x01 = Ping, 0x02 = Pong
    uint32_t sequence;   // 递增序列号
    uint64_t timestamp;  // 发送时间戳(纳秒)
    uint8_t  reserved[7]; // 对齐填充
} __attribute__((packed));

4.3.2 心跳间隔与超时

这是最容易出问题的地方。我见过有人把心跳间隔设成 1 秒,超时设成 3 秒——结果网络稍微抖动一下,就开始切主备了。

我的经验公式:

场景 心跳间隔 超时次数 总超时时间
同机房交易 1 秒 3 次 3 秒
同城跨机房 2 秒 3 次 6 秒
异地灾备 5 秒 2 次 10 秒
避坑指南: 我曾经把超时次数设成 1 次,结果网络偶尔丢一个包就触发切换,导致交易中断。后来改成至少 3 次连续失败才判定断连,误判率降到了 0.01% 以下。

4.4 超时重试策略

心跳检测发现连接断了,接下来怎么办?直接重连?别急,这里也有讲究。

4.4.1 指数退避(Exponential Backoff)

我最常用的策略。每次重试间隔翻倍,防止雪崩效应。

import time
import random

def connect_with_backoff(max_retries=5, base_delay=0.1):
    """
    带指数退避的重连策略
    """
    for attempt in range(max_retries):
        try:
            # 尝试建立连接
            connection = create_connection()
            return connection
        except ConnectionError as e:
            # 计算退避时间:base * 2^attempt + 随机抖动
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.1)
            print(f"第 {attempt+1} 次重连失败,等待 {delay:.2f} 秒...")
            time.sleep(delay)
    
    raise Exception("重连失败,已达最大重试次数")

4.4.2 快速重试 + 慢速恢复

交易系统里,我更喜欢这种策略:

  • 前 3 次:间隔 100ms,快速尝试恢复
  • 3-10 次:间隔 1 秒,中等频率
  • 10 次以后:间隔 10 秒,降低频率防止打满 CPU

这样做的好处是:如果是瞬断,能快速恢复;如果是持续故障,不会浪费资源。

4.4.3 熔断机制

连续重试失败超过一定次数,我建议直接熔断。什么意思?就是停止重试,上报告警,等待人工介入。

熔断阈值参考:
- 连续 15 次重试失败 → 熔断
- 熔断持续时间:30 秒
- 熔断结束后:尝试半开探测(发一次心跳,成功则恢复,失败则继续熔断)

4.5 整体架构图

下面这张图展示了心跳检测与重试的完整流程:

心跳检测与重试策略流程图 交易客户端 交易服务端 Ping (seq=1, ts=...) Pong (seq=1, ts=...) 心跳超时检测 连续失败计数 ≥3次? 触发重连 + 指数退避 熔断 + 告警 否(已达最大重试次数) Ping Pong 检测 计数 判断 重连 熔断

4.6 实战中的几个关键点

最后,分享几个我在实战中踩过的坑:

  1. 心跳不要和业务消息混在一起:我曾经把心跳和行情数据放在同一个线程处理,结果行情拥堵时心跳也发不出去,导致误判。后来单独开一个线程处理心跳。
  2. 记录心跳日志:每次心跳超时或重连,都要打日志。排查问题时,这些日志就是你的救命稻草。
  3. 测试网络抖动场景:用 tc 命令模拟丢包、延迟,看看你的心跳机制能不能扛住。我一般会测 1%、5%、10% 三种丢包率。
  4. 考虑时钟同步:如果心跳带时间戳,确保客户端和服务端的时钟是同步的。NTP 服务一定要配好。
一个小技巧: 在开发环境,我会把心跳间隔设成 100ms,超时次数设成 2 次。这样能快速验证心跳逻辑是否正确。上线前再改成生产环境的参数。

嗯,心跳检测这块内容就这些。说白了,就是「定期确认对方活着,死了就赶紧重连」。但细节决定成败——间隔设多少、超时几次、重试策略怎么选,每个参数都直接影响系统的稳定性。

我见过太多系统,因为心跳参数没调好,导致频繁切换、交易中断。希望今天的分享能帮你避开这些坑。


专注资料整理