LoRaWAN告警通知RPC机制:从选型到架构实战
发布时间:2026/9/26 21:05:27来源:尧图网络
开场为什么 LoRaWAN 告警要折腾成 RPC做 LoRaWAN 项目做得久了你会发现真正难的不是把数据收上来而是怎么在第一时间把“有设备出事了”这件事通知出去。我们做过好几个物联网项目传感器从井盖、消防水压到农业大棚都有数据上来之后如果只写进数据库那基本等于白干因为客户要的是“地埋式井盖被打开了3 秒内告诉我”而不是“下周报表里多了一行异常记录”。这套机制的背景很简单客户端有一套自己的 LoRaWAN 网络终端设备通过网关把数据汇聚到网络服务器再由网络服务器把上行数据推给业务应用。我们内部的业务应用叫 ThinkLink负责统一处理设备数据、规则引擎、告警工单和消息触达。设备上报的数据一旦触发阈值ThinkLink 这一侧就要马上进入告警流程。为了让告警从 LoRaWAN 链路走到 ThinkLink 业务侧我们没有用传统的 HTTP 回调而是单独设计了一套基于 RPC 的告警通知机制。标题里有个让人容易迷糊的词RPC。如果你做测绘或者遥感看到 RPC 可能会想到 GDAL 里的有理多项式系数Rational Polynomial Coefficients正射校正模型两个完全不是一个东西。我这里说的 RPC 就是 Remote Procedure Call远程过程调用。设计这套机制时最大的初衷就是告警通知不能只做“消息发出去”还要能确认对方有没有收到、处理结果如何、超时了怎么办、连接断了怎么重试。这些都是 RPC 协议本身更擅长解决的语义问题。这篇文章我把整个机制的来龙去脉、协议设计、代码实现、真实运行时的坑都梳理一遍。适合这部分人群正在用 LoRaWAN 做物联网应用集成、想把设备告警做得更可靠、或者在考虑“为什么用 RPC 不用 HTTP/MQTT”这类选型问题的读者。看完你至少能照着搭一套最小可用版本不会被网上那些把三个词拼一起但啥实质内容都没有的文章糊弄过去。1. 方案选型为什么告警通知非要用 RPC1.1 HTTP 回调哪里不够早期我们确实用的是 HTTP WebhookLoRaWAN 应用服务器收到设备上行消息解析以后向 ThinkLink 打一个 POST 请求ThinkLink 再去判断告警条件然后调短信、App 推送接口。跑了一段时间问题一个一个冒出来。最典型的是超时行为不可控。设备上报通常集中在某个时间段比如早上高峰期或者整点定时上报一旦同一时刻有几百个告警同时进来HTTP 回调经常出现响应慢。官方建议 HTTP 客户端连接超时要给足但我们的规则引擎计算耗时不定调用外部短信通道还可能再卡几秒结果就是调用方要么等得没耐心要么直接超时把消息丢掉了。HTTP 是单向的除了回调返回一个200 OK调用方其实感知不到 ThinkLink 内部告警处理的真实状态。你说回调返回 200 就算成功了吗不一定可能只是网关收到了业务侧解析失败了。真正的问题是HTTP 接口横在中间的人往往只关心“我发出去了”而不关心“对方真处理了没有”。另外在告警场景里告警消息并不总是“推送”给所有设备用户也可能是一个主动拉取的请求——比如 ThinkLink 侧的客户端向 LoRaWAN 网关查询某个设备最近状态、重新推送某条告警、取消某个误报。这类“你问我答”和“我命令你执行”的交互用 HTTP 也能做但你需要为每一类交互去定义 URL、参数、鉴权方式、错误码接口写得越多维护成本越高最后接口文档比代码还长。1.2 RPC 的语义更适合告警这种强交互场景RPC 把网络交互抽象成“调用一个函数”。对告警通知而言这个抽象非常贴切。设备触发了阈值LoRaWAN 上行数据到达业务系统做的事情本质上就是“调用一个远程函数NotifyAlert(ctx, alertMsg)”函数返回值告诉你通知是否接收、是否被拒绝、是否因为重复而被过滤、是否需要重试。和 HTTP 回调相比RPC 有几个特性是我们在实际项目里真正离不开的强类型接口不用再约定到处都不一致的 JSON 字段RPC 的接口定义即文档客户端和服务端自动生成代码字段错了编译期就报错。错误传递机制RPC 错误可以对上层暴露具体状态码和描述比如“设备不存在”“规则引擎繁忙”“目标通道熔断”而不是 HTTP 请求后把详情塞进 JSON 响应体里靠人去看。超时和重试策略几乎每个成熟的 RPC 框架都自带超时、重试、熔断能有效避免告警在链路中途莫名消失。流式与订阅很多 RPC 框架支持双向流像 LoRaWAN 这种持续上行的场景可以保持一条长连接数据来了就推而不是反复建连。对比下来我们最终选择在 ThinkLink 和 LoRaWAN 接入层之间引入一个 RPC 服务层。LoRaWAN 网络服务器收到应用消息后把解析好的告警数据通过 RPC 调用提交给 ThinkLinkThinkLink 对外也开放了 RPC 查询接口用于查状态、补发、取消告警。有人会问MQTT 不也能做实时通知吗如果只是做广播通知MQTT 确实非常合适。但我们的场景里离不开“请求—响应”和“错误码”比如 ThinkLink 需要主动向 LoRaWAN 设备下发命令、需要拉取某一时间段告警摘要。MQTT 虽然是消息层但缺少方法调用的语义如果要实现还需要上层再套一套协议。RPC 把“请求—响应”和“单向通知”两种模型都统一了所以最终选择了 RPC。2. RPC 与 LoRaWAN 告警链路整体架构2.1 链路组成部分一条完整链路大概是这样的LoRaWAN 终端设备传感器 ↓ LoRaWAN 网关集中器 ↓ LoRaWAN 网络服务器LNS ↓ 接入网关适配层解析 payload通过 RPC 提交 ↓ ThinkLink 告警服务RPC Server 端 ↓ 规则引擎 / 告警触发 / 消息通道终端设备可以是市面上常见的 LoRaWAN 节点比如用 ESP32-S3 加上一块 SX126x 射频模块自己搓的节点也可以是现成的井盖传感器、消防水压传感器、温湿度采集器。LoRaWAN 网络服务器我们用的是自研的轻量版本也对接过 ChirpStack跑下来都通。终端设备通过 OTAA 或者 ABP 入网按周期上报数据帧。LoRaWAN 网络服务器负责校验 MIC 和加解密解出来之后是原始应用载荷可能是 BCD 编码、 字符串或者自定义二进制结构。接入网关拿到这些数据以后先做应用层解析提取出设备地址、数据类型、传感器数值再转换成标准告警对象。RPC 调用就从这一层发起。序列化协议选择上我们最开始用的是 JSON-RPC 2.0因为调试方便出了问题打印报文一目了然。后来节点数量涨到几千台为了性能换了 gRPC Protocol Buffers。如果你是自己搭最小的演示系统先用 JSON-RPC 就好理解清楚了再换也不难。2.2 接口定义与调用模型这套机制的接口设计围绕三个核心动作上报告警NotifyAlert(ctx, AlertMessage) returns (AlertAck)接入网关调用 ThinkLink把一条告警事件提交上去。查询设备状态GetDeviceStatus(ctx, DeviceQuery) returns (DeviceStatus)ThinkLink 主动拉取 LoRaWAN 终端状态用于派工前确认设备在线状态。取消告警CancelAlert(ctx, AlertId) returns (CancelAck)用于处理误报或者设备恢复正常后的自动消除。比如上报告警这条 RPC消息体定义为字段类型说明device_euistring终端设备唯一标识alert_typestring告警类型如 leakage、open、low_batteryalert_levelint32告警级别1 提醒2 告警3 紧急alert_valuedouble触发告警的测量值thresholddouble触发阈值payloadbytes原始上行数据留档备查reported_atint64事件时间戳Unix 毫秒rule_idstring触发规则标识便于回溯返回AlertAck里包含alert_id、accepted、message。这样接入网关的调用方不需要自己去维护状态 ThinkLink 侧收下就返回唯一告警 ID后面取消、确认都以这个 ID 为主键。从 LoRaWAN 网关侧看RPC 调用要解决的问题就是一次上行消息到达后如何安全地触发服务端方法。如果采用 gRPC服务端方法用 protobuf 定义后客户端生成的桩代码直接调用如果采用 JSON-RPC方法名就是字符串比如alert.notify参数是标准的 JSON 对象。2.3 为什么接入网关里不放业务逻辑设计的时候我们特意保持接入网关薄、ThinkLink 厚。接入网关只做三件事解析帧、转换数据格式、发起 RPC。真正的规则判断全部放在 ThinkLink 里面。原因很简单LoRaWAN 的 payload 协议各厂家千奇百怪接入网关只负责格式转换减少对上层的影响。告警规则变化频繁比如“水位超过 1 米就告警”改到“超过 1.2 米才告警”如果逻辑写在接入层每改一次规则都得重新发布网关。设备类型差异很大同一时间可以同时存在几十种传感器型号规则引擎统一在 ThinkLink 管理更合理。当然也不是所有判断都放上部。有一种例外情况设备心跳丢失。LoRaWAN 终端长时间不上报网络服务器层面就能检测到这类“设备失联”告警直接在网络服务器生成再通过 RPC 提交给 ThinkLink。因为这类异常不依赖业务规则而是链路层面的基础健康监控放太上层反而绕路。3. 核心机制实现数据从 LoRaWAN 上行到 ThinkLink 落库3.1 终端侧数据帧格式设备采集到数据以后通过 LoRaWAN 协议把 payload 发出来。以一个简单的温湿度传感器为例payload 里可能包含两个温湿度值对应温度2 字节和湿度2 字节再加电池电量1 字节。典型格式字节位置含义0-1温度有符号整数实际温度 值 ÷ 102-3湿度无符号整数实际湿度 值 ÷ 104电池电量百分比接入网关拿到 payload根据设备类型查表解析成键值对。LoRaWAN 的端口FPort和 frame payload 长度有限终端厂商普遍都会做这种二进制压缩而不是直接发 JSON因为 LoRaWAN 的空中传输速率非常有限一发大字符串就是好几百毫秒的空中时间电池扛不住。如果你用的是 ESP32-S3 这样的开发板自己写节点要注意一点LoRaWAN 的注册和入网参数AppEUI、DevEUI、AppKey一定要支持配置。我们手上有几个测试节点是用 ESP32-S3 加 SX1262 做的踩过的坑是配置参数写死在代码里后期要换网络服务器简直想哭。这是整个链条里最容易忽视又最耗时间的部分。3.2 接入网关解析与 RPC 发起接入网关的代码大致走这个流程# 以 Python 示例框架采用 gRPC from thinklink_pb2 import AlertMessage from thinklink_pb2_grpc import AlertServiceStub def handle_uplink(event): # event 是网络服务器传入的上行消息 payload event.payload device_eui event.device_eui fport event.fport # 1. 根据设备型号解析 payload sensor_data decode_payload(device_eui, fport, payload) # 2. 解析出结构化数据 temperature sensor_data.temperature humidity sensor_data.humidity battery sensor_data.battery # 3. 不在这里判断是否告警直接提交原始数据 alert AlertMessage( device_euidevice_eui, alert_typesensor_report, alert_valuetemperature, reported_atnow_ms(), payloadbytes(payload) ) # 4. 发起 RPC 调用 try: ack alarm_stub.NotifyAlert(alert, timeout5) log(falert accepted: {ack.alert_id}) except RpcError as e: # 根据错误码决定是否需要缓存重试 requeue_alert(alert, e.code())这里有一个细节RPC 调用超时怎么设。LoRaWAN 上行数据本身已经到了网络服务器如果接入网关直接调用 ThinkLink 挂起超过几十秒那这条告警就等于没有时效性了。但也不能把它设得太短因为 ThinkLink 侧还要跑规则引擎而且外部消息通道短信、IM如果响应慢会直接影响 RPC 的返回时间。我们的做法是把 RPC 调用的整体超时设置为 10 秒然后把告警写入本地 Redis 临时队列异步确认最终状态。同时设置了多种重试策略对于语法错误级别的 RPC 错误码不重试直接记录日志对于网络断开或服务端繁忙级别的错误码自动重试重试间隔 5 秒、30 秒、2 分钟逐级退避如果重试超过 5 次还没有成功则标记为“待人工处理”并打开一条降级通道比如直接调用短信接口发一条最简短的告警。3.3 gRPC 超时和重试背后的原理这里要展开讲一下为什么 RPC 框架比 HTTP 更适合这种带失败语义的场景。以 gRPC 为例一次 RPC 调用从客户端发起后会生成一个带有 deadline 的上下文。如果服务端处理超过 30 秒没有返回客户端会主动断开错误信息通常是“cannot finish rpc call in 30 seconds: null”这种格式。你可能会觉得这个错误提示很莫名其妙null 是什么其实就是服务端根本没返回值也没抛自定义错误只是客户端在 deadline 过期时强制中断了。所以我在接入网关一开始就把 deadline 设成 10 秒配合独立的告警队列。RPC 服务端可能因为外部短信通道缓慢导致处理时间超过 deadline没关系客户端先拿到“服务端繁忙”级别的错误不会傻等。而框架层面的错误码DEADLINE_EXCEEDED进一步分类决定是否重试。如果你在日志里看到像rpc failed; curl 56 schannel: server closed abruptly这种错误说明底层传输用的 HTTP/2 连接被服务端或中间层异常关闭了。这在我们 Windows 环境下跑客户端时经常碰到原因通常有两个一是长连接空闲时间过长被防火墙打了 FIN二是服务端超时回收连接但客户端还继续往旧连接上发数据。gRPC 有自己的 keepalive 机制但 Windows 下要注意把 keepalive 时间和操作系统 TCP keepalive 参数配合好否则还是会随机出现传输层断开。3.4 服务端实现规则引擎和状态机ThinkLink 服务端接受到 RPC 请求后会先校验设备去重和幂等性。同一个设备如果反复发送同一条告警我们不希望在 1 分钟内创建一打工单。所以每条告警会有唯一指纹字段计算指纹相同的直接返回已受理避免重复通知。规则引擎部分用一个简单的配置驱动结构实现。每条规则由事件条件、阈值、持续时间、通知目标组成。不考虑动态编排的话最简单的规则表达式可以设计成{ rule_id: rule_temp_high, condition: temperature_value 60, duration: 10s, level: 2, channels: [app_push, sms], recover_condition: temperature_value 55 }当temperature_value 60这一状态持续超过 10 秒才会触发告警这是为了避免瞬时抖动带来的误报。持续 10 秒怎么实现设备通常 30 秒上报一次所以可以简单地在前 10 秒内即使有超阈值数据也不发等待更长时间窗口的第二条或第三条数据确认。如果客户端需要立即感知也可以在规则里配置duration: 0表示即时触发。我们实际使用中绝大多数设备瞬时数据都会有毛刺所以至少保证 2 个上报周期再触发会比较合理。ThingLink 侧的告警状态机一般是pending→confirmed→processing→resolved如果误报则可直接置为cancelled。RPC 接口里CancelAlert就负责把pending和confirmed状态的告警取消掉。3.5 RPC 服务端和客户端的连接治理RPC 连接治理是整套机制里最容易被忽略的。很多项目上线第一天跑得好好的到了第三天出现大量超时一看原因发现都是连接管理做的不好。客户端为每个设备建一条长期连接结果几千个设备几千条连接服务端连接数爆炸。加上状态不好时连接反复重建网络拥塞更严重。我们的做法是接入网关作为单点客户端通过连接池复用一定数量的 RPC 连接。设备数量和单条连接上的消息并发之间做个平衡一般 50 台设备共用一条连接毫无压力。如果一条连接断掉连接池会自动重新建立但这期间积聚在队列里的告警需要做背压控制不能无脑重试把服务端打挂。服务端方面我建议一定要设置最大并发请求数和队列长度超出后直接进入熔断。告警服务的核心价值是“及时触达”如果服务端已经过载继续塞请求只会让已受理的告警也一起慢最后所有消息都延迟。我们把服务端最大并发限制在 200超过的 RPC 请求直接返回RESOURCE_EXHAUSTED错误客户端收到后会把这部分告警投递到本地磁盘备份队列等负载降低后再重放。4. 常见问题与排查实录RPC 告警链路故障速查4.1 超时类错误典型报错cannot finish rpc call in 30 seconds: null这是 gRPC 客户端在 30 秒内无法收到服务端响应时的默认错误。遇到这类错误先把超时缩小比如在客户端显式设置deadline5s然后观察是全部调用超时还是部分调用超时。如果是全部超时优先排查网络连通性其次是服务端进程是否挂起如果是部分超时大概率是某条数据让服务端处理耗时异常比如短信通道不可用导致服务端的 RPC 调用被同步阻塞。我们的对策是规则引擎和外部通道之间做异步化不可能因为一条短信通道卡死而影响整个告警 RPC 服务。同一类问题也包含error grabbing logs: rpc error: code unknown desc warning: incomplete log。这个通常发生在服务端日志采集模块上。RPC 调用虽然成功但服务端把日志往日志中心写入时超时返回warning: incomplete log。这不会影响告警主流程却会严重影响事后排查。处理方案很简单日志写入接受失败降级失败的日志先落到本地文件后台再补传而不是让 RPC 主流程陪葬。4.2 连接被中断典型报错rpc failed; curl 56 schannel: server closed abruptly (missing close_notify)这个错误在 Windows 客户端调用 gRPC 服务端时经常出现。missing close_notify表示 TLS 连接正常关闭时需要发送 close_notify 通知但服务端或网络代理直接断开了 TCP 连接没有发这个通知。原因往往不是太多就是连接空闲被防火墙或负载均衡器回收。线上处理方式也很直接就是开启 gRPC 的 keepalive# gRPC 客户端开启 keepalive 示例 import grpc options [ (grpc.keepalive_time_ms, 10000), (grpc.keepalive_timeout_ms, 3000), (grpc.keepalive_permit_without_calls, 1), (grpc.http2.max_pings_without_data, 0), (grpc.http2.min_time_between_pings_ms, 10000), ] channel grpc.insecure_channel(localhost:50051, optionsoptions)尽量保持每 10 秒发送一次 keepalive 探测防火墙的空闲回收时间一般配置为 5 分钟这样做之后基本再没遇到这个问题。如果不能改 keepalive另一个土办法是定期断连重建。每半小时主动重新建立连接把旧连接关掉。虽然不太优雅但对稳定性要求不高的本地环境是有效的兜底方案。4.3 服务端日志类问题如果服务端宁可频繁报error grabbing logs: rpc error: code unknown desc warning: incomplete log通常是日志中心切换或者磁盘占满导致的。这一般不是关键告警链路的问题但如果不处理后期运维会非常痛苦。我们统一升级为异步日志上报日志不落盘就意味着不可查所以需要先在本地按日期分区写小文件再异步发送到日志中心。发送失败时本地文件保留 7 天确保告警事后分析的数据不丢。4.4 容器环境下的幻影问题还有一个坑如果你的 RPC 服务是跑在 Kubernetes 里的服务端 pod 异常重启或者节点资源不足时kubelet 和容器运行时之间的通信也会报类似failed to create pod sandbox: rpc error: code unknown desc failed to create的错误。这个现象出现的时候你可能会误以为是业务 RPC 服务挂掉了结果排查一圈发现是容器运行时层出了问题。不要把业务 RPC 服务和容器运行时 RPC 混为一谈日志路径完全不同。业务正常的告警日志一截都发不出来你 ps 一看进程还在但其实就是节点上磁盘 IO 被打爆或者 sandbox 镜像拉取失败。这种情况处理方式是给业务 RPC 服务单独分配一个健康检查探针通过 gRPC health checking protocol 检查服务真正可用而不是只检查 pod 进程是否存在。这样能快速区分是业务问题还是容器环境问题。4.5 底层数据库 RPC 错误系统接着运行一段时间后日志里出现了一个眼生的错误ORA-28576: lost rpc connection to external procedure agent。这是 Oracle 数据库调用外部过程代理时连接丢失本身是数据库侧的 RPC 机制跟我们的 gRPC 链路无关但在同一套监控面板上一起出现时差点误导了排查方向。后来定位到是两个告警服务共享了一组数据库连接池外部过程代理程序崩溃影响了整个数据库连接池的稳定性导致业务侧的健康检查也跟着告警。我的建议是不同服务之间尽量不要共享数据库连接池尤其是跟外部过程代理相关的连接最好单独隔离连接池和调停代理服务能避免很多鸡飞狗跳的半夜排查。4.6 排错速查表下面这张表适用场景是LoRaWAN 设备已经上报但 ThinkLink 侧没收到告警。现象可能原因第一步排查方向网络服务器收到数据但接入网关没有解析日志payload 格式不对或者设备应用端口不匹配抓取网络服务器原始数据和接入网关日志对照接入网关有解析日志但 RPC 调用没发出本地队列积压连接池耗尽查看连接池监控确认 RPC 客户端连接数量RPC 调用发出但超时ThinkLink 服务端过载、外部消息通道阻塞看服务端请求队列深度单独测短信接口耗时RPC 调用成功但用户没收到通知规则未命中、用户订阅配置问题确认规则事件和用户通道配置是否匹配用户收到通知但在平台没看到告警工单告警创建流程异步失败查看工单创建日志检查数据库连接多个设备同时上报告警全部超时并发触顶或服务端熔断查看服务端最大并发限制和服务端日志4.7 误报与风暴的治理经验最后必须说一个运维侧的真实教训RPC 通知机制可以非常可靠地把每一条告警都送达但它也意味着会非常可靠地把每一条误报都送达。设备在调试阶段、在更换电池、在重启入网的过程中都可能发出异常数据。如果我们对这些数据都执行完整告警流程客户会在一个小时内收到几十条垃圾通知然后把系统定义成“狼来了”。所以在 ThinkLink 规则引擎里专门加了一个“设备状态互斥”逻辑设备处于调试模式、维护模式、离线恢复后 5 分钟内只记录日志不触发面向客户的告警通知。这些状态信息通过 RPC 的GetDeviceStatus查询在告警前先确认设备当前模式。实际运行下来误报率从一开始的百分之十几压到百分之三以内客户的信任感完全不一样。另一个经验是告警风暴的预防。如果网关批量断线重连大量设备同时上报“离线恢复”事件RPC 服务端可能一瞬间收到上千条相同类型的告警。我们采用滑动窗口去重加分级降频同一设备同一类型告警窗口期内只发一条不同设备同类型告警一小时内最多通知 3 次其余只写工单。这样一来即使出现大规模网络抖动也不会把用户通知通道打爆。5. 总结与扩展思考这套机制从搭建到现在最大的体会是RPC 不是银弹但它能让告警通知变得像调用本地方法一样可控。你把网络交互的问题收敛成了函数调用的语义问题接下来要做的就是定义清楚超时、重试、幂等、流控这些服务治理的常规手段。相比 HTTP 回调RPC 在这类告警通知场景里确实省心很多。关于再往后扩展的方向我自己的想法是还可以做两件事一是把 ThinkLink 的 RPC 接口升级为支持双向流让 LoRaWAN 侧可以持续接收设备上下行命令的下发结果二是把告警通知的 RPC 调用链跟值班调度系统打通告警确认后直接通过 RPC 往值班组播报一条任务工单。如果后面真的做了我会再来分享实际踩坑的过程。留一个问题给你思考如果你现在的 LoRaWAN 告警服务间通信已经很稳定了是否还需要考虑在边缘侧先做一层本地告警聚合再决定要不要上云我做过本地聚合的版本省下的流量和通知费用比想象中多得多但代价是边缘逻辑多了一层复杂度这个权衡值得好好算一算。
网站建设高端定制企业官网