运行边缘服务
DeviceChain 有三个组件位于平台边缘,它们都与周围的无状态服务不同:
- Sparkplug-B 接入和 LwM2M 接入是权威在线状态传输。它们会被明确告知设备何时连接和断开,而不是根据静默推测。
- 边缘代理是运行在现场设备上的独立二进制程序。到云端的链路中断时,本地缓冲;连接恢复后转发。
三者都以单实例运行,并持有数据库中不存在的实时状态,但重启代价各不相同。本页介绍实例数量及原因、故障切换损失、在线状态的保证与限制,以及应监控的内容。
了解协议功能或设备如何映射到平台,请阅读 Sparkplug-B、LwM2M或设备在线状态。
两个接入服务都需主动启用,不在默认功能领域集合中。两者都硬性依赖设备管理服务,由它解析产出的事件。
各组件单实例运行的原因
每个组件有自己的单实例原因,并非同一策略套用三次。
| 服务 | 为什么只有一个 | 第二个实例的影响 |
|---|---|---|
| Sparkplug 接入 | 作为 Sparkplug **Host Application(主机应用)**加入消息代理,一个 Sparkplug 环境只有一个主机应用。 | 发布冲突的主机 STATE,每条消息接入两次。 |
| LwM2M 接入 | DTLS 是绑定在一个 UDP 套接字上的有状态会话。 | 备用实例也绑定套接字时,会静默接收并丢弃分配给它的报文。流量消失,不会明确报错。 |
| 边缘代理 | 独占本地缓冲目录,并使用一个云上行身份。 | 共用目录的两个实例冲突于文件锁;共用身份的两个实例反复将对方踢下上行连接。 |
两个接入服务不仅靠文档要求,还由平台强制约束:
- 所有权租约。 每个服务获取 30 秒窗口的租约。替代 Pod 在获得租约前不连接、不绑定任何内容。
- 有界重叠。 两个 Pod 同时服务的窗口,最多是租约窗口加一个续约间隔,约十秒。失去租约的主实例只有下一次续约察觉后才自我驱逐,随后还需要释放消息代理或 DTLS 状态。因此重叠被限制,但没有消除。
- 没有写入屏障。 两条路径都没有阻止旧主实例写入的机制。虽然携带租约 epoch,接入路径不按它拒绝写入。
- chart 拒绝。 任一领域超过一个副本时 chart 拒绝渲染。限制针对领域本身,不针对发布策略,因此将
strategy覆盖为RollingUpdate不能绕过。
主动结束自己的 Pod 会在退出时释放租约,替代实例启动后立即获取。Pod 判定无法继续服务时会主动结束;LwM2M 接入中包括领导任期无法建立,或 CoAP/DTLS 传输停止读取。30 秒等待只适用于突然丢失,没有机会释放租约的节点故障、SIGKILL 或内存不足终止。
chart 对单副本领域跳过中断预算,否则要求一个可用 Pod 的预算会完全阻止节点排空。因此排空边缘服务所在节点会停止该传输,直到 Pod 重新调度并取得租约。恢复自动进行,但不是即时恢复,应优先主动发布,而不是排空节点。
故障切换的代价
两个服务都将重启和领导权交接视为常规操作,并自动恢复,但恢复内容不同。
| Sparkplug 接入 | LwM2M 接入 | |
|---|---|---|
| 在线状态 | 重建。 新主实例重新建立会话,请求边缘节点重新宣告,并核对实际存活设备,因此不会遗漏交接期间的断开。 | 从存储投影和设备注册有效期重建,不主动探测,避免将休眠队列模式设备误报为离线。 |
| 窗口内遥测 | 丢失。消息代理不会为未连接主机保留 DATA。 | 窗口内发送的报文丢失;可确认 CoAP 消息由设备重传。 |
| 被观察资源 | 不适用。 | 不会重新建立,见下文。 |
| 恢复时间 | 替代 Pod 调度与启动时间,加最长 30 秒租约窗口。 | 同左,再加套接字绑定时间。 |
重启、发布或领导权交接会丢失所有 LwM2M 观察关系。在线状态会恢复,遥测却要等每个设备重新注册。默认设置下,健康设备可能静默长达一天,期间一直显示在线。参见观察关系丢失。
观察关系丢失
这是本页最容易出乎预期的运维事实。
DeviceChain 要求 LwM2M 设备 Observe(观察)资源,使设备主动推送读数。观察关系只在建立它们的进程存活期间存在。重启、发布或领导权交接会丢失全部观察关系,并且不会自动重建。
在线状态恢复,但遥测不会。设备只有下次重新注册才重新上报。这由设备自身行为和时间安排决定,唯一边界是注册有效期。默认 86400 秒,意味着完全健康的设备也可能静默一天,同时在控制台一直显示在线。
无法容忍时,可降低 maxLifetimeSeconds,参见 LwM2M 设置。不要低于设备实际请求的最长有效期,否则每次交接都会把设备作为已死亡而过期。
在线状态保证
每个设备携带在线状态来源,为 INFERRED 或 ASSERTED。相关约束比表面看起来更严格。
只有来源能交还设备。 权威传输首次代表设备发声时,设备变为 ASSERTED。超时、数据事件或静默都不会使其回到 INFERRED。唯一方法是降级:由来源声明不再代表设备,而不是声明设备状态。参见恢复推断在线状态。之前通过 Sparkplug 或 LwM2M 上报、现在改用普通 MQTT 的设备,会保留权威来源,并继续豁免不活跃清扫。
顺序由平台生成的会话标识决定,不使用设备提供的值。 平台为每组连接/断开事件盖上自行生成的会话标记。Sparkplug birth/death 序列号仅用于匹配对应的出生与死亡,不比较大小,因为会回绕。LwM2M 注册 ID 也不作为会话身份。因此,两种传输上旧会话的延迟或重放消息,都不能拆除新会话。
标记来自接受连接的消息代理节点时钟。多节点集群中,新会话标记不保证大于前一次;时钟落后的节点可能生成更小值。平台会协调这种情况,而不是假设不会发生。发现存活设备实际会话标记小于存储标记时,将记录改为实际会话,使后续断开仍能识别。
设备不能自行声明权威在线状态。 普通设备载荷路径提交的连接/断开事件会直接拒绝,不是仅忽略。只有传输服务本身产出它们。ASSERTED 设备豁免不活跃清扫,允许设备自行声明在线就可能使它永久保持在线。
推断超时固定十分钟,不可调整。 无权威传输的设备静默十分钟后被清扫为离线,每分钟复查。目前没有逐设备覆盖或配置项。降级设备也重新受该超时约束,这正是降级的主要作用。
设备卡在在线状态
依赖在线状态向人发送告警前,应先了解该失效方式。
ASSERTED 设备死亡却未宣告时,可能无限期显示在线。 不活跃清扫有意跳过 ASSERTED 设备,因为权威传输上的静默不能证明死亡。这两种传输没有其他超时、看门狗或清扫器。只有两种方式修正,且都不是超时:
- 设备自己的传输产生新信号。 DeviceChain 自身 MQTT 代理声明的设备无需主动操作:修复轮次定期比较代理活动连接与平台状态并纠正差异,参见设备在线状态。Sparkplug 和 LwM2M 没有此轮次,必须由设备产生信号。
- 降级。 当来源已经消失,永远不会再发送信号时,应使用此方式。三个权威传输都适用。
具体原因包括:
- Sparkplug 死亡证书丢失。 代理不投递节点 DEATH 时,设备持续在线直到下次协调。协调仅在主机重新连接代理时运行。主机持续连接却再也听不到节点,不会重新协调。
- 节点重新宣告。 边缘节点建立新会话时,旧会话子设备也一并替换。未在新会话中重新宣告的子设备继续显示连接,直到下一次协调才有机会纠正。
- LwM2M 注册过长。 消失设备在注册有效期结束时标记离线。默认 86400 秒,即整整一天。
- 权威传输被移除。 停用设备使用的 Sparkplug 来源或 LwM2M 凭据后,不再有任何来源发声。设备停留在最后权威状态,直到有人释放它。该操作正是为此设计,因为消失的来源不再通知平台。
两种传输中唯一可调整项是 maxLifetimeSeconds,且只适用于 LwM2M。 每次注册有效期最多为该值,直接限制死亡设备显示在线多久。例如 3600 将上限设为一小时。值必须高于设备群实际请求的最长有效期。MQTT 有自己的边界:修复轮次间隔 brokerPresence.reconcileSeconds,默认五分钟。
Sparkplug 没有等效设置。如果 Sparkplug 在线状态对运维至关重要,请将连接信号与基于超时的数据缺失规则配对;它按静默触发,不受在线状态显示影响。
恢复推断在线状态
降级是从 ASSERTED 回到 INFERRED 的唯一转换。这是来源释放管理权,不是描述设备本身,不声明连接状态。设备是否在线、最近连接、最近断开和最近上报时间都保持原样。
改变的是哪些机制可以纠正状态。ASSERTED 设备抑制平台两种修复机制;释放后交还给它们,同时修复两种冻结:
- 冻结在线。 重新进入不活跃清扫,在真实最近活动十分钟后标记离线。
- 冻结离线。 指令不再被暂扣。暂扣条件是设备为 ASSERTED 且非活动,因此 INFERRED 设备可以派发指令。定期复查暂扣集合的轮次会在几分钟内释放积压,下次读数使设备重新显示在线。应及时修复,因为暂扣指令计入每租户上限,来源离去导致离线卡住的设备,最终可能使同租户健康设备也无法入队。
降级有两条路径:来源释放自己的设备,或运营方代替已离去来源释放设备。
来源关闭时自动释放
消息代理声明的 MQTT 在线状态依赖在线状态监听连接:event-sources 连接代理系统账户,接收连接与断开通知。监听因以下原因无法启动时,event-sources 遍历仍被其声明为 ASSERTED 的设备并释放:
- 被明确禁用;
- 缺少 NATS 系统账户凭据;
- 消息代理不可达。
这是六种启动失败原因中的三种,划分有明确依据。前两种属于配置;实例所有副本读取相同值并得出相同结论,所以释放代表实例,而不是单个副本猜测。
第三种证据不同。监听先尝试连接 30 秒,才报告代理不可达,因此意味着半分钟没有系统账户连接,而不是单次失败,可能代理停机或拒绝凭据。MQTT 网关也位于同一代理;代理不可达时,没有设备能通过它连接。释放不是对设备群的猜测,而是与代理消失一致的唯一判断。
三者中只有第三项可能在 Pod 运行期间改变,所以只有它持续复查。每次释放轮次都先重新连接系统账户,包括第一轮。代理响应时,不释放任何设备:服务退出并重启 Pod。替代实例正常连接并运行监听。因此代理恢复表现为 Pod 重启,而不是设备群被释放。如果不复查,释放会按协调间隔持续遍历当前 ASSERTED 设备;其他副本持续声明时,两者会无限轮流修改每一行,中间不活跃清扫可能将已连接但安静的设备标记离线。
以下情况不释放:
- 已成功连接代理,但订阅失败。这只是该副本的问题,其他副本可能正常接收通知。
- 实例根本没有监听可运行的两种原因:没有指向平台代理的来源,或没有服务间配置。
六种原因都会设置 presence_tap_off{reason},可据此判断具体情况。
监听运行后,如果代理永久关闭连接,服务应重启,而不是关闭监听。如果代理不再接受系统账户凭据,各 event-sources Pod 会在大致同一时刻健康检查失败,由 Kubernetes 重启。重启期间 HTTP 接入不可用,MQTT 遥测在代理中等待。每个重启 Pod 使用挂载凭据连接;仍被拒绝时,监听以 broker_unreachable 原因关闭,并执行上述释放。
依赖自动释放前,应了解三点:
- 缺少凭据和代理不可达会先等待两分钟。 初始化会在启动服务的同一次运行中生成凭据并滚动代理,因此两者可能只是竞态。明确的
enabled: false没有歧义,立即生效。代理等待后还会复查;凭据无法如此复查,因为配置只在启动读取一次,变更会滚动 Pod。 - 需要自己的网关来源和服务间配置,分别提供发布身份,以及枚举租户、读取投影的方法。缺少它们时完全不运行,会记录日志并指向手动释放,此时手动操作是唯一方法。
- 限速且集合会自动减少。 每秒释放 25 台设备,释放设备退出遍历集合,因此中断后自然继续,不需从头开始。
presence_still_asserted表示剩余数量;健康释放会将其降至零并保持。
Sparkplug 和 LwM2M 设备不会自动释放。这些来源消失是因为运营方移除了它们,不是标志改变,因此应由运营方释放。
手动释放
dcctl presence demote 遍历某租户中一个来源的 ASSERTED 设备,并逐一释放:
dcctl presence demote --tenant acme --source sparkplug:plant-a \
--email ops@acme.example --password "$DC_PASSWORD" \
--reason "plant-a gateway decommissioned"
| 标志 | |
|---|---|
--tenant | 必填。一次降级仅作用于一个租户。 |
--email / --password | 必填。操作所授权的身份,必须是租户成员或超级用户。 |
--server | API 调用的实例主机,默认 localhost。HTTPS 添加 --tls。 |
--source | 必填,绝不推断,因为影响范围是整个事件来源。按设备状态报告的精确值填写:MQTT 和 HTTP 使用来源配置 ID(mqtt1、http1),Sparkplug 使用 sparkplug:{hostId},LwM2M 使用 lwm2m。 |
--device | 可重复。在该来源内部限定设备;省略则释放整个来源。 |
--reason | 必填。写入本次运行产生的每个事件,是设备群在线状态写入的唯一记录。 |
--page | 每次调用的设备数,默认 200。 |
--dry-run | 仅报告将释放的设备,不实际释放。 |
--yes | 跳过实际运行的确认提示。没有终端可确认时,除非指定该项,否则拒绝实际运行。 |
无人使用的来源不是错误,只是不匹配任何设备。但第一页匹配零台设备,比起工作已完成,更可能是 --source 拼错。命令会说明这一点,而不报告成功。
API 中的等效操作是 device-state 的 demoteAssertedPresence mutation,需要 state:demote 权限。这是写权限,不包含在所有成员默认获得的只读基线中。初始 tenant-admin 角色具有所有租户权限,因此包含该项;其他需要的角色应明确授予。
释放计量
释放与连接、断开一样,受每租户接入上限约束。已达到上限的租户,其修复也可能与引起压力的连接变化一起被拒绝,并计入 presence_events_refused_total。不会丢失:被拒绝设备保持 ASSERTED,下一轮仍会发现。修复只能按限额允许的速度推进。
两种传输的租户归属
两种传输都由连接确定租户,不读取设备提供的内容来决定。 这是该部分最强的隔离性质。
- Sparkplug。 每条消息归属于它所到达的消息代理连接配置的租户。主题中的 group ID 是客户标签,不全局唯一,任何发布者都可设置,因此不用于确定租户。配置拒绝同一代理端点对应两个租户,否则 group ID 就会成为唯一隔离依据。
- LwM2M。 设备由握手时提供的已认证 DTLS 预共享密钥身份绑定租户。设备注册载荷中自报的端点名不用于身份。未预配身份握手失败,拒绝响应不回显身份,防止探测者比较错误来枚举有效凭据。
deviceAuthMode: required 下,两种传输都提供可信设备身份,无需第二份逐事件凭据。LwM2M 身份绑定 PSK,因此逐设备认证;Sparkplug 身份来自主题,因此强制设备认证不能阻止发布者在同一租户内冒充另一设备。参见 Sparkplug 设备认证。
Sparkplug 设备认证
两种传输都将流量标记为经过传输认证,因此平台在 deviceAuthMode: required 下无需逐事件凭据即可信任设备身份。LwM2M 身份绑定已认证 PSK,是真正的逐设备身份。
Sparkplug 身份从主题推导,认证粒度只到消息代理连接。启用强制设备认证,不能阻止租户代理上的发布者以同租户另一设备身份发送。两条路径都阻止跨租户访问。若需要租户内设备身份隔离,应在自己的消息代理通过逐客户端凭据和主题权限实施,因为真正的边界在那里。
边缘传输的设备标识
边缘传输上的设备有三个标识,分别承担不同工作。混淆会导致静默失败或错误路由。自动预配设备没有名称,控制台也不能搜索它。预配前请阅读本节。
其中两个看起来像名称,第三个是生成值,因此很容易混淆。
| 标识 | 作用 | Sparkplug | LwM2M |
|---|---|---|---|
| 1. 租户锚点 | 决定数据属于哪个租户,绝不使用消息内容,见上文。 | 消息代理连接。 | 已认证 PSK 身份。 |
| 2. 设备解析键 | 决定哪台设备,即设备外部 ID,设备不能自行选择。 | 主题的 group/node[/device] 字符串,例如 plant-a/line-3/press-1。 | 服务配置中 PSK 身份旁边填写的外部 ID,不是设备发送的端点名(ep)。 |
| 3. 设备令牌 | 控制台、API 和每个事件实际使用,自动生成而非自选。 | sp-…,例如 sp-plant-a-line-3-press-1-9f2c1a8b4d3e。 | lw-…。 |
LwM2M 的 ep 只记日志,其他用途忽略。固件发送 ep=urn:imei:35… 的设备不会按它匹配,也不会明确提示。
生成令牌是因为外部 ID 常包含 /、.、空格或非 ASCII 字符,而令牌不允许这些内容。因此 plant-a/line-3/press-1 转为类似 sp-plant-a-line-3-press-1-9f2c1a8b4d3e 的值。后缀区分规范化后相同的两个外部 ID。
实际后果是,自动预配设备完全没有名称。注册仅携带令牌、外部 ID 和设备类型。控制台设备列表因此以生成的 sp-… / lw-… 令牌显示,Name 列为 —,没有其他易识别名称。
也不能搜索。控制台设备列表没有搜索框,只是 Status、Token、Name、Type、Description 和 Created 的分页列表,没有外部 ID 列。API 设备搜索仅接受页码、页大小和设备类型。查找设备时:
- 控制台中按令牌找。生成令牌包含外部 ID 的变形(
plant-a/line-3/press-1→sp-plant-a-line-3-press-1-…),翻页读取 Token 列就是查找方式。 - API 中使用
devicesByExternalId,接受精确外部 ID 并返回设备。它是精确匹配查找,不支持前缀或子字符串;控制台未调用它,所以仅 API 可用。
设备完全不出现时,先检查第二个标识,再怀疑传输。尤其是 LwM2M,PSK 身份错误会在注册前的 DTLS 握手失败,拒绝响应有意不透露原因。
LwM2M 运维
仅解码 SenML-JSON 遥测。 其他内容格式的通知被计数并丢弃。实际后果不容易从标准本身发现:
SenML 在 LwM2M 1.1 才引入。仅支持 1.0 的设备可注册、维持会话、更新在线状态并接受 Read/Write/Execute 指令,却不会产生任何测量值。不会明确失败。应检查 observe_establish_refused_total,而不是 notify_unknown_content_format_total。参见仅支持 LwM2M 1.0 的客户端。
仅支持 LwM2M 1.0 的客户端
此类设备可以注册、保持会话、正确更新在线状态并接受 Read/Write/Execute 指令,但不会产生任何测量值,也不会明确报错,读数只是始终不出现。
应检查 observe_establish_refused_total。DeviceChain 在 Observe 请求中要求 SenML-JSON,所以合规的 1.0 客户端用 4.06 Not Acceptable 拒绝 Observe,此后根本不发送通知。该拒绝在此计数,也是主要原因。设备的 notify_unknown_content_format_total 保持 零,因为它统计另一种情况:设备确实通知,但格式适配器无法解码。
观察限制
观察关系有固定且不可配置的限制。 DeviceChain 仅对固定 IPSO 范围内的对象建立观察,每个对象实例一条,每次注册最多 32 条。对象允许列表是构建的固定属性,配置无法扩展。范围之外的资源不会被观察,任何配置都不能改变。超过每次注册上限时,监控 observation_overflow_total。
会话回收
默认不回收会话。 idleTimeoutSeconds 默认为 0,表示永不回收,适用于始终连接设备。队列模式设备群应设置为明显大于预期唤醒间隔。太短会在休眠期间驱逐会话密钥,迫使重新握手,失去 DTLS Connection ID 避免握手的意义。
暴露 LwM2M 端口
面向设备的 CoAP/DTLS 端口是 UDP 5684,chart 和基础设施模块都不将它暴露到集群外。按默认安装,真实 LwM2M 设备群无法访问服务。需要自行提供 UDP 路径。
所有服务都仅位于集群内部,没有会话亲和性配置,默认 ingress 控制器只处理 HTTP。
外部暴露明确由运营方决定,没有随平台发布的实现。自行提供 UDP 路径,例如 LoadBalancer 或 NodePort 服务,或外部 UDP 代理,必须确保同一会话的每个报文都到达实际服务的那个 Pod。
LwM2M 设置
| 设置 | 默认值 | 作用 |
|---|---|---|
listen.port | 5684 | CoAP/DTLS 服务绑定的 UDP 端口。 |
security.connectionIdLength | 8 | DTLS Connection ID 字节长度。蜂窝或漫游设备群应保持非零,使会话在地址变化后存活。0 禁用,每次重新绑定都必须握手。 |
security.idleTimeoutSeconds | 0 | 无流量多久后回收会话。0 永不回收。队列模式应高于唤醒间隔。 |
security.handshakeTimeoutSeconds | 10 | 单次 DTLS 握手上限,避免停滞握手长期占用资源。 |
security.maxSessions | 100000 | 活动会话表上限。超限握手被拒绝并计数,不静默接受。 |
maxLifetimeSeconds | 86400 | 所有注册有效期的上限。直接限制死亡设备显示在线多久。 必须高于设备请求的最长有效期。 |
ingestRateLimit.messagesPerSecond | 1000 | 每租户持续接入上限,按解码读数计数,也在解码前按 Notify 消息计数。未设置或非正值使用默认,不表示无限制。 |
ingestRateLimit.burst | 2000 | 上述限额的突发额度。 |
downlink.timeoutSeconds | 10 | 单次设备指令交换时间上限。到期报告失败,不悬而未决。慢速蜂窝休眠设备可提高。 |
downlink.concurrency | 16 | 跨设备指令并行度。无论此值多少,同一设备自己的指令始终按序执行。 |
Sparkplug 运维
每个来源都是独立对外连接。 来源指定一个消息代理、一个租户和订阅分组。不可达代理使用独立退避循环重试,只影响该来源,不影响 Pod 或其他租户来源。监控 connect_failures_total。
应依据该指标,而不是 Pod 健康状态判断来源连接。
一个分组订阅被拒绝,会停止整个来源,直到修复消息代理 ACL。 来源为全部分组发布一个统一在线/离线状态,不能部分在线。代理可能接受连接却拒绝某分组订阅,通常因为来源凭据无读取权限。此时来源:
- 不宣告在线;
- 不接入任何分组;
- 断开,并使用同一退避循环重试,最大间隔 30 秒。
缺少分组却宣告在线更糟:该组边缘节点会向不存在的订阅刷新缓冲数据,随后来源因静默而将设备标记断开。断开前,来源自行发布离线状态,因为正常断开不触发遗嘱。因此代理已存储却未确认的在线宣告会被替换,不会残留。监控 subscribe_failures_total:任何增长都表示某来源停机,日志会列出被拒绝分组。
重新连接由平台处理,不交给 MQTT 客户端库。 每次重连建立真正全新的会话和时间戳。Sparkplug 要求主机 birth 与 death 证书使用相同时间戳,使边缘节点拒绝旧会话延迟死亡。这也是所有副本共用一个客户端 ID 的原因:代理的重复 ID 接管负责驱逐僵尸主机。
Sparkplug 按租户接入上限计量 DATA 消息中的读数,并丢弃超出部分。不计量消息速率、birth 或 death;消息总大小只受代理限制。参见 Sparkplug 接入限制。
Sparkplug 接入限制
读数消耗租户接入限额。 NDATA 或 DDATA 中每个数值指标算一个读数,使用与其他设备传输相同的按读数计量的租户上限(参见默认允许量)。每条消息在存储前计费一次,处理器内部重试不重复计费。超限读数丢弃并计入 ingest_samples_shed_total。clean-session 主机没有代理重投递,因此丢弃即丢失,与其他传输超限读数相同。
| 设置 | 默认值 | 作用 |
|---|---|---|
ingestRateLimit.messagesPerSecond | 1000 | 每租户持续接入上限,按读数计量,租户层级可覆盖。未设置或非正值使用默认,不表示无限制。 |
ingestRateLimit.burst | 2000 | 上述限额的突发额度,单位读数。 |
未计量的内容。 主机必须观察每条消息以保持会话正确,因此不计量消息速率。birth 的指标值也不计费:Sparkplug 按变化上报,缓慢变化指标可能只在 birth 出现;重连或故障切换后的 rebirth 会一次重发所有节点指标。birth 改由会话限制,每个 birth 序列一次,请求 rebirth 使用退避。birth/death 声明的在线状态转换也不计量。失控边缘节点仍可按代理投递速度持续发送,应在代理或订阅分组范围上限制。
平台施加背压时也丢弃读数。 平台拒绝新事件期间(参见接入链路背压),消息中尚未存储的读数立即丢弃,不进行处理器内部重试,计入 ingest_failures_total。包括不受限额丢弃的 birth 指标值;缓慢变化指标这样丢失后,要等下次变化或节点 rebirth 才恢复。birth/death 的连接和断开转换仍接受。
单事件读数限制通过拆分执行。 此路径的每事件读数上限拆分消息,而不是拒绝。包含数千指标的 DDATA 变为多个事件,每个指标仍对应一条存储读数,各自需要行写入、状态更新和共享引擎上的规则评估。拆分限制单个事件,限额限制每秒读数,但没有限制一条消息的总量。应在边缘节点限制每次发布指标数。
租户生命周期门控仍适用。删除中租户流量与其他路径一样拒绝,计入 tenant_deleted_dropped_total。
未知身份
如何处理未知身份由配置决定。 启用自动注册时,无对应设备的 Sparkplug 身份会创建设备;关闭时丢弃遥测,并计入 unknown_device_dropped_total。边缘节点正在发布却没有数据显示时,检查该指标。
边缘代理
边缘代理不是第四条接入路径。它运行在现场,为本地设备提供普通 MQTT 端点,将发布内容缓冲到本地磁盘,再发布到云端已经接入的相同设备主题。平台端不感知代理参与,因此云服务没有专门针对代理的配置。
它不是 chart 功能领域。 不出现在领域列表或部署配置中,不能像服务一样启用。它作为静态二进制和容器镜像发布,由你自行部署,例如现场网关 systemd 单元、容器或边缘手写 Kubernetes 清单。
缓冲存储
缓冲采用丢弃最旧内容的环形存储。 本地存储是持久磁盘缓冲,默认 1 GiB。满时丢弃最旧的尚未转发事件,以接收新事件,不丢弃最新事件。
这是刻意选择。设备发布时,代理依据自己的持久存储立即确认。丢弃最新事件就会丢弃刚承诺保留的内容,并使中断结束时缓冲只剩陈旧数据,而非最新数据。
每次丢弃都会统计,以缓冲自身首序列减去该代理已转发且已确认事件数计算。后者是投递账目,并且持久化,因此重启后计数不会归零。
有一种情况不覆盖:持久计数不存在,例如首次启动或进度文件被删除时,从缓冲当前首序列初始化。之前驱逐的内容视为已处理,因此这种状态下重启会重置证据。如果丢弃计数重要,应在重启间完整保留存储目录。
缓冲限制字节数,不限制年龄。 等待事件保留设备上报时间;没有时间的 JSON 事件按代理存储时刻补齐。因此到达平台时,至少与链路中断一样旧,若设备之前也缓冲则更旧。平台拒绝比到达时刻早超过 366 天的读数,长期保留事件到达时会丢失。代理不会警告,应观察 spool_oldest_age_seconds。在超过 335 天(28,944,000 秒)时告警,能留下一个月恢复上行链路。
上行恢复后,代理重新转发全部缓冲。对于 JSON 对象载荷,会添加重放稳定身份和事件时间,使已经投递消息在云端唯一性检查中合并,仅显示一次。
其他载荷形状原样转发,采用至少一次投递,不稳定连接后的重连可能投递两次。边缘代理后使用非 JSON 解码器时,应让读数处理容忍重复。
部署代理之前
- 不配置凭据时,本地 MQTT 监听开放。 设置
local.username和local.passwordEnv以要求凭据。开放是受信任局域网中的有效选择,代理启动时会明确警告,使选择可见。无论哪种,都只是网络访问控制,不是逐设备身份;明文 MQTT 会在局域网中明文传送密码。 - 指标和健康端点仅绑定回环地址。 设计上设备 MQTT 端口是代理唯一向局域网暴露的入口。其他主机抓取代理指标时,需要在该机器上提供转发。
边缘代理设置
| 设置 | 默认值 | 作用 |
|---|---|---|
instanceId | — | 必填。转发目标云实例。其他实例的发布不转发,计入 instance_mismatched_total。 |
agentId | — | 必填。上行身份,必须唯一,共用身份的两个代理会循环断开对方。 |
local.listenPort | 1883 | 现场设备连接的 MQTT 端口。 |
local.storeDir | — | 必填。持久缓冲目录,每目录一个代理。 |
local.spoolMaxBytes | 1 GiB | 缓冲预算,超出丢弃最旧事件,最低 16 MiB。 |
local.metricsPort | 9090 | 仅回环的指标和健康端口,明确设置 0 禁用。 |
uplink.brokerUrl | — | 必填。云端 MQTT 转发端点。 |
uplink.connectTimeoutSeconds | 30 | 单次上行连接尝试时间上限。 |
uplink.backoffMinSeconds / uplink.backoffMaxSeconds | 1 / 60 | 链路中断期间重连退避边界。 |
应监控什么
平台发布的告警规则和 Grafana 仪表盘覆盖检测、指令投递、消息、数据库和复制等其他部分。下表指标没有任何默认告警或面板。 指标都会发出并被抓取,但编写自己的规则前,不会通知你。先配置无主实例告警。
所有指标带 devicechain_ 和服务段前缀:devicechain_sparkplugingest_、devicechain_lwm2mingest_、devicechain_edge_。没有逐设备或逐租户标签,因此抓取不存在相应高基数风险。
无主实例告警
每个接入服务首先应配置无主实例告警:
sum(devicechain_lwm2mingest_is_leader) != 1 or absent(devicechain_lwm2mingest_is_leader)
sum(devicechain_sparkplugingest_is_leader) != 1 or absent(devicechain_sparkplugingest_is_leader)
零表示无人提供该传输服务,其所有设备都静默不可达。任何非一值都值得唤醒值班人员。这是整个部分最关键的信号,目前没有默认通知。
absent() 必需,但不是为了无来源的 Pod。两个服务都在初始化无条件注册 is_leader,先于检查是否有服务内容。因此未配置来源的 Sparkplug Pod 整个生命周期都发布 0,!= 1 已足够触发。absent() 覆盖完全没有序列可求和的情况,例如没有副本启动或未抓取。空结果上的 != 1 仍为空,意味着静默而非触发。两种传输都适用,因此都使用配对表达式。
两服务的 is_leader 在副本取得租约时变为一,不等领导任期状态构建完成。正常接管不会在重建期间显示无主;但此时可能隐藏卡在构建中的主实例。LwM2M 的第二个指标 is_serving 可定位,见下文。
Sparkplug 接入指标
前缀:devicechain_sparkplugingest_。
| 信号 | 含义 |
|---|---|
is_leader | 服务 Pod 为 1,其他为 0。总和不为 1 时告警。 |
connect_failures_total | 配置的消息代理不可达。增长表示某来源停机,而 Pod 看起来健康。 |
subscribe_failures_total | 代理接受连接,却拒绝或未确认某分组订阅,通常凭据无读取权限。来源保持离线,不接入任何分组并重试。任何增长都应告警。 |
messages_total | 入站 Sparkplug 流量。活跃设备群中平线表示订阅丢失或来源死亡。 |
presence_emitted_total | 产出的连接/断开信号。 |
rebirth_requests_total | 请求节点重新宣告的次数。持续增长表示节点无法重新同步。 |
rebirth_enqueued_total / rebirth_dropped_total | 会话状态机请求的 rebirth,以及发布队列已满而未接收的数量。丢弃属于延迟信号,不等于终止失败:节点下一窗口会重新请求。但持续丢弃表示请求速度超过发布速度。结合只统计实际发上线路、受发布能力而非需求限制的 rebirth_requests_total:丢弃且请求量升至稳定上限表示扇出超过健康发布者能力;丢弃且请求量平线表示发布本身卡住,应检查代理连接。 |
unknown_device_dropped_total | 关闭自动注册时,无对应设备身份的流量。 |
decode_errors_total | 格式错误载荷。 |
ingest_failures_total | 已接受消息的读数,或 birth/death 声明的连接转换被丢弃,以及在线状态协调产生但无法存储的断开批次。读数在平台背压时立即丢弃;其他情况在内部重试耗尽后(设备管理或代理不可达),或连接关闭期间丢弃。拆分为多事件的消息,前部分可能已存储。clean-session 主机没有重投递,因此这些内容均丢失。 |
ingest_samples_shed_total | 租户超过接入限额而丢弃的 DATA 读数,表示发送量超过层级允许值。 |
tenant_deleted_dropped_total | 租户正在删除而拒绝的流量。 |
samples_too_old_dropped_total | 时间戳早于主机接收时刻超过 366 天的读数,消息其他部分仍存储。超过这么久未变化的指标,每次 birth 都会丢弃;未设时钟、时间接近 1970 的节点全部读数会丢弃。服务警告日志列出设备。 |
LwM2M 接入指标
前缀:devicechain_lwm2mingest_。
| 信号 | 含义 |
|---|---|
is_leader | 持有租约的 Pod 从取得租约起为 1。总和不为 1 时告警。 |
is_serving | Pod 的 CoAP/DTLS 读取循环实际运行后为 1。必须结合 is_leader:这是区分正常重建注册表和卡在重建的唯一方式,其他就绪、存活和 is_leader 信号都可能健康。is_leader == 1 and is_serving == 0 持续超过正常接管时长时,应独立告警。 |
active_registrations / active_sessions / active_observations | 服务看到的活动设备群。重启前后观察 active_observations,可发现上述观察关系丢失及恢复。 |
registrations_total / registration_updates_total | 设备加入和保持会话。 |
registration_expiries_total | 未注销而注册过期,表示设备消失。 |
handshake_failures_total / auth_errors_total | DTLS 失败设备和未预配身份。 |
observe_establish_refused_total | 设备拒绝或其他原因失败的 Observe。仅支持 1.0 的客户端症状:用 4.06 拒绝 SenML Observe,之后不通知。 |
notify_unknown_content_format_total | 确实发送通知,但格式无法解码。仅支持 1.0 的客户端该值为零。 |
notify_decode_failures_total | 格式错误载荷。 |
notify_records_non_numeric_total / notify_records_non_finite_total / notify_records_unnamed_total | Notify 中未产生测量值的读数。非数值是正常情况:布尔或字符串 IPSO 读数说明设备正常,该计数可与完全静默区别,否则表面相同。后两项是固件错误:无穷或 NaN,或没有资源路径。 |
observation_overflow_total | 超过每次注册 32 条观察上限,部分资源未观察。 |
telemetry_too_old_dropped_total | 读数时间早于适配器接收时刻超过 366 天,Notify 其他部分存储。服务警告日志列出设备。 |
ingest_messages_shed_total / ingest_samples_shed_total | 超出租户接入上限的 Notify 消息和读数。 |
notify_ingest_dropped_total | 平台拒绝或无法接收,导致读数丢弃的 Notify,包括背压期间。没有重试,下一 Notify 取代丢失内容。拆分消息的前部分事件可能已存储。 |
shadows_reconstructed_total | 领导权变更后重建的在线状态,突增是故障切换特征。 |
commands_failed_total / commands_not_served_total | 未成功到达的下行指令。 |
command_live_claim_errors_total | 因指令投递服务无法确认而未执行的指令。每个指令在到达设备前立即向指令投递服务确认,未确认绝不发送。持续增长表示 LwM2M 指令都无法到达设备,应为此告警。指令会重试,不会丢失。 |
commands_stale_dispatch_total | 平台已重新准备或发送指令,因此丢弃的投递。不是故障,每次都代表避免了一次重复物理执行。中断或切换后增长是正常现象。 |
commands_overflow_parked_total{reason} | 在指令投递服务暂存、稍后按序发送的指令。full:设备队列已满,响应慢。offline:没有活动连接。bind:刚连接,等待指令尚未交付;故障切换后预期突增,设备同时重连。unconfirmed:前面的指令无法确认。 |
command_overflow_blocked_total | 无法足够快暂存指令而必须等待的次数。只在指令投递服务缓慢或不可达时增长,此时所有 LwM2M 指令都等待。 |
command_drain_turns_total | 排空轮次,每轮最多投递某设备 4 个暂存指令。本指标增长而 commands_overflow_parked_total 平线,表示正在清除积压。 |
边缘代理指标
前缀:devicechain_edge_。
| 信号 | 含义 |
|---|---|
uplink_connected | 0 表示现场正在缓冲。 |
spool_oldest_age_seconds | 首要积压信号,按实际时间表示代理落后多久。 |
spool_used_bytes / spool_limit_bytes | 缓冲距离开始丢弃还有多少余量。 |
dropped_total | 数据已经丢失,为腾出空间驱逐最旧事件。任何增长都是真实丢失。 |
forward_errors_total | 转发失败次数,事件仍缓冲以便再次投递。 |
received_total / forwarded_total | 接收和转发吞吐。 |
malformed_total | 无法转发而被丢弃的事件,避免阻塞后续队列。 |
local_auth_enabled | 0 表示现场 MQTT 监听不要求凭据。 |
验证范围限制
这些服务的验证有两项限制:
- 没有随项目发布的测试工具演练真实 Sparkplug 设备群。 项目不对第三方边缘节点或消息代理进行端到端驱动,Sparkplug 行为由针对平台自身实现的测试覆盖。
- LwM2M 路径验证更充分。 使用独立第三方 LwM2M 客户端栈,并从服务端读取判断结果,防止行为错误的客户端制造通过结果。套件按计划运行,属于参考验证,不是发布门禁。
边缘代理由自身测试覆盖,没有集群内部署验证。推广到设备群前,先在一个现场试点部署。