工业边缘系统里的心跳,不只是"每隔 30 秒发一个包"。它至少承担三层职责:让中间网络设备不回收空闲连接,让通信双方尽早发现半开连接,让平台能够判断设备和关键业务链路是否健康。
先看一个典型场景:一台部署在 4G 专网里的工业网关,通过 MQTT 长连接持续上送采集数据。如果链路长时间没有流量,运营商 NAT 会悄悄回收这条会话;此时网关侧可能毫无感知,直到下一次上报失败才发现连接已经失效。更隐蔽的是半开连接------网关断电或网线被拔出后,服务端 TCP 栈仍认为连接存在,平台界面一直显示"在线",实际设备早已失联。这类问题无法靠业务数据本身暴露,必须由心跳机制主动探测、尽早暴露。
心跳的价值正在于此:它把"连接是否可用"从"业务是否正常"中独立出来,用最小的流量成本持续验证链路活性。对工业场景而言,一次误判离线可能触发不必要的重连和补传,一次漏判离线则可能让运维在设备真正故障前毫无察觉。因此心跳不是可有可无的保活包,而是整个设备可用性判断的基础设施。
这篇文章面向工业网关开发者、协议运行时工程师和运维负责人,梳理 TCP Keepalive、应用层心跳、设备健康判定、断线重连和监控指标的设计细节。全文从"为什么需要心跳"出发,逐步展开核心参数、协议边界、设备侧与服务端设计、重连策略和监控告警,最后给出常见坑位清单,帮助你在自己的系统里落地一套完整、可运维的心跳方案。
一、为什么需要心跳
长连接在工业现场会遇到几类典型问题,每一类都会直接影响设备可用性判断:
- NAT / 防火墙超时:4G 运营商网络、私有 APN 和企业防火墙会回收长时间没有流量的会话。这类超时通常不可见,设备侧无法预知回收时刻,只能靠心跳主动维持会话活性;
- 半开连接:一端掉电、网线拔出或网络切换后,另一端可能仍认为 TCP 连接存在。此时 TCP 层没有 FIN/RST 通知,对端只能通过超时探测才能发现异常;
- 应用假死:TCP 栈还能应答,但协议进程死锁、队列阻塞或业务线程已经停止处理。连接层面一切正常,业务却已停滞,这类问题 TCP Keepalive 无法发现;
- 中间设备故障:连接看似建立,但报文已经无法端到端送达。TLS 代理、负载均衡或协议网关可能让本段 TCP 存活,端到端链路却已异常;
- 设备状态变化:网关在线不代表采集、上送、OTA 或控制链路都正常。设备进程活着,但某个业务模块可能已经退出或卡死。
这些问题的共同点是:连接状态与业务状态并不等价。只看 TCP 连接是否建立,无法回答"设备是否真的在工作"这个问题。
- NAT / 防火墙超时:4G 运营商网络、私有 APN 和企业防火墙会回收长时间没有流量的会话;
- 半开连接:一端掉电、网线拔出或网络切换后,另一端可能仍认为 TCP 连接存在;
- 应用假死:TCP 栈还能应答,但协议进程死锁、队列阻塞或业务线程已经停止处理;
- 中间设备故障:连接看似建立,但报文已经无法端到端送达;
- 设备状态变化:网关在线不代表采集、上送、OTA 或控制链路都正常。
因此,心跳设计要区分四个概念:
| 概念 | 回答的问题 |
|---|---|
| Keepalive | 连接空闲时,路径是否还能收发 |
| Liveness | 通信对端或协议进程是否还活着 |
| Health | 关键业务功能是否正常 |
| Availability | 当前设备能否完成业务目标 |
心跳只能直接证明"心跳路径可用"。要判断健康,还需要结合数据新鲜度、任务状态、资源水位和错误率。例如:心跳正常但采集数据已停滞 10 分钟,说明采集任务可能卡死;心跳正常但上送队列持续增长,说明上行链路或服务端处理出现瓶颈;心跳正常但 CPU 或内存持续高位,说明设备可能即将进入不稳定状态。因此,心跳是健康判断的必要条件,但不是充分条件------它负责回答"链路是否活着",而"业务是否健康"需要结合更多维度综合判断。
二、心跳机制的核心参数
1. 心跳周期
30-60 秒是常见起点,但不能机械套用。周期应由路径空闲超时、业务容忍度、流量成本和设备功耗决定。周期设置过短,会带来不必要的流量和功耗开销,大量设备还会放大服务端压力;周期设置过长,则可能让 NAT 会话先于心跳被回收,或让离线判定时间过长,影响故障响应速度。
一个合理的推导思路是:先确认链路路径上的最小空闲超时(如运营商 NAT 的 120 秒),再留出安全余量,把心跳周期定在超时时间的一半左右;随后结合业务对离线判定的容忍度,反推超时阈值和连续失败次数。例如路径超时 120 秒、业务容忍 90 秒内发现离线,心跳周期可定为 30 秒,连续 3 次未收到即判定离线,总判定时间约 90 秒,刚好落在容忍范围内。
建议规则:
- 心跳周期应小于 NAT / 防火墙空闲超时,并保留安全余量;
- MQTT keepalive 通常从 60 秒开始评估;
- 电池或低流量设备可以使用更长周期,但必须有明确的离线判定时间;
- 大量设备应加入随机抖动,避免同一时刻集中心跳造成惊群;
- 心跳不应携带大体积状态数据,避免把保活包变成遥测流量。
2. 超时判定
不要因为一次丢包就判定离线。工业无线网络本身存在抖动,推荐使用连续丢失次数或时间窗判定。
示例策略:
- 心跳周期 30 秒;
- 连续 3 次未收到,或 90-120 秒未见有效心跳;
- 从
online切到offline前可进入degraded或unknown; - 恢复收到心跳后再切回
online,并记录状态迁移原因。
阈值要可配置。不同链路、不同客户和不同协议的容忍度不一样。
3. 心跳方向
只由客户端发心跳并不总是足够。更完整的模型包含:
- 链路心跳:确认 TCP / TLS / MQTT / WebSocket 通道可用;
- 应用心跳:确认协议任务仍在收发处理;
- 设备心跳:上报网关运行状态和业务模块状态;
- 服务端探活:必要时由服务端发起只读健康查询。
双向心跳不是简单地两边都发包,而是明确谁负责证明哪一层活着,以及超时后谁负责断开、重连和告警。
4. 业务承载
心跳可以分成两类:
- 纯心跳:只包含设备 ID、序号和时间,开销小,语义清楚;
- 状态心跳:附带版本、运行时长、CPU / 内存、网络信号、模块状态等轻量信息。
不要把完整采集数据、全量日志或大批量指标塞进心跳。状态心跳适合承载少量摘要,详细数据应走独立通道。
三、TCP Keepalive 的边界
TCP Keepalive 是操作系统 TCP 栈提供的保活机制。连接空闲一段时间后,内核会发送探测报文,连续失败后判定连接死亡。
Linux 常用系统级参数:
bash
# 当前运行时生效
sudo sysctl -w net.ipv4.tcp_keepalive_time=600
sudo sysctl -w net.ipv4.tcp_keepalive_intvl=60
sudo sysctl -w net.ipv4.tcp_keepalive_probes=3
# 持久化需要写入 /etc/sysctl.d/ 下的配置文件,并执行 sysctl --system
注意,这些是系统级参数,会影响本机所有启用 Keepalive 的 TCP 连接。生产环境修改前应评估影响,并纳入配置管理。
Linux 上也可以对单个 socket 设置:
python
import socket
sock = socket.socket()
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)
不同操作系统的选项名存在差异,macOS 等平台并不使用同一组 TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT 常量。跨平台网关应做适配,不能假设代码在所有 Linux 发行版和嵌入式系统上都可用。
TCP Keepalive 能发现部分死连接,但它有明确限制:
- 只覆盖 TCP 层,不能证明应用进程还在处理业务;
- TLS 代理、负载均衡和协议网关可能让本段 TCP 存活,端到端链路已经异常;
- 默认探测周期通常太长,不适合快速判定设备离线;
- 无法表达设备业务状态、采集状态或上送队列状态。
所以工业网关通常采用"TCP Keepalive + 应用层心跳 + 业务健康检查"的组合。
四、应用层心跳
1. WebSocket Ping / Pong
WebSocket 协议自带 ping / pong 控制帧,适合作为链路和应用层保活。下面是更完整的异步示例:
python
import asyncio
import contextlib
import logging
import websockets
log = logging.getLogger(__name__)
async def heartbeat(ws, interval=30, timeout=10):
while True:
await asyncio.sleep(interval)
try:
# websockets 的正确顺序是:先 await ping() 拿到 pong waiter,
# 再等待 pong waiter 完成。
pong_waiter = await ws.ping()
await asyncio.wait_for(pong_waiter, timeout)
except (asyncio.TimeoutError, ConnectionError):
log.warning("websocket heartbeat timeout")
await ws.close()
return
async def consume(url):
async with websockets.connect(url) as ws:
task = asyncio.create_task(heartbeat(ws))
try:
async for message in ws:
await process(message)
finally:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
这里的超时对象是"等待 pong 返回",不是等待 ping 发送完成。任务退出时也应主动 cancel,避免遗留后台协程。
2. MQTT Keepalive
MQTT 的 keepalive 由协议层处理。连接空闲时客户端发送 PINGREQ,broker 返回 PINGRESP。如果已有其他控制报文流动,可以推迟心跳。
python
from paho.mqtt.client import CallbackAPIVersion, Client
client = Client(callback_api_version=CallbackAPIVersion.VERSION2)
client.reconnect_delay_set(min_delay=1, max_delay=60)
client.connect("broker.example.local", 1883, keepalive=60)
client.loop_start()
MQTT 3.1.1 通常按 1.5 倍 keepalive 判定客户端超时,具体行为以 broker 实现和版本为准。keepalive 设置要结合 NAT 超时、消息 QoS、低功耗策略和离线判定需求。
3. 常见协议的心跳方式
| 协议 | 常见机制 | 工程注意点 |
|---|---|---|
| MQTT | keepalive / PINGREQ | 区分链路在线和订阅、发布权限正常 |
| WebSocket | ping / pong 控制帧 | 超时应等待 pong,不只等待 ping 发出 |
| IEC 104 | TESTFR 链路测试帧 | 需要同时管理 t0-t3 参数和链路状态 |
| HTTP API | 应用层健康接口或请求超时 | HTTP 本身不是长连接,需设计会话语义 |
| Modbus TCP / RTU | 无统一心跳 | 常用轮询响应成功率做活性判断 |
不要把不同协议的心跳语义混在一起。链路活着、协议会话活着、业务数据正常,是三种不同状态。
五、设备心跳设计
设备心跳建议携带少量可验证信息:
json
{
"device_id": "gw-001",
"seq": 1024,
"sent_at": "2026-08-28T02:30:00Z",
"firmware": "1.5.0",
"uptime_s": 86400,
"status": {
"collect": "running",
"uplink": "connected",
"queue": 12,
"rssi_dbm": -71
}
}
设计要点:
seq用于识别重复、乱序或过期心跳;- 服务端以收到时间计算离线,不信任设备时钟;
- 本地状态机和超时计算使用单调时钟;
- 心跳发送成功后再更新
last_confirmed; - 异常时记录错误类型,不只记录"失败";
- 状态字段保持小体积,详细指标走独立遥测。
一个简化的设备侧任务:
python
import asyncio
import logging
import random
import time
class DeviceHeartbeat:
def __init__(self, device_id, transport):
self.device_id = device_id
self.transport = transport
self.seq = 0
self.consecutive_failures = 0
self.last_confirmed = time.monotonic()
async def run(self, interval=30, send_timeout=10):
while True:
self.seq += 1
payload = {
"device_id": self.device_id,
"seq": self.seq,
"sent_at": time.time(),
}
try:
await asyncio.wait_for(
self.transport.send(payload),
timeout=send_timeout,
)
self.last_confirmed = time.monotonic()
self.consecutive_failures = 0
except (OSError, asyncio.TimeoutError) as exc:
self.consecutive_failures += 1
logging.warning("heartbeat failed: %s", exc)
jitter = random.uniform(0, interval * 0.1)
await asyncio.sleep(interval + jitter)
如果传输层没有确认语义,send 返回只代表"已交给本地栈",不代表对端已经处理。此时需要应用层 ACK、MQTT QoS 或服务端收到记录来闭环。
六、服务端健康判定
服务端不应把设备表简单分成"在线 / 离线"。更稳妥的是维护状态机:
text
unknown → online → degraded → offline → online
建议规则:
- 收到有效心跳后更新
last_seen; - 超过阈值进入
degraded或unknown; - 连续超时后再进入
offline; - 状态迁移必须记录时间、原因和上一次心跳序号;
- 设备离线后不要立刻删除记录,否则后续迟到心跳和补传数据难以关联;
- 对周期休眠设备使用预约上线窗口,不能用统一 120 秒阈值;
- 对序号回退、设备重启、时钟跳变做分类处理。
简化示例:
python
import asyncio
import time
from dataclasses import dataclass
@dataclass
class DeviceState:
device_id: str
last_seen: float
last_seq: int = -1
online: bool = True
class HealthMonitor:
def __init__(self, offline_after=120):
self.offline_after = offline_after
self.devices = {}
def record(self, device_id, seq, received_at=None):
now = received_at or time.monotonic()
state = self.devices.get(device_id)
if state is None:
state = DeviceState(device_id, now)
self.devices[device_id] = state
if seq < state.last_seq:
# 重复、乱序或设备重启,需要按业务分类处理。
return
state.last_seen = now
state.last_seq = seq
state.online = True
async def check_loop(self, interval=10):
while True:
now = time.monotonic()
for state in list(self.devices.values()):
if not state.online:
continue
if now - state.last_seen > self.offline_after:
state.online = False
await self.mark_offline(state.device_id)
await asyncio.sleep(interval)
async def mark_offline(self, device_id):
print(f"device offline: {device_id}")
这个示例只表达判定思路。生产实现还应处理并发锁、事件总线投递失败、审计日志、迟到心跳、恢复通知和长期离线设备的归档策略。
七、断线重连与退避
重连要避免两个极端:立刻高频重连会冲击网络和服务端;固定长间隔又会延长故障恢复时间。常用方案是指数退避加随机抖动。
python
import asyncio
import random
import time
class ReconnectingClient:
async def run(self):
backoff = 1
max_backoff = 60
stable_after = 30
while not self.shutdown:
connected_at = time.monotonic()
try:
async with self.connect() as conn:
await self.handle(conn)
except (OSError, asyncio.TimeoutError, ProtocolError):
stable = time.monotonic() - connected_at >= stable_after
if stable:
backoff = 1
delay = backoff * random.uniform(0.5, 1.5)
await asyncio.sleep(delay)
backoff = min(backoff * 2, max_backoff)
重连策略还应注意:
- 连接成功后重新认证、恢复订阅和同步状态;
- 只重试可重试错误,证书错误、权限错误和配置错误应直接告警;
- 断线期间采集数据写入有界本地队列;
- 恢复后补传要幂等,避免重复计数或重复控制;
- 服务端故障时按站点或客户分批恢复,控制重连风暴;
- 记录每次断链、重连、恢复耗时和最终错误码。
八、监控与告警
心跳机制上线后,至少监控以下指标:
- 心跳发送成功率;
- 心跳往返时延;
- 连续失败次数;
- 状态迁移次数;
- 在线 / 离线 / 降级设备数量;
- 重连次数和重连耗时;
- 断链期间的队列长度与丢弃数量;
- 设备恢复后的补传成功率;
- 不同网络类型、区域和固件版本的离线率。
告警不应只看单台设备。可以从三个层级判断:
- 单设备异常:设备离线、心跳超时、反复重连;
- 群体异常:同一站点、同一运营商、同一固件版本集中离线;
- 业务异常:心跳正常但数据停滞、队列持续增长或协议任务无响应。
第三类最容易漏掉。心跳正常只说明保活路径正常,不能代表业务链路健康。
九、常见坑
| 坑 | 表现 | 处理 |
|---|---|---|
| 心跳频率过高 | 流量、功耗和服务端负载上升 | 按路径超时和业务容忍度设置周期 |
| 没有超时判定 | 平台显示假在线 | 使用连续失败阈值和状态机 |
| 只依赖 TCP Keepalive | 应用死锁或中间代理不可达时仍在线 | 增加应用层心跳和业务健康检查 |
| 服务端直接删除离线设备 | 迟到心跳和补传数据无法关联 | 保留状态记录并做归档 |
| 使用墙钟计算本地超时 | NTP 校时导致状态跳变 | 本地判定使用单调时钟 |
| 无抖动的大规模重连 | 服务恢复瞬间连接风暴 | 指数退避加随机抖动 |
| 心跳与业务状态混淆 | 心跳正常但数据已经停滞 | 分别评估链路、任务和数据新鲜度 |
| 队列无上限 | 断网后存储被写满 | 有界队列、淘汰策略和补传策略 |
十、边缘运行时中的角色
在 Zenova EdgeOS 这类工业边缘运行时中,心跳应作为标准能力提供:
- 设备侧链路保活和应用任务心跳;
- 服务端离线、降级、恢复判定;
- MQTT、WebSocket、IEC 104 等协议的差异化配置;
- 断线重连、有界缓存和幂等补传;
- 心跳成功率、往返时延和状态迁移指标;
- 按设备、站点、网络和固件版本聚合的运维视图。
Zenova EdgeOS 方案基础 License ¥400 / 台起。
TL;DR
工业网关心跳设计的核心是:TCP Keepalive 只负责 TCP 层保活,应用层心跳负责协议和任务活性,健康判定还要结合数据新鲜度、队列、错误率和资源状态。心跳周期、超时阈值、双向职责、状态机、重连退避和监控指标必须一起设计。心跳不是简单的定时包,而是工业网关可用性判断的基础设施。