心跳检测与连接保活配置

解读

在国内高并发、长连接场景(如直播弹幕、IM、支付回调、物联网终端上报)中,面试官问“心跳”并不只是考你会不会 set_time_limit(0),而是想看你是否真正理解:

  1. TCP 长连接在 PHP 进程模型下的生命周期;
  2. 网关/负载均衡(Nginx、OpenResty、SLB、API 网关)对空闲连接的默认熔断策略(普遍 60 s 无数据即 RST);
  3. PHP-FPM 的“无状态”特性与 Swoole/Workerman 常驻内存模型的差异;
  4. 如何针对业务特征选择“应用层心跳”还是“TCP KeepAlive”,并把参数配到“操作系统→Web 服务器→PHP 扩展→业务代码”四层,做到可灰度、可观测、可热更新。

一句话:让连接在“不被网关掐、不被框架杀、不空跑占 FD”的前提下,稳定存活,并在异常时秒级感知、快速重连。

知识点

  1. TCP 保活机制
    • net.ipv4.tcp_keepalive_time/intvl/probes 内核参数含义与默认值
    • 与 Nginx keepalive_timeoutkeepalive_requests 的区别
  2. PHP-FPM 的“请求级”生命周期
    • request_terminate_timeoutmax_execution_time 对长连接的隐性限制
  3. Swoole/Workerman 常驻进程模型
    • heartbeat_check_intervalheartbeat_idle_time
    • onCloseonHeartbeat 回调的用法
  4. 应用层心跳设计
    • 二进制 ping/pong 帧 vs. JSON {"type":"ping"}
    • 心跳间隔动态退避算法(退避上限 ≤ 网关超时 ½)
  5. 观测与治理
    • 利用 tcpdump 抓包判断 RST 来源
    • 把连接存活时间、心跳延迟、重连次数写 Prometheus,通过 Grafana 告警
  6. 国内云厂商“隐藏”限制
    • 阿里云 SLB 默认 60 s 空闲断开,腾讯云 CLB 默认 900 s,但 HTTP/HTTPS 监听会强制 60 s
    • 微信小程序 WebSocket 10 分钟强制断连,需要业务心跳 < 5 min

答案

“心跳检测与连接保活”在 PHP 体系里要分“运行模式”给出可落地的四层配置方案。

一、PHP-FPM 短连接场景(传统 HTTP)
目标:减少 TIME_WAIT,复用连接,而不是“长心跳”。

  1. Nginx 反向代理到 FPM 时开启 HTTP/1.1 长连接:
    upstream fpm {
        server 127.0.0.1:9000;
        keepalive 32;               # 复用 32 条长连接
        keepalive_timeout 60s;      # 与内核匹配
        keepalive_requests 500;     # 500 次后强制重建,防内存泄漏
    }
    
  2. FPM 池配置
    pm.max_requests = 500          # 与 keepalive_requests 对齐
    request_terminate_timeout = 60s # 必须 ≥ 网关空闲超时
    
  3. 内核(/etc/sysctl.conf)
    net.ipv4.tcp_keepalive_time = 60
    net.ipv4.tcp_keepalive_intvl = 10
    net.ipv4.tcp_keepalive_probes = 3
    

说明:PHP-FPM 本身无法发心跳,只能依赖 OS 级 TCP KeepAlive 做“死后清理”,真正的“保活”由 Nginx 与客户端完成。

二、Swoole 常驻进程场景(WebSocket、MQTT、游戏网关)

  1. Server 端配置
    $server->set([
        'heartbeat_check_interval' => 30,   // 每 30s 遍历一次
        'heartbeat_idle_time'      => 120,  // 120s 内无收发即踢
        'dispatch_mode'            => 2,    // 固定 FD 分配,避免惊群
    ]);
    
  2. 业务层兜底
    onReceive/onMessage 里更新 $connection->lastActiveTime,防止“心跳帧”被计入业务 QPS;
    提供 onHeartbeat 回调,写日志 → Prometheus。
  3. 客户端配合
    浏览器 WebSocket:每 50s 发 {"type":"ping","t":timestamp},服务端回 {"type":"pong","t":同上}
    若连续 2 次未收到 pong,客户端主动重连并做指数退避。
  4. 云厂商限制
    若前端有 SLB,把 heartbeat_idle_time 调到 55s 以内,防止 SLB 先 RST 导致 Swoole 误判为“异常断开”。

三、Workerman 场景(物联网 TCP 网关)

$gateway = new Gateway("tcp://0.0.0.0:8282");
$gateway->pingInterval = 30;
$gateway->pingNotResponseLimit = 1;  // 一次 pong 超时即踢
$gateway->pingData = '';             // 二进制空包,节省流量

内核同时打开 TCP KeepAlive,双保险:Workerman 发现超时立即回收 FD,OS 级探测兜底清理“死连接”。

四、灰度与观测

  1. 把“心跳超时断开”事件打上 reason=heartbeat 标签,写日志文件;
  2. Filebeat → Kafka → Flink 计算“连接存活时长”P99,夜间对比不同版本;
  3. 上线前用 tc qdisc 模拟 5% 丢包,验证重连风暴是否打爆 ulimit -n

总结:
PHP 做心跳必须“先分清运行模式”,再“从内核到业务”逐层给参数,最后用监控闭环。回答时把“PHP-FPM 无法做心跳,只能依赖 Nginx+内核;Swoole/Workerman 才有应用层心跳”作为分水岭,再给出数字(30s、60s、120s)和云厂商限制,就能让面试官听到“落地经验”而非“八股文”。

拓展思考

  1. 如果业务是“百万级物联网终端 2G 网络”,心跳包 1 字节也嫌贵,如何做到“零心跳”还能感知掉线?
    提示:利用 TCP 半开检测 + 应用层“上行数据驱动”模型,终端只在需要时上报,服务端用 tcp_user_timeout + EOF 分包符判断静默掉线,节省 30% 流量。
  2. 当 Swoole 开启 reactor_num=16,心跳检测会集中在一个线程,导致 10 万连接时 CPU 飙高,如何优化?
    提示:把 heartbeat_check_interval 设为 0,关闭内置检测,自己在 Worker 进程里按 FD 取模分批 customHeartbeat,将 O(n) 均摊到多进程。
  3. 国内金融级双向认证 TLS 场景,心跳包是否需要加密?
    提示:心跳帧走同一 TLS 记录层即可,无需二次加密,但要防止“心跳扩展”被用于 TLS 重协商攻击,需关闭 TLS 重协商并启用 ssl_security_level=2