工业网关最怕的不是连接断开,而是连接看起来还在、业务实际上已经不可用。NAT 超时、链路抖动、设备休眠、进程卡死和中间网络设备静默丢包,都可能制造"假在线"。心跳机制的价值,是把物理链路、协议连接和业务健康拆开判断,让服务端知道设备是否还能工作、数据是否还新鲜、故障应该何时触发告警。
本文按工程落地顺序,从 TCP Keepalive、应用层探活、设备状态上报、服务端健康判定到断线重连,给出可直接复用的设计方法。
一、为什么不能只看 TCP 连接
TCP Keepalive 只能证明内核层面的连接对象还存在。典型的半开连接场景中,一端已经断电或网络已经中断,另一端可能长时间维持 ESTABLISHED 状态。尤其在经过 NAT、4G/5G 专网和工业防火墙时,五元组映射超时与 TCP Keepalive 周期并不一致。
因此工业网关至少要区分三层健康:
- 链路层是否可达,例如网卡、蜂窝模块、VPN 隧道。
- 连接层是否活跃,例如 TCP Keepalive、TLS 会话、MQTT 会话。
- 业务层是否可服务,例如采集线程、协议解析、缓存落盘和关键任务队列。
只有第三层仍然正常,设备才算真正在线。
二、心跳参数怎么定
心跳频率不能只凭经验写一个 30 秒。参数要同时匹配网络成本、故障发现时间和业务容忍度。
- 心跳间隔:常见为 30 秒到 60 秒;低功耗或按流量计费设备可适当拉长。
- 超时次数:建议连续 3 到 5 次未收到有效响应再判离线,避免一次抖动造成误报。
- 判定窗口:离线阈值应大于网络最大抖动周期,通常为心跳间隔乘以失败次数。
- 双向上报:客户端发心跳,服务端也要能下发探活或能力协商。
- 业务承载:心跳包可以携带时间戳、版本、序列号、CPU、内存、温度和队列深度。
一个实用规则是:告警窗口要小于业务允许的最大数据空窗。如果采集任务要求 5 分钟内必须发现故障,就不要设计成 10 分钟才判定离线。
三、TCP Keepalive 只做底层补充
Linux 可以通过 sysctl 调整全局 Keepalive 参数:
bash
# 空闲 600 秒后开始探测
sudo sysctl -w net.ipv4.tcp_keepalive_time=600
# 每次探测间隔 60 秒
sudo sysctl -w net.ipv4.tcp_keepalive_intvl=60
# 连续 3 次失败后判定连接失效
sudo sysctl -w net.ipv4.tcp_keepalive_probes=3
单个长连接也可以在应用代码中覆盖系统配置:
python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3)
这些参数适合发现操作系统层面的死连接,但不能替代应用层心跳。把 TCP Keepalive 当作兜底,而不是健康判定的唯一依据。
四、应用层心跳要能反映业务状态
WebSocket 双向探活
WebSocket 连接适合使用协议自带的 ping/pong。客户端定时发送 ping,并为 pong 设置超时;连续失败后主动关闭旧连接并进入重连流程。
python
import asyncio
import websockets
async def heartbeat(ws):
while True:
try:
pong = await asyncio.wait_for(ws.ping(), timeout=10)
await pong
except asyncio.TimeoutError:
log.warning("heartbeat timeout")
await ws.close()
return
await asyncio.sleep(30)
async with websockets.connect("wss://edge-gateway.local/ws") as ws:
asyncio.create_task(heartbeat(ws))
async for message in ws:
await process(message)
MQTT 利用 Keepalive 与会话状态
MQTT 的 Keepalive 由客户端和 Broker 协商,Broker 在约 1.5 倍周期内没有收到报文时可以断开连接。对工业网关来说,还要结合遗嘱消息、会话过期和主题订阅恢复判断业务是否真正恢复。
python
import paho.mqtt.client as mqtt
client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
)
client.will_set(
topic="gateway/status",
payload='{"online":false,"reason":"unexpected_disconnect"}',
qos=1,
retain=True,
)
client.connect("mqtt-broker.local", 1883, keepalive=60)
五、设备心跳最好携带状态快照
只发送 ping 或固定字符串,服务端无法判断设备是不是卡在"能回包但业务已停滞"的状态。建议心跳里加入时间戳、运行时长、关键线程状态和资源指标。
python
import asyncio
import time
class DeviceHeartbeat:
def __init__(self, device_id):
self.device_id = device_id
self.last_seen = time.time()
self.online = True
async def send_heartbeat(self):
while self.online:
payload = {
"device_id": self.device_id,
"timestamp": int(time.time()),
"uptime": get_uptime(),
"status": {
"cpu": get_cpu_usage(),
"memory": get_memory_usage(),
"disk": get_disk_usage(),
"queue_depth": get_queue_depth(),
},
}
try:
await self.send(payload)
self.last_seen = time.time()
except Exception as exc:
log.error("heartbeat failed: %s", exc)
await asyncio.sleep(30)
状态快照不要无限膨胀。优先选择能反映服务能力的少量字段,并明确单位、采样窗口和缺失值语义。
六、服务端健康判定要使用状态机
服务端不应在单次超时后立刻把设备标记离线。推荐设置 ONLINE、SUSPECT、OFFLINE 和 RECOVERING 四个状态,通过连续失败、恢复心跳和业务探针共同迁移。
python
class HealthMonitor:
def __init__(self, timeout=120, suspect_after=2):
self.timeout = timeout
self.suspect_after = suspect_after
self.devices = {}
def record_heartbeat(self, device_id, payload):
now = time.time()
item = self.devices.setdefault(device_id, {"misses": 0})
item["last_seen"] = now
item["misses"] = 0
item["status"] = "ONLINE"
item["last_payload"] = payload
async def check_loop(self):
while True:
now = time.time()
for device_id, item in list(self.devices.items()):
silence = now - item.get("last_seen", 0)
if silence > self.timeout:
await self.mark_offline(device_id)
elif silence > self.timeout / 2:
item["status"] = "SUSPECT"
await asyncio.sleep(10)
async def mark_offline(self, device_id):
log.warning("device %s offline", device_id)
await event_bus.publish("device.offline", {"device_id": device_id})
真实系统还要处理时钟漂移。设备时间不可信时,服务端应以接收时间计算超时,同时保存设备上报时间用于检查 NTP 偏差。
七、重连必须有退避和抖动
大量网关同时掉线后,如果都以固定一秒间隔重连,会把入口瞬间打满。指数退避需要设置最大值,并加入随机抖动,避免重连风暴。
python
import asyncio
import random
class ReconnectingClient:
def __init__(self):
self.shutdown = False
async def run(self):
backoff = 1
max_backoff = 60
while not self.shutdown:
try:
async with self.connect() as conn:
backoff = 1
await self.handle(conn)
except ConnectionError as exc:
delay = min(backoff, max_backoff)
delay = random.uniform(delay * 0.8, delay * 1.2)
log.warning("disconnected: %s, retry in %.1fs", exc, delay)
await asyncio.sleep(delay)
backoff = min(backoff * 2, max_backoff)
重连成功后不要只记录"连接恢复"。还要确认订阅、证书、时间同步、缓存补发和关键任务是否恢复到可服务状态。
八、监测指标至少覆盖这几类
- 心跳发送成功率、服务端接收成功率。
- 从最后一次有效心跳到当前时间的静默时长。
- 设备状态迁移次数、频繁上下线设备列表。
- 重连次数、平均恢复时间和退避分布。
- 心跳携带的 CPU、内存、磁盘和队列深度。
- 心跳包大小、发送频率和网络流量成本。
告警去噪也很重要。短时间抖动可以合并为一次事件,持续离线才升级为高优先级工单。
九、常见坑
- 只依赖 TCP Keepalive,导致半开连接长期占用资源。
- 心跳间隔过短,低功耗设备和蜂窝网络流量成本升高。
- 没有双向探活,只能发现客户端到服务端的单向故障。
- 单次超时即判离线,告警被网络抖动淹没。
- 重连没有退避与抖动,故障恢复时形成连接风暴。
- 心跳只回固定字符串,无法发现业务线程卡死。
- 未统一时钟,时间戳漂移导致误判和审计困难。
十、运行时层面的配合
协议运行时或边缘操作系统可以把设备心跳、连接管理、健康状态和事件总线统一起来。例如 Zenova EdgeOS 这类运行时,可以把心跳接收、状态机、离线事件和重连策略放到公共层,业务应用只上报关键状态与探针结果。这样不同协议、不同设备型号不需要各自重复实现一套脆弱的保活逻辑。
基础 License ¥400/台起。是否引入运行时,关键看设备规模、协议数量和运维成本,而不是只看单台实现难度。
十一、TL;DR
工业网关心跳要同时解决连接活跃和业务健康两类问题。TCP Keepalive 负责兜底,WebSocket/MQTT 等应用层协议负责探活,设备心跳携带状态快照,服务端用状态机做健康判定,重连采用指数退避与随机抖动,最后用成功率、静默时长、上下线次数和恢复时间建立监测闭环。
下一步建议
- 先定义业务允许的最大数据空窗,再反推心跳间隔和超时次数。
- 建立
ONLINE、SUSPECT、OFFLINE、RECOVERING状态机。 - 给心跳增加最小状态快照,覆盖资源与关键任务队列。
- 为重连加入指数退避、随机抖动和恢复后的业务自检。
- 用指标验证误报率、漏报率和故障恢复时间,再逐步调参。