命令
DeviceChain 的命令是双向且持久化的。发出的命令会根据设备能力契约验证并存储。只有设备确实能够接收时才派发,并持续跟踪,直到设备报告结果或命令生存时间(TTL)耗尽。这里没有发出后就不再关注的命令。
发出命令的方法见发送命令。
能力契约
设备配置文件可以声明设备接受的命令。每条命令都有带类型的参数模式:名称、数据类型、是否必填、最小/最大值和枚举。这些声明使配置文件成为契约,而不是标签。
命令入队时,根据已发布配置文件版本验证,而不是草稿。有三种结果:
| 配置文件…… | 结果 |
|---|---|
| 没有声明命令 | 接受任何命令。声明命令集合是可选的,因此尚未采用它的配置文件保持原有行为。 |
| 声明了命令,且键匹配其中一个 | 按该命令参数模式验证载荷。未知参数、错误类型、超范围值和缺失必填参数全部被拒绝。未声明参数的命令接受任何格式正确的 JSON 载荷。 |
| 声明了命令,但键没有匹配项 | 拒绝。不能向设备发送其能力契约不包含的命令。 |
命令键精确匹配,包括大小写。大小写错误的键意味着错误的控制操作,这正是验证要阻止的情况。
验证有意读取已发布快照。已经编写但尚未发布的定义还没有传达给任何下游,强制执行它会拒绝设备实际上接受的命令。发布配置文件,才能让新命令生效。
你也可以读取已发布命令集合。平台通过与入队检查相同的查询,从配置文件已发布版本解析设备当前接受的命令。这里没有设备自行报告的内容。控制台使用该列表直接提供命令:已声明命令选择器,以及根据选中命令参数模式构建的带类型表单,而不是自由文本框。没有声明命令的配置文件仍显示自由文本表单,与平台接受范围一致。已编写但未发布的命令在选择器旁列为不可用,因此缺失命令会被理解为“尚未发布”,而不是功能缺失。
命令生命周期
发出的命令会持久化,并通过一组状态跟踪。
以下状态表示命令尚未完成:
QUEUED:已接受并验证,等待首次派发判断。它通常是过渡状态,命令不会长期停留在此。例外是等待维护窗口的破坏性较高的命令:它保持QUEUED,dispatchAfter设为下一次开启时间,直到窗口开启。HELD:平台明确知道设备不在,因此有意保留命令。这是离线设备群积压所在的状态,可以持续数天。保留的命令仍属于处理中,可取消;TTL 到期记录EXPIRED而非TIMEOUT,因为命令从未发出。设备返回时,它重新进入QUEUED。已排队的命令所属设备随后被停用时,命令同样被保留,并一直保留到设备被重新启用,而不是到它重新连接:参见为已停用设备保留的命令。SENT:已发布到设备自己的命令主题,等待响应。应理解为向平台认为在线的设备派发,而不是设备已收到的证明。设备在在线检查与发布之间掉线,仍会进入此状态。持续数分钟没有结果的命令,可能已没有任何组件持有;参见平台失去命令跟踪时。PARKED:命令已发布,但传输发现设备没有可用连接,平台仍持有它。这用于休眠设备,命令在下次唤醒时投递。与HELD一样,它仍属于处理中,可取消;TTL 到期记录EXPIRED而非TIMEOUT,因为命令从未到达设备。参见注册不等于可达。LwM2M 设备仍连接时也可能出现PARKED:设备队列已满、刚重连,或前面的命令无法确认。此类命令稍后按顺序投递,无需等待唤醒。
以下是终态,进入后不再迁移:
-
SUCCESSFUL/FAILED:设备报告了结果。平台自身无法完成命令时也记录FAILED:- 设备传输完全无法承载命令;
- 设备已回答,但所有尝试后平台仍无法记录答案;
- 平台无法向设备发布命令,并已停止重试;
- 命令在等待维护窗口,而其设备的配置文件已不再定义它。
每种情况下,命令的
error字段都会说明原因,避免将平台侧FAILED误认为设备报告故障。 -
TIMEOUT:已派发,但设备从未回答。自行执行交换的传输会以同样方式报告不回答的设备,因此 LwM2M 适配器的“无应答”同样落为TIMEOUT(error中保留其自身措辞),而不是FAILED。 -
EXPIRED:尚未到达设备,TTL 就已耗尽。 -
CANCELLED:运维人员或租户取消了命令。
EXPIRED 和 TIMEOUT 回答不同问题,混淆会导致排查方向错误。EXPIRED 表示命令未到达设备,连续出现说明投递未成功;TIMEOUT 表示命令已发出但没有回复,指向设备。
已结束命令的含义
每条终态命令还带有一个 outcome,说明实际发生了什么,因为仅 FAILED 就涵盖三种不同的事实:
SUCCEEDED:设备报告成功。NOT_DELIVERED:已证明从未到达设备(EXPIRED、CANCELLED,以及传输无法承载命令、平台停止发布,或设备的配置文件不再定义某条正在等待窗口的命令的FAILED情形)。REJECTED:设备收到命令并报告失败。UNKNOWN:可能已执行:TIMEOUT,或设备已回答而平台丢失了回答的FAILED。
它由结束该命令的写入记录,而非根据 status 和 error 推断,进行中的命令没有该值。批次重试按它选择。
向不在线设备发送命令
通过 MQTT,命令只到达当时已连接并订阅的设备。消息代理不会保留命令等待设备稍后领取。不检查时,向缺席设备发布的命令会丢失,平台无法知道:记录为已发送、没有回复,一周后变成 TIMEOUT。这会形成永久记录,归咎于从未收到命令的设备。
因此平台在发布前检查。传输报告设备未连接时,命令进入 HELD,不发布。设备返回时,命令回到 QUEUED,通常在重连后一两秒内,因为拥有连接的传输会直接报告。
定期扫描也根据平台当前对设备的认识,重新检查保留命令,因此即使通知丢失也能释放积压。两条路径都依赖平台知道设备已返回。在线状态记录变化才真正解除保留,通知只是加快执行。
有四项限制:
- 检查需要报告连接的传输。 对只传递数据的设备,“近期没有事件”不是无法接收的证据。每小时上报的设备每小时静默 59 分钟,但始终可达。这些命令仍按原方式派发。
- 这是检查,不是队列。 设备在检查与发布之间断开,命令仍会丢失。检查消除的是平台事先能知道的情况。为休眠设备维护自身队列的传输单独处理,参见注册不等于可达。
- **只有来源启用后,才会向 Sparkplug 设备投递命令。**Sparkplug 节点位于你自己的 MQTT 基础设施,平台通过其 Host Application 会话发送,且仅在来源主动启用时。关闭时(默认)命令立即记录为
FAILED;启用后,命令由节点上报该值来确认,而不是靠应答。参见 Sparkplug 命令。 - 保留可以比引发它的传输活得更久。 只有报告设备缺席的传输可以报告其返回。如果该传输停止运行,积压会等待各命令自行过期。将设备改回推断在线状态可以释放命令,因为保留依据是设备被明确报告缺席,而推断状态的设备并非如此。
对于已知间歇连接的设备,连续 TIMEOUT 仍应优先理解为连接时间问题,而不是固件问题;不过这种连续情况现在应少得多。
注册不等于可达
在线状态检查询问设备是否已注册。对于有意休眠的设备,例如队列模式 LwM2M 设备,这与当前是否可达不同。设备已注册,因此检查通过、命令被发布,但传输发现没有可用连接,命令无法到达。
这与上面的情况相同,只是低一层,以前结果也相同。命令停在 SENT,而该状态也表示“平台已交给设备”,因此一个状态有两种相反含义。一周后记录变成 TIMEOUT,归咎于从未获得命令的设备。
现在这类命令记录 PARKED:已发布、没有接收者,仍由平台负责在设备下次唤醒时投递。由于平台仍持有:
- TTL 到期记录
EXPIRED而非TIMEOUT。 命令没有到达设备,记录准确说明,不归咎于设备未回答。 - 可以取消单条命令或整个设备群操作中的命令,取消后不会在下次唤醒时发出。但这不保证操作从未执行。退回可能是设备连接失效前已收到的发布的重试,因此取消应理解为“不会再次投递”,不是“从未执行”。过去,取消设备群操作会把仍被平台持有的暂存命令报告为已发送、无法撤回。
- 计入租户未投递命令上限,与
QUEUED和HELD相同,因为平台仍承担这项工作。
PARKED 不总表示设备休眠。LwM2M 适配器在设备仍连接时,也会因该设备队列已满、刚重连且等待命令优先,或前面的命令无法与 command-delivery 确认而退回命令。这些命令也记录 PARKED,稍后分批按顺序投递给已连接设备,而不是等下次唤醒。
平台失去命令跟踪时
退回机制只在仍有组件持有命令时有效。命令也可能进入 SENT 后,没有任何组件继续处理:
- 发布它的 Pod 在记录结果前退出;
- 传输用完重试次数后放弃;
- 实例没有运行执行退回的组件。
设备和命令本身都没问题。命令已经没有所有者,而 SENT 的退出都需要所有者,除了将它记录为 TIMEOUT 的 TTL。
这与 PARKED 要消除的错误标记相同,只是来自另一条路径,平台采用相同方式解决。后台扫描查找停在 SENT、没有结果且时间超过平台可能仍在处理的命令。阈值从消息层重试预算和运维人员可配置的最慢投递扫描推导,目前约十分钟,不是随意指定。可以安全重新启用的命令进入 PARKED,与其他暂存命令一样在设备下次唤醒时投递。
两项限制是有意的:
- 只适用于 LwM2M 设备。
PARKED断言命令没有到达任何设备,普通 MQTT 设备无法作出这种断言。MQTT 命令实时投递给当时连接者,无法区分“没有到达”和“到达但答案丢失”,因此这些设备的行为不变。 - 重新启用接受答案丢失时可能执行两次。 没有记录结果,不等于从未执行:设备可能已执行,但报告丢失。重新启用仍是更好的选择,因为替代方案是把平台确实未投递的命令必然记录为
TIMEOUT,归咎于无过错设备。这与退回机制相同,都是至少一次保证:应理解为“会被投递”,不是“此前未执行”。
延迟到达的投递不同,不会导致第二次执行。长时间 LwM2M 故障后,重新启用命令的原始投递仍可能出现,例如故障切换后重新投递,或适配器恢复后首次读取。LwM2M 命令到达设备前,适配器会与平台确认持有投递仍是命令当前投递。平台已重新启用或重新发送过的旧投递会被丢弃,不执行。确认会将 sentTime 移到实际向设备发送的时刻。
租户可以保留多少积压
未投递命令积压只有三种消退方式:
- 设备返回或唤醒;
- 命令 TTL 到期,记录
EXPIRED; - 有人取消,记录
CANCELLED。
设备群持续离线且无人干预时,积压保留到 TTL 到期。因此它按租户限制,每个层级都有真实数值:任何设置都不表示“无限制”。 无界积压会让租户触发运维人员看不到的持久存储增长。
限制沿级联解析:有租户覆盖值就使用它,否则使用层级值,再否则使用平台默认 10,000。已达到限制的租户下一条命令会被拒绝,代码为 HELD_CEILING_EXCEEDED。
计数包含所有 QUEUED、HELD 和 PARKED 命令,不只是因设备缺席被保留的命令,因此即使整个设备群在线,也可能仅因入队处理中数量被拒绝。
排队命令在一个处理周期内排空,因此约贡献一个周期的入队数量,虽小但不为零;接近上限并高速发送的租户会遇到这一限制。
一部分上限为投递机制预留
不是全部上限都供你使用。其中一部分——默认 20%,即平台默认 10,000 中的 2,000——为平台自身命令投递保留,只有平台可以使用。所有代表你发出命令的入口都受剩余额度限制:控制台、SDK、dcctl 和自有集成一视同仁。
预留源于设备群操作可能造成的情况。“重启所有泵”是一项合法请求,却能立即占满上限。之后,自动化规则尝试发送的每条命令都会被拒绝,直到积压排空;离线设备群可能等待数天。预留让设备群操作进行期间,告警驱动自动化仍能工作。
发送一条或一万条命令时都同样适用。批次接受到与单条命令循环相同的限制,没有绕过方式,也没有哪种形式更有优势。参见一条命令,多台设备。
拒绝信息指出实际适用的限制。预留限制生效时,还会说明预留多少,让看到上限 10,000 却在 8,000 被拒绝的调用方能够区分两个数字。预留是运维设置,不是租户设置,不能逐租户提高或降低。
重试上限拒绝
HELD_CEILING_EXCEEDED 是入队路径唯一的临时拒绝。其他拒绝说明请求在下一次尝试仍然错误;这一项会随着积压投递自行解除。只有这个代码值得重试,其余应该提示用户处理。参见发送命令。
谁报告结果
只有设备可以报告成功。SUCCESSFUL 和设备报告的 FAILED 是只能由设备产生的两种结果。其他终态由平台自行写入:
TIMEOUT、EXPIRED或CANCELLED;- 传输完全无法承载命令导致的
FAILED; - 设备答案已到达,却无法记录到命令导致的
FAILED; - 平台无法发布且停止重试导致的
FAILED; - 命令在等待维护窗口而其设备的配置文件已不再定义它导致的
FAILED。
第二种和第三种都不是 TIMEOUT,理由从相反方向一致:一种设备确实回答了,另一种设备根本没收到内容。两者使用 TIMEOUT 都会让你去检查正常硬件。
- 答案丢失时,命令
error字段说明情况,设备报告结果无法恢复。如果重要,请再次询问设备。 - 命令无法发布时,完全没有内容到达设备,修复原因后可以再次发出命令。
报告结果是设备一侧的契约,参见响应命令。从不响应的设备会让命令保持 SENT,直到 TTL 将其变为 TIMEOUT。例外是 LwM2M,数分钟没有结果的命令会重新启用,等待下次唤醒,详见平台失去命令跟踪时。
每条命令都有 TTL:通过 expiresAt 指定,或使用平台默认七天。如果设备不报告结果,而一周超过命令有效用途,请自行设置。
检测规则的发送命令动作通过 expiresAfter(如 15m 的时长,范围为一分钟到七天)设置同样的限制。命令的 expiresAt 为规则触发的时刻加上该时长,因此发送给离线设备的命令会在几分钟后变为 EXPIRED,而不必等满一周。
取消记录为 CANCELLED。直到最近,取消和 TTL 到期共用 EXPIRED,因此更改前取消的命令仍显示 EXPIRED。历史数据中两者都有,且没有记录哪些 EXPIRED 来自取消,因此之后无法区分。
每台设备在只属于自己的主题接收命令,并只被授权访问该主题。设备无法观察租户内发给其他设备的命令。
投递顺序不是执行顺序
平台按入队顺序投递同一设备的命令,但设备是否按顺序执行由设备决定。一次处理一条命令的设备会按顺序执行,同时运行多个处理器的设备则不会,除非把命令分组。例如,.NET SDK 的 MaxConcurrentCommands 设置会放弃不同命令间的执行顺序,除非它们共享同一通道。顺序本身有意义的序列,例如先写固件再执行,必须在设备侧分组,因为平台无法让同时运行两条命令的设备串行执行它们。
维护窗口
有些命令会中断设备(如重启)或清除其状态。维护窗口是一个每周重复的时间表,规定此类命令何时可以开始。窗口关闭时到达的命令会被接受,并等待下一次开启。
影响级别与窗口
每个命令定义都有一个 impact:NORMAL(默认:读取或无害的写入)、DISRUPTIVE(中断设备,如重启)或 DESTRUCTIVE(清除状态)。只有 DISRUPTIVE 和 DESTRUCTIVE 命令会为窗口而保留;NORMAL 命令从不保留。影响级别是配置文件已发布词汇表的一部分,因此随已发布版本一起传递;在该字段出现之前发布的定义按 NORMAL 读取。LwM2M 的 lwm2m.reboot 命令创建时为 DISRUPTIVE;你已声明的定义不会改变。
维护窗口是租户资源:令牌、名称、一个 IANA 时区(如 Europe/Berlin,Local 和未知名称会被拒绝)以及 1 到 56 个每周时段。每个时段包含星期(MONDAY 到 SUNDAY)、以该时区 24 小时制墙上时钟表示的 start(HH:MM)以及 1 到 10080 的 durationMinutes。时段可以跨过午夜,也可以跨过一周的结束。
[
{ "day": "SATURDAY", "start": "22:00", "durationMinutes": 240 },
{ "day": "SUNDAY", "start": "02:00", "durationMinutes": 120 }
]
窗口分配给设备类型,每个类型最多一个。这就是 v1 的全部范围:没有分组、按设备或租户默认的窗口,没有禁止窗口或节假日,也没有每窗口的并发限制。
保留记录在命令上
当 DISRUPTIVE 或 DESTRUCTIVE 命令被入队到类型带有窗口的设备,且窗口处于关闭状态时,命令会被接受并等待。它保持 QUEUED,dispatchAfter 为下一次开启时间,maintenanceWindowToken 指明窗口。没有新增状态。窗口开启时,命令照常发送,但仍绑定在这次开启上:如果窗口关闭时它还没有发出(例如设备离线),它会等待下一次开启,而不是在设备返回时立即发送。单条命令、批量命令以及检测规则的发送命令动作均适用。
保留是命令行上的一个字段,由认领(claim)检查:命令在 dispatchAfter 之前不能被认领投递,其绑定的开启关闭之后也不能。这是有意为之。每条投递路径都以认领结束,所以规则只写一次,没有任何路径可以绕过:周期性投递扫描、LwM2M 唤醒排空(休眠设备醒来)以及实时确认都遵守它。如果把闸门放在每条路径前面,就要求所有现有和未来的路径都记得它。
窗口只控制开始。在窗口内已被认领的命令何时结束就何时结束。等待中的命令计入租户的未送达命令上限,并可像任何排队命令一样取消,包括整批取消。批次记录会报告 waiting:批次触发时有多少被接纳的成员因窗口而被保留。这是存储的事实,旧批次为 null。
拒绝而非注定失败
如果下一次开启时间晚于命令的 expiresAt(默认有效期为七天),入队会以稳定代码 WINDOW_AFTER_EXPIRY 被拒绝,而不是接受一条只会过期的命令。在批次中,它是与其他拒绝一样的逐设备拒绝:除非设置 allowPartial,否则整个批次被拒绝。
为什么用每周时段加时区,而不是 cron
运维人员描述维护的方式是“周六晚上,四小时”,而不是一个表达式。命名时区中的每周时段直接表达了这一点,保存时可以完整校验,也可以以日历形式展示。cron 会允许不属于维护窗口的时间表(每 7 分钟、每月 31 日),并且在命令仍在等待时很难解释。
夏令时遵循两条规则。不存在的开始时间(春季跳变的空档)在时钟跳变的那一刻开启。出现两次的开始时间(秋季回拨的重叠)在第一次出现时开启。持续时间是经过的时间,因此四小时的窗口在夏令时切换日也是真实的四小时。
编辑和删除窗口
已在等待某个窗口的命令会在大约一分钟内跟随新的时间表。命令投递中一个持有领导者锁的轮次会轮询窗口的所有者以获取当前时间表。这是轮询而不是事件,所以上限约为 60 秒,即使通知丢失也能收敛。计数器 command_delivery_window_restamps_total 统计其保留被移动的命令数:窗口被编辑或删除,在设备返回前再次关闭,或其设备类型改用了另一个窗口或不再有窗口。
- 只要有设备类型在使用,删除窗口就会被拒绝。如果窗口仍然消失了,等待中的命令会被释放并正常派发。
- 如果设备在整个开启期间都离线,窗口一关闭,其未送达的命令就不能再被认领,并由同一轮次顺延到下一次开启。如果命令投递无法访问窗口的所有者,等待中的命令继续等待,不会被释放。
- 等待中的命令跟随其设备类型,而不是标记它们时的窗口。如果某个类型改用另一个窗口,同一轮次会把已在等待的命令移到新窗口的下一次开启。如果类型的窗口被取消分配,它们会被释放并正常派发。已停用设备的命令留在标记它们时的窗口上,直到设备被启用;随后的轮次再移动它们。
- 只有窗口的变更才会释放等待中的命令;配置文件的变更不会。如果设备类型改用把该命令标为
NORMAL的配置文件版本,已在等待的命令会继续等待该窗口开启,或直到你取消它们。变更之后入队的命令遵循新的配置文件版本,不会被保留。如果新的配置文件版本完全不再定义该命令,等待中的命令永远不会被发送:它们以FAILED结束,结果为NOT_DELIVERED,其error为 "Command no longer defined by this device's profile"。计数器command_delivery_window_commands_undefined_total统计这些命令。 - 释放不是暂停。被释放的命令会在投递轮到它们时立即发送,不受任何窗口限制,因此从一个有大量等待中破坏性命令的类型上解除窗口,会在大约一分钟内把它们全部发出。若要停止等待中的命令,请在解除窗口之前取消它们(或其批次),或者把类型改用另一个窗口。
- 如果一条 LwM2M 命令在窗口内被认领,而窗口在它能发送给设备之前关闭,它不会被发送,而是回到等待状态(
PARKED),等待下一次开启。
不受控制的情况
- 原始
lwm2m.execute,或其键在已发布词汇表中未声明为DISRUPTIVE或DESTRUCTIVE的任何命令,都不受控制。这包括未声明词汇表的配置文件。 - 空中升级的控制尚未构建。同一个窗口解析器已对外提供,供以后的升级功能使用。
- 入队时如果无法访问设备管理,命令会被拒绝:闸门失败即关闭,绝不会不受控制地发送命令。如果命令投递没有配置设备管理端点,闸门即为关闭状态,启动时会有警告说明。
设置方法见维护窗口。
一条命令,多台设备
命令批次将同一命令分发到多台设备,记录为单个操作。可以显式指定设备,也可以从实体组解析,每台设备收到相同命令键和载荷。发出方法见向设备群发送命令。
上述行为仍逐设备适用:每条命令按设备能力契约验证,设备不在线时保留,跟踪相同生命周期,并使用相同 TTL。批次不改变单条命令的行为,而是改变平台对整个操作记录什么。
记录正是关键。被平台拒绝的设备根本没有命令行,没有表示“想创建但未创建”的状态。没有批次记录,拒绝就不留痕迹,晚上发送设备群操作、早晨回来的运维人员将没有内容可查。记录保留目标解析出多少设备、实际入队多少,以及哪些被拒绝和原因。
批次与单条命令循环有三项区别,都不只是便利:
- 分组目标在发出时冻结。 批次解析分组已发布成员关系,绝不使用草稿选择器,并记录解析所用分组版本。之后编辑分组不能改变已发出的内容,审计仍能回答批次发出时分组意味着什么。
- 部分分发是明确决定,不是默认行为。 真实设备群中某些设备无法接受命令,调用方必须声明是否接受这种情况。不接受时,整个批次被拒绝,不创建任何内容,包括记录,因为没有操作发生。接受时,可接收设备获得命令,其余记录为拒绝。
- 可以作为单个操作取消。 撤销循环需要分别取消每条命令,并仍知道所有发出时使用的令牌。
记录上的数量——解析设备数、接受设备数——描述的是批次发出时刻,不是现在。命令行不会永远存在,实时计数可能低于创建时真实数量,却没有拒绝说明缺口。回答“排队的 5,000 条已有多少发出”等现在时问题时,应搜索批次创建的命令,而不是重新读取批次。
拒绝以两种方式保存,大规模分发需要两者:
- 单独设备列表,限制长度,避免一次设备群操作保存无界数据块;
- 完整的按代码汇总数量,绝不截断。
汇总让记录可以自行核对:即使设备名称样本较短,解析设备总数仍始终等于接受数加上所有拒绝数。
取消批次只停止尚未发出的命令
取消批次将 QUEUED、HELD 或 PARKED 命令移到 CANCELLED。已经 SENT 的命令不变,收到命令的设备仍会执行。
SENT 是唯一边界:平台取消仍持有的内容,不干预已经发到线上的内容。SENT 命令已向认为在线的设备派发,取消无法撤回,只会让平台停止等待设备答案,将真实结果替换为“运维人员取消”的记录。整个设备群会执行,响应却被丢弃,记录说操作已取消。
取消单条命令使用完全相同边界:取消 QUEUED、HELD 和 PARKED,SENT 原样返回,而不是改成 CANCELLED。参见取消单条命令。
LwM2M 设备多一道停止检查。适配器在执行前立即与平台确认命令。批次取消时,已发布但尚未到达设备的命令会在此停止,并记录 CANCELLED。批次取消结果仍将它计为已发送,因为取消时它确实处于该状态。
批次取消绝不拒绝。因为部分设备已执行而拒绝启动“刹车”,会让其余设备仍受命执行,这是最坏结果。因此,它始终生效,并报告拦住多少命令。批次记录标记取消时间和该次捕获数量。首次标记优先:第二次取消不会覆盖第一次记录。