心跳检测与连接保活配置
解读
在国内高并发、长连接场景(如直播弹幕、IM、支付回调、物联网终端上报)中,面试官问“心跳”并不只是考你会不会 set_time_limit(0),而是想看你是否真正理解:
- TCP 长连接在 PHP 进程模型下的生命周期;
- 网关/负载均衡(Nginx、OpenResty、SLB、API 网关)对空闲连接的默认熔断策略(普遍 60 s 无数据即 RST);
- PHP-FPM 的“无状态”特性与 Swoole/Workerman 常驻内存模型的差异;
- 如何针对业务特征选择“应用层心跳”还是“TCP KeepAlive”,并把参数配到“操作系统→Web 服务器→PHP 扩展→业务代码”四层,做到可灰度、可观测、可热更新。
一句话:让连接在“不被网关掐、不被框架杀、不空跑占 FD”的前提下,稳定存活,并在异常时秒级感知、快速重连。
知识点
- TCP 保活机制
net.ipv4.tcp_keepalive_time/intvl/probes内核参数含义与默认值- 与 Nginx
keepalive_timeout、keepalive_requests的区别
- PHP-FPM 的“请求级”生命周期
request_terminate_timeout、max_execution_time对长连接的隐性限制
- Swoole/Workerman 常驻进程模型
heartbeat_check_interval、heartbeat_idle_timeonClose、onHeartbeat回调的用法
- 应用层心跳设计
- 二进制 ping/pong 帧 vs. JSON
{"type":"ping"} - 心跳间隔动态退避算法(退避上限 ≤ 网关超时 ½)
- 二进制 ping/pong 帧 vs. JSON
- 观测与治理
- 利用
tcpdump抓包判断 RST 来源 - 把连接存活时间、心跳延迟、重连次数写 Prometheus,通过 Grafana 告警
- 利用
- 国内云厂商“隐藏”限制
- 阿里云 SLB 默认 60 s 空闲断开,腾讯云 CLB 默认 900 s,但 HTTP/HTTPS 监听会强制 60 s
- 微信小程序 WebSocket 10 分钟强制断连,需要业务心跳 < 5 min
答案
“心跳检测与连接保活”在 PHP 体系里要分“运行模式”给出可落地的四层配置方案。
一、PHP-FPM 短连接场景(传统 HTTP)
目标:减少 TIME_WAIT,复用连接,而不是“长心跳”。
- Nginx 反向代理到 FPM 时开启 HTTP/1.1 长连接:
upstream fpm { server 127.0.0.1:9000; keepalive 32; # 复用 32 条长连接 keepalive_timeout 60s; # 与内核匹配 keepalive_requests 500; # 500 次后强制重建,防内存泄漏 } - FPM 池配置
pm.max_requests = 500 # 与 keepalive_requests 对齐 request_terminate_timeout = 60s # 必须 ≥ 网关空闲超时 - 内核(/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、游戏网关)
- Server 端配置
$server->set([ 'heartbeat_check_interval' => 30, // 每 30s 遍历一次 'heartbeat_idle_time' => 120, // 120s 内无收发即踢 'dispatch_mode' => 2, // 固定 FD 分配,避免惊群 ]); - 业务层兜底
在onReceive/onMessage里更新$connection->lastActiveTime,防止“心跳帧”被计入业务 QPS;
提供onHeartbeat回调,写日志 → Prometheus。 - 客户端配合
浏览器 WebSocket:每 50s 发{"type":"ping","t":timestamp},服务端回{"type":"pong","t":同上};
若连续 2 次未收到 pong,客户端主动重连并做指数退避。 - 云厂商限制
若前端有 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 级探测兜底清理“死连接”。
四、灰度与观测
- 把“心跳超时断开”事件打上
reason=heartbeat标签,写日志文件; - Filebeat → Kafka → Flink 计算“连接存活时长”P99,夜间对比不同版本;
- 上线前用
tc qdisc模拟 5% 丢包,验证重连风暴是否打爆ulimit -n。
总结:
PHP 做心跳必须“先分清运行模式”,再“从内核到业务”逐层给参数,最后用监控闭环。回答时把“PHP-FPM 无法做心跳,只能依赖 Nginx+内核;Swoole/Workerman 才有应用层心跳”作为分水岭,再给出数字(30s、60s、120s)和云厂商限制,就能让面试官听到“落地经验”而非“八股文”。
拓展思考
- 如果业务是“百万级物联网终端 2G 网络”,心跳包 1 字节也嫌贵,如何做到“零心跳”还能感知掉线?
提示:利用 TCP 半开检测 + 应用层“上行数据驱动”模型,终端只在需要时上报,服务端用tcp_user_timeout+EOF分包符判断静默掉线,节省 30% 流量。 - 当 Swoole 开启
reactor_num=16,心跳检测会集中在一个线程,导致 10 万连接时 CPU 飙高,如何优化?
提示:把heartbeat_check_interval设为 0,关闭内置检测,自己在Worker进程里按 FD 取模分批customHeartbeat,将 O(n) 均摊到多进程。 - 国内金融级双向认证 TLS 场景,心跳包是否需要加密?
提示:心跳帧走同一 TLS 记录层即可,无需二次加密,但要防止“心跳扩展”被用于 TLS 重协商攻击,需关闭 TLS 重协商并启用ssl_security_level=2。