运行检测引擎
检测规则由一个与平台其他服务不同的服务评估:它将实时状态保存在内存中,由一个活动实例进行检测,并使用事件时间而非系统时钟。本页介绍运维契约:这种设计的收益、代价,以及如何区分健康引擎和卡住的引擎。
如果要了解规则的表达能力或编写方式,请先阅读事件处理与告警。
有意只运行一个活动引擎
任何时刻只有一个引擎进行检测。chart 默认部署单副本,采用先停止旧实例再创建新实例的滚动策略,这是实现该约束的最简单方式。
这不是可以随意解除的扩展限制。引擎将每个打开的窗口、运行中的定时器和已触发边沿的锁存状态保存在内存中,并作为一个检查点提交。两个引擎读取同一流时,各自只能看到部分内容,提交的状态也只建立在局部视图上。
三项机制保护这个不变量,但强度并不相同:
- 发布策略防止新旧实例在发布期间重叠,仅覆盖发布场景。
- 分区租约决定哪个副本可以工作。副本只有持有租约时才获取、确认、保存检查点和发布;租约失效时立即停止。
- 引擎拒绝提交落后于已有检查点的检查点。 驱逐、节点排空或手动删除 Pod 都会立即调度替代实例。若两个引擎短暂同时运行,落后者会停止,而不是覆盖较新的状态。
发布策略仅完全覆盖发布路径。驱逐或节点排空时,替代实例会在旧实例停止前被调度。租约和检查点屏障限制影响:未持有租约的 Pod 停止消费,落后的检查点被拒绝。但这也是应优先主动发布,而不是排空引擎所在节点的原因。
运行热备用副本
可以运行多个副本,额外副本是备用,不是写入者。同时设置:
functionalAreas:
event-processing:
replicas: 2
strategy: RollingUpdate
只增加 replicas 而不更改 recreate 策略会导致渲染失败,因为该策略在创建新 Pod 前停止所有旧 Pod,备用副本恰好会在需要它时停机。
备用副本就绪并提供 API,但不持有租约:不消费、不提交,在主实例释放租约或租约过期后接管分区。它节省 Pod 启动时间,以及驱逐时等待替代 Pod 调度的时间,但不会省略下一节介绍的重启开销。备用副本没有预加载引擎状态,因为这样做意味着读取主实例仍在更新的检查点。
检测触发的动作也遵守此原则。备用副本不派发任何动作,因此引擎中的租户对外动作限额只在进行检测的副本上计费一次。分区转移期间,新旧副本可能短暂同时派发。两者发送的告警更新或连接器请求由消息总线去重存储;指令携带防止二次入队的键,但该窗口中各副本仍会分别消耗自己的对外限额。
单副本没有 Pod 中断预算,排空所在节点会使检测暂停,直到 Pod 重新调度。备用副本可以避免这段等待。
重启的开销
重启是常规操作,不是事故。启动时,引擎加载最近检查点并从该位置重放流,重新推导之前的状态。
如果唯一的引擎 Pod 未释放分区就停止,例如崩溃或内存不足被终止,待派发动作会在替代实例接管后继续,最长约 35 秒。检测恢复更晚:替代实例先等待额外交接期,因为它无法区分已停止的 Pod 和网络断开但仍运行的 Pod,然后执行下述重放。正常关闭会释放分区,跳过这两个等待。Pod 停止时如果消息代理暂时不可访问,会持续尝试释放,直到宽限期结束前不久。消息代理中断超过 30 秒时,即使 Pod 仍运行,持有权也会结束。代理恢复后,引擎重新获取分区、等待交接并重放,因为没有记录证明它之前已正常停止。
| 行为 | |
|---|---|
| 已处理并提交的事件 | 不会丢失。只有包含事件的检查点提交之后才确认消息;未提交内容会重新投递。 |
| 已发送的告警与指令 | 重新推导并发送,再去重:告警更新具有幂等性,指令携带防止二次入队的键。 |
| 已发送的对外 webhook 和连接器发布 | 同样去重。30 分钟内重新推导的检测会被消息总线识别,不会再次派发;约十分钟内重新发送的请求(例如 Pod 停止时正在执行的检测)只存储一次。更长中断后,请求可能再次发送,详见下方投递说明。 |
| 规则触发计数 | 会多计。 重放会再次递增。最近触发时间正确;应将计数视为下界,而非精确总数。 |
| 打开的窗口、保持期和定时器 | 从检查点恢复。部分完成的保持期仍处于同一阶段。 |
| 使用动态属性阈值的规则 | 参见下面的限制。 |
阈值读取设备属性的规则并不完全重放安全。重放读取属性的当前值,而非原事件时刻的值。如果期间属性改变,可能丢失一次触发,或额外产生一次触发。固定阈值规则不受影响。如果需要精确重放,例如审计清除或复现事故,请优先使用固定阈值。
投递:所有路径都是至少一次
遥测链路端到端采用至少一次投递,因此应按可能重复设计,而不是假定只发生一次。
- 检测结果可能重复产生,通过身份标识去重。
- 告警更新具有幂等性,重复更新落在同一个告警上。
- 指令携带由触发推导的键,因此重复不会再次入队。
- 引擎在约十分钟内重新发送的对外 webhook 或连接器发布(同一次检测的重试)会被消息总线识别并只存储一次。此后,请求仍可能两次到达目标,例如长期中断后的重放,或连接器服务因响应丢失而重试。每个请求携带由触发推导的
X-DC-Idempotency-Key头,剩余重复需要由接收端点去重。队列和消息代理目标通过元数据传递该键,大多数消息代理无法直接利用它。对外接收端必须按幂等设计。
下游不可用时,消息保持未确认并按定时器重试,不会连续轰击。五次投递尝试、总计约四分钟后:
- 对外连接器请求进入死信,供检查;
- 某些动作仍无法派发的检测结果也进入死信,同时记录显著错误。死信详情按动作类型和幂等键列出最后一次尝试失败或被对外速率拒绝的动作。其他动作已经执行,或被有意跳过(守卫条件不满足、未启用或参数无效)。
不会自动重新执行死信。记录使故障可见、可诊断,不会消除故障后果。未派发的触发动作要等条件先解除再重新越界才会再次出现;未派发的解除动作则会使本应解除的告警继续激活。死信需要调查,不会自行消化。
被租户对外速率拒绝的动作以 shed 原因进入死信,但受记录预算限制。这会立即发生,不等待重试,因为等待无法使租户重新回到限额以内。速率按触发遥测到达平台的时间计量,因此重启后处理积压时,按原发生时刻计费,而不是瞬间全部计费。超过每租户约每秒一封(突发 60 封)、全局每秒十封的预算后,丢弃动作改为计数,每租户每分钟汇总一封死信。
某个检测动作持续失败时,每次重试都会再次为其 webhook 和连接器动作消耗租户对外速率,即使消息总线只存储一次重发请求。因此,租户达到待处理指令上限等持续故障,可能使同一租户的其他动作被丢弃。引擎重启后重新发布的检测在 30 分钟窗口内由消息总线去重,只存储一次,不会二次派发或计费。
服务在最后一次尝试中放弃的消息,例如处理期间 Pod 停止或处理器超时,也会以 no-outcome 原因记录。这种死信不会终结指令,因为最后一次尝试可能已经完成工作,只丢失确认。高吞吐流(设备事件、指令和检测动作)的死信引用原消息,而不复制它,并说明在原流过期前如何查找;过大的消息也采用同样方式。连接器请求的死信引用连接器服务自己的死信流,后者保留完整请求。记录由拥有消息的服务在下次拉取时写入,因此服务停机期间记录会延迟到达,而不是永远缺失。
引擎读取解析后事件的路径是例外。它只有在检查点包含事件后才确认,并在每次启动时从最近检查点重新读取流。因此,保存检查点失败期间耗尽投递次数的事件不会丢失,也不进入死信,而是被计数并由 ReplayCoveredDeliveriesExhausted 告警报告(参见投递次数耗尽的消息)。
使用 dcctl dead-letters list 读取,它以运营方身份认证:
dcctl dead-letters list --server <host> --email <you> --password <secret> \
--tenant acme --since 2026-09-04T00:00:00Z
实例管理 GraphQL 端点也提供 deadLetters,权限与审计日志相同。记录默认保留 30 天,长于底层消息流,这是单独保存记录的目的。保留期限可按部署配置。
ReactPoisonDropping 告警专门报告这种情况,应紧急处理。
规则动作之间没有依赖。每次投递都会尝试每个动作,因此持续失败的动作,例如租户待处理指令超限或 webhook 端点停机,不会阻塞告警或其他动作。完全不响应的服务也一样:每次尝试限于消息下一次重新投递前的时间,每个动作获得剩余时间中的一份,因此靠前动作不会耗尽后续动作的时间。已成功动作每次重试也会重新发送,并按上述机制去重。不能让一个动作以另一个动作成功为执行条件。
时间:何时发生的含义
引擎使用事件时间,即读数携带的时间戳,而不是消息到达时间。因此需要两项设置。
迟到容忍时间决定引擎等待乱序事件多久,才认定某时刻已确定。如果设备缓冲后批量上传,或上游环节可能停顿,可提高容忍时间;代价是所有基于时间的决策同样延迟。
设备上报时间戳会被限制,如果它相对于平台接收时刻处于过远的未来。这防止错误设备时钟推动整个引擎的时间前沿。过去时间戳视为迟到,不会被截断。
超出容忍时间的行为取决于规则类型,设置前应了解:
- 有窗口的规则丢弃读数:滚动窗口聚合、会话/间隙规则,以及重复、滑动聚合和关联等滑动类型。窗口判断的是一段时间;当前覆盖范围之外的读数不能作为该时段的证据,否则“三次读数在十秒内超过 80”可能被相隔一小时的读数触发。
- 持续时间规则丢弃落后前沿超过保持时长的匹配读数。 这种读数只能改变引擎已经判定的保持期。范围内读数按自己的时间排列,而不是按到达时间。迟到读数若显示条件在持续过程中停止,会从最新匹配读数重新开始;匹配条件的迟到读数不能跨越已观察到的中断重新打开持续过程,因此不能产生证据不支持的持续时间告警。不匹配读数无论多迟到都会终止已触发告警,除非它比告警更早:告警触发前的读数在触发后到达,不会撤销告警。
- 无窗口规则仍评估迟到读数,包括阈值、计数窗口和变化率。它们将读数与前一读数或固定边界比较,没有时间跨度可超出。
所有情况下,读数都会正常存储并展示在图表中;影响仅限检测。
容忍范围内不变:仍落在窗口内的乱序读数会正常合并,这正是容忍设置的作用。窗口因此最多可延长一个容忍时间,但不会更多。
滑动类型和持续时间规则会统计丢弃量,每个丢弃读数的规则计一次。detect_late_samples_total 在以下情况递增:读数到达时所属窗口已过去;持续时间规则丢弃落后前沿超过保持时长的匹配读数;或不匹配读数在告警触发后到达,但其采样时间位于触发该告警的持续过程内、触发之前。早于整个持续过程的迟到读数被忽略且不计数,因为过程在它之后开始。因此规则突然沉默时仍有指标可查,常见原因是存储转发上传。滚动窗口和会话规则静默丢弃,不计入该指标。
持续时间规则在前沿越过保持期末尾时触发,前沿落后最新读数一个容忍时间。显示条件停止的读数如果在此之前到达,会结束过程而不触发,即使所有读数按序到达、条件已保持完整时长。因此,只有条件持续保持时长加迟到容忍时间,才一定触发。
另一个重要性质是:前沿由整个实例共享,不是逐设备跟踪。活跃设备会将它推进到大致“现在”,无论某安静设备离线多久。如果将容忍时间提高到覆盖半小时存储转发上传,所有租户的所有时间决策都会延迟半小时。如果不能接受,应缩短上传批次,或不要对这些指标使用时间窗口规则,参见连接设备。
共享前沿也影响设备时钟。设备时间戳持续落后设备群,超过规则保持时长加容忍时间时,无论因为时钟慢还是传输路径慢,该规则都不会触发:所有匹配读数被视为迟到并计入 detect_late_samples_total。应校正设备时钟,或将保持时长设为大于该滞后。
在平台内部等待的读数也一样。event-sources 停机期间,平台消息代理持续存储设备通过 MQTT 发布的数据;服务恢复后处理积压。每个读数保留自己的时间:设备上报时间,或者没有 occurredTime 时消息代理接收时间。期间只有绕过 event-sources 的传输继续到达;LwM2M 和 Sparkplug 属于此类,会将前沿保持在“现在”,HTTP 接入则由 event-sources 提供,与其一起停机。中断超过规则窗口后,积压对滑动规则,以及超过保持时长的持续时间规则,都会迟到,与存储转发上传相同:正常存储和绘图,不用于这些规则,detect_late_samples_total 递增。
数据缺失规则最快多久触发?
数据缺失或静默规则不能在设备停止上报的瞬间触发,因为没有事件到达来触发评估。最短时间为:
规则超时 + 迟到容忍时间 + 空闲检查间隔与检查点间隔中的较长者 + 一个时钟周期
默认设置下,大约是规则超时再加十五秒。超时应设为真正关心的静默长度,并预期稍后检测,而非恰好该时刻。
数据缺失有两条触发路径,只有一条等待消息代理。如果后续事件将引擎事件时间推进到规则截止时刻之后,立即触发,包括处理积压期间,这符合重放正确性。另一条是真正静默,不会再有事件触发;这条路径按墙上时钟触发,而且必须先由消息代理确认已无待处理内容,避免尚未读取设备事件的积压被误认为设备静默。
已触发告警无法解除
告警由最后一个贡献规则解除时清除,而不是任意规则解除就清除。多个规则触发同一告警时,必须全部解除。
除此以外,最常见原因是规则类型仅在事件到达时重新评估。设备触发告警后完全停止上报,就没有证据表明条件已结束,告警继续激活。重复发生规则在设备持续上报时没有这个问题:携带该指标的不匹配读数仍会使早期匹配退出尾随窗口,计数低于 N 时解除告警。计数窗口和会话规则的问题更严重:计数窗口只计匹配事件,会话也仅由匹配事件打开,因此不匹配流量无法让它们观察到条件结束。告警持续到下一窗口完成,或下一会话关闭且条件不再成立;这可能永远不发生。
推荐与数据缺失规则配对,让停止上报的设备产生独立、可处理的信号,而不是只留下陈旧告警。
还应检查三种原因:
- 运营方手动“解除”不会移除底层条件。 条件仍成立时,下个事件会重新激活同一告警。解除表示你已看到它,不等于抑制。
- 设备离开规则范围后静默,会保留已触发告警。范围变更在设备下个事件生效;
device-management多副本时约五秒内生效,而静默设备没有下个事件。 - 触发告警的规则不再运行。 加载时被跳过的规则,会在配置的 Rule Health(规则健康) 页签显示编译错误,既不评估,也无法解除之前的告警。修复或停用规则后,手动解除告警。
规则不触发
按常见程度排序:
- 配置从未发布。 只有包含规则的配置版本发布后,规则才处理实时遥测。草稿不触发。
- 设备无法解析到配置。 类型没有配置,或配置从未发布的设备,完全不匹配任何规则。不会报错,只是不评估。
- 指标名称与设备发送的不一致。 引用不存在指标的条件有效且可编译,却永远不成立。检查设备最近事件中的精确键名。
- 规则限定的分组不包含设备。 成员关系在事件解析时记录,刚加入分组的设备在下个事件参与;
device-management多副本时约五秒内生效。 - 动态阈值需要的属性未设置。 表单阈值读取设备自己的属性;缺少数值型
SERVER或SHARED属性时不触发。非数值或CLIENT范围属性视为未设置。带回退值的 CEL 表达式使用回退值。 - 规则评估时出错。 这是最难发现的情况,见下文。
- 变更尚未到达引擎。 引擎未收到的发布、回滚、设备或属性变更,会在下次与设备管理比较时发现,详见下文。
- 升级后规则无法编译。 新版本可能拒绝旧版本接受的规则。引擎加载时跳过它,配置的 Rule Health(规则健康) 页签显示编译错误及原因。发布说明列出此类变更。
发布或回滚配置、创建或更改设备类型、设置阈值属性时,会通知检测引擎一次。如果通知丢失,例如消息代理当时不可用,或设备管理在变更后立即重启,变更本身仍有效。引擎接管检测时,以及之后每五分钟,会与设备管理比较所有租户的已发布规则、活动配置版本、设备和阈值属性,并纠正差异。因此丢失通知只造成几分钟延迟,最多约七分钟,删除设备或属性最多约十分钟,不会永久丢失;无需再次发布。
DetectFactsRepaired 表示已执行修复。DeviceFactPublishFailing 表示通知发送失败。DetectFactReconcileFailing 表示比较本身失败,此时遗漏变更会继续遗漏,直到比较成功。
规则表达式评估失败时跳过事件。规则健康仍显示活动、触发计数为零,与条件从未成立的规则无法区分;统计错误的平台指标也不按规则细分。
如果 DetectFanoutEvalErrors 告警触发却无法定位规则,对每个可疑规则运行画布预览。只有预览会把评估错误归属到具体规则。
发布前预览
预览用平台同一引擎重放真实历史,不发布任何内容。这是检查规则最有用的工具,但以下限制解释了多数意外结果:
- 从窗口起点冷启动。之前开始的保持期或窗口不可见,跨越结束时刻的聚合窗口不会关闭。
- 不解析设备属性,所有设备都视为没有属性。因此表单动态阈值预览为永不触发;带回退值的 CEL 对所有设备都使用回退值,即使设备实际有属性。
- 不应用分组范围,有范围的规则会对整个配置预览。
- 无法为从未上报的设备启用数据缺失检查。
- 没有迟到容忍时间。读数相对其他重放历史落后超过滑动窗口或持续时间规则保持时长时,不会使用。
如果预览因窗口已超过保留期或达到扫描限制而截断,或将读数视为迟到而跳过,会明确通知,而不是静默返回较短结果。判断规则不触发前,先阅读该通知。
配置
以下均为可选设置;默认值适合多数部署。
检测引擎没有独立时钟偏差设置。设备时间戳可以领先平台时钟多少,在事件解析时决定一次。存储历史、实时投影、检测和重放都读取同一个已限制值。设置位于设备管理功能领域,名为 maxEventFutureSkewSeconds,单位秒,默认 300。设备上报时间超过平台接收时刻加该值时,按上限存储,不拒绝读数。
另一个方向固定不可配置:读数比平台接收时刻早超过 366 天会被拒绝,不存储也不评估。两方向有意区别处理。超前时钟会冻结共享实时状态,因此限制时间;过旧时间会为没有记录过的时期创建存储,因此拒绝而不是移动时间。参见读数允许追溯多远。
负值在启动时被拒绝,因为它原本意味着完全关闭边界。一个多年后的事件会固定设备最近活动时间;所有投影只保留严格更新的值,因此不活跃清扫再也不会触发,设备永远无法显示离线。没有受支持的关闭方式;如果设备群确实存在时钟漂移,可提高数值。
下面的 watermarkLatenessSeconds 控制另一方向:偏差限制时间戳可领先多少,迟到限制引擎等待落后事件多久。
| 设置 | 默认值 | 作用 |
|---|---|---|
watermarkLatenessSeconds | 5 | 等待乱序事件多久,才认定某时刻已确定。如果事件批量到达或上游可能停顿,应提高此值;它是防止误报数据缺失的主要措施。还容忍解析引入的设备事件轻微乱序,该乱序随 device-management 副本数增加而增长:窗口规则仍统计范围内事件,持续时间规则按其时间放置,其他规则忽略比已见读数更旧的读数。它不覆盖发布失败后重试的事件,后者至少迟到 60 秒。 |
idleAdvanceGuardSeconds | 5 | 引擎必须静默多久,才按墙上时钟触发。负值关闭该路径:数据缺失规则只能由后续事件推动事件时间越过截止时刻,永久静默设备不会触发。 |
detectShards | 1 | 引擎状态按各规则的序列(设备,关联规则则为其锚点)拆成多少份,范围 1 到 64(其他值在启动时被拒绝)。无论取何值,检测结果、事件时间时钟和已保存的检查点都完全一致,因此可在任意一次重启时上调或下调,无需任何迁移。当前版本中各份依次处理,大于 1 的值还不会让检测更快:除非正在做测量,请保持为 1。当前生效的值可在指标 detect_shards 中查看。 |
checkpointEvents | 1000 | 两次检查点之间最多处理的事件数。 |
checkpointIntervalSeconds | 10 | 检查点最大间隔,安静流也会提交。最大 30:检查点负责确认流,间隔接近或超过消息代理 60 秒确认窗口时,会导致安静流消息重新投递。30 秒为检查点本身留出时间,超过该值启动时拒绝。 |
checkpointTimeoutSeconds | 10 | 检查点中的单次调用(发布检测结果、保存快照)最长允许的时间,超时则放弃并将该检查点计为失败,避免检测被不再响应的数据库或消息代理卡住。最大 120。快照保存的上限会自动增长,为上一次成功保存耗时的四倍(最多两分钟),因此在较慢数据库上的大型引擎不会一直失败;仅当第一次保存就比该值慢时才需要调大。 |
maxRuleDurationSeconds | 86400 | 规则可声明的最长时间跨度,包括窗口、保持期、静默超时或会话间隙。强制执行,超限规则发布时拒绝,见下文。 |
maxRulesPerTenant | 500 | 每租户规则上限。仅测量和报告,不强制执行,见下文。 |
maxLiveKeysPerTenant | 1000000 | 每租户活动窗口和定时器上限,同样仅测量。 |
maxRetainedSamplesPerTenant | 5000000 | 每租户打开窗口中保留读数上限,同样仅测量。 |
outboundMessagesPerSecond | 100 | 每租户对外连接器动作派发速率,按触发遥测到达平台时刻计量。 |
outboundBurst | 200 | 上述速率的突发额度。 |
shedLetterPerSecond | 1 | 每租户被丢弃对外动作单独记录为死信的速率。 |
shedLetterBurst | 60 | 上述速率的突发额度。 |
shedLetterGlobalPerSecond | 10 | 所有租户的总记录速率。超过任一预算的丢弃动作计数后,每租户每分钟汇总一封死信。 |
shedLetterGlobalBurst | 100 | 上述速率的突发额度。 |
规则时长上限强制执行
maxRuleDurationSeconds 是上表唯一会拒绝规则而非仅报告的设置。规则声明更长窗口、保持期、超时或间隙时,配置发布被拒绝,错误明确字段和上限。加载已发布规则时再次应用同一上限,保证两处对可运行规则的判断一致。
窗口规则会为每个设备,在整个窗口期间保留每个读数一条记录。内存占用随规则持续存在,而进程由所有租户共享。这项成本不能等观测后再限制,因为指标变化时内存已经分配。提高上限会提高整个实例的风险,应先估算:对使用长窗口的规则,将上报速率 × 窗口 × 设备数 × 32 字节求和。
静默超时和会话间隙即使不保留读数,也按同样原因设限。这些规则在每次设备上报时向定时器堆加入新条目,被取代的条目直到截止时刻才删除。因此,一个每十秒上报的设备,三天静默超时会积累约 26,000 个待处理条目,仅这一台设备就如此。长超时的内存开销与长度成正比,与长窗口相同。
上限在引擎加载规则时执行,不只是发布时。窗口超过当前上限的规则加载编译失败并被跳过,不会运行。证据是引擎日志中的错误,以及配置 Rule Health(规则健康) 页签上的 Compile error(编译错误) 和诊断信息;该页签会按当前上限重新编译所有已发布规则。不会触发告警,也没有指标变化,因为跳过规则没有可测量状态。
两种情况都会静默产生该问题:
- 降低
maxRuleDurationSeconds。 旧高上限下发布的规则在下次重启停止工作,不享有旧规则豁免。 - 升级到引入上限的版本。 限制出现前发布的规则,例如七天聚合,会在升级后首次启动时被拒绝。
降低上限或升级前,检查所有已发布规则是否超过新值,并主动缩短或停用。之后检查引擎日志的 failed to compile; skipping,并在配置 Rule Health(规则健康) 页签确认预期规则正在运行。
表达式成本上限固定
配置发布时会检查检测规则中每个 CEL 表达式的成本,包括条件、动作守卫、载荷模板和告警键模板。最坏估计成本超过 100 的规则被拒绝,错误列出估计值和上限。加载已发布规则时再次执行同一限制。动态分组选择器保存时也使用同一上限。
所有租户上限相同,没有提高单个租户或实例上限的设置。它限制一条读数对所有租户共享的单个引擎造成的工作量。遍历读数测量值的表达式,例如 m.all(...)、m.exists(...),通常最容易超限,应改为明确引用所需指标。
每租户规则数、活动键和保留样本三个上限,超出时只产生指标和日志。不会阻止租户。 一个租户编写病态规则,仍可能耗尽共享引擎内存。监控 DetectTenantOverStateBudget 并采取行动;告警就是实际约束手段。
同时监控三个内存维度。它们有不同失效方式,互不推导,单独看任一项可能仍健康,而引擎内存已经耗尽:
| 指标 | 统计内容 | 增长原因 |
|---|---|---|
devicechain_eventprocessing_detect_live_keys | 打开的窗口和定时器 | 大量设备使用大量规则 |
devicechain_eventprocessing_detect_retained_samples | 打开窗口中保留的读数 | 活跃设备上的单个长窗口规则:一个活动键,数十万条读数 |
devicechain_eventprocessing_detect_pending_timers | 定时器堆条目 | 高频上报下的长静默超时或会话间隙:同样一个活动键,完全没有保留读数 |
后两项正是为了覆盖第一项不增长、内存却最快耗尽的情况。只有前两项具有租户级上限;定时器堆只报告实例总量,因为按租户归属需要每次检查点遍历整个堆。
应监控什么
| 信号 | 含义 |
|---|---|
DetectCheckpointsStalledWithBacklog | 最重要的告警。 有待处理工作时检查点停止,可能数据库或消息代理不可达,或循环卡住。检测没有推进。 |
DetectLoopStalled | 持有分区的副本上的检测循环已超过两分钟没有推进,但该副本仍报告自己处于活跃状态。循环卡在某次调用中,通常是一个已经不再响应的数据库或消息代理连接。本页其他指标都在同一个循环上读取,因此会停留在最后一次的值,看起来一切正常。检测没有进行;请重启告警所指的 Pod。待命副本不会触发该告警。 |
DetectConsumerBacklogHigh | 引擎落后。积压期间抑制静默路径的数据缺失检测;后续事件仍会触发超时数据缺失。 |
DetectWatermarkLagHigh | 引擎事件时间落后实际时间。 |
DetectFanoutEvalErrors | 一个或多个已发布规则评估失败,见上方说明。 |
ReactPoisonDropping | 检测的某些动作每次投递都失败,通常是指令投递服务或消息总线持续不可用数分钟。其他动作每次都尝试;死信列出失败动作。没有自动重放,应紧急处理。 |
DeadLetterWriteLost | 某项工作被放弃,且无法写入死信流。检查消息代理和服务日志。服务拒绝写入死信也计入;设备管理无法编码接入事件失败记录也计入,此时事件已确认却没有发布失败信息;死信存储或指令回写耗尽尝试次数也计入。 |
DeadLetterStoreLosing | 死信已进入流,却无法写入存储,最终会从流中过期而没有记录。检查运营方数据库。 |
ReactConnectorEgressShedding | 按遥测到达平台时间线,租户超过对外速率,动作被丢弃。在预算内,每个动作以 shed 原因进入死信,可用 dcctl dead-letters 读取。重启后追赶本身不会触发。 |
ReactShedLettersOverBudget | 租户丢弃动作速度超过逐条记录预算,超出部分每租户每分钟汇总一封死信。 |
RateMeteringClockFallback | 对外动作缺少触发时间,连续一小时按消息代理或到达时间计量,可能把追赶误认为洪峰。检查事件处理与对外连接器是否运行同一版本。触发时间晚于消息代理时间时另以 capped 来源统计,不触发本告警;这是 Pod 与消息代理时钟偏差,不是缺失时间。 |
DetectFactReconcileFailing | 无法与设备管理比较规则、配置版本、设备和阈值。失败期间不会移除内容,但遗漏变更持续遗漏,直到恢复。 |
DetectLiveGapFillFailing | 实时检测已停止,因为无法读取事件流的一部分。引擎不会跳过尚未看到的事件,因此在代理恢复响应之前不会应用任何新事件,积压会增长。规则和租户的变更仍会应用,且不会丢失任何数据。请检查代理以及引擎与代理的连接。如果十分钟后仍无法读取同一范围,引擎会重启检测任期并从事件流中重新读取。如果事件流仍无法读取,重建同样会失败,连续五次重建失败后 Pod 会重启:即 JetStream 中断超过十分钟且租约仍在时,最终会以 Pod 重启收场。 |
DetectLiveDeliveriesRecovered | 代理已投递的事件未到达引擎,引擎已从事件流中读取这些事件并按顺序应用。检测结果是正确的。如果反复出现,说明与代理的连接在断开,或有两个引擎同时在消费。 |
DetectFactsRepaired | 引擎修复了未收到通知的状态。检测已恢复正确;反复出现说明通知丢失。 |
DeviceFactPublishFailing | 设备管理无法发送变更通知。变更仍会到达引擎,只延迟几分钟。 |
DetectTenantOverStateBudget | 租户超过不强制执行的规则数、活动窗口和定时器,或窗口保留读数上限。 |
两个引擎同时作为写入者时,检查点因陈旧被拒绝的引擎停止检测、报告未就绪,并以非零状态退出,由新实例替代。它不会保持运行并假装健康。可观察到事件处理 Pod 重启,退出前日志显示检查点因陈旧被拒绝。两分钟没有副本持有分区时,DetectHasNoLeader 触发。
控制台设备配置的 Rule Health(规则健康) 页签提供逐规则状态、最近触发时间、触发计数,以及实时检测结果流。
检测循环的时间花在哪里
引擎落后时(DetectConsumerBacklogHigh),决定怎么处理的关键是循环把时间花在了什么上。
devicechain_eventprocessing_detect_loop_seconds_total 回答这个问题:循环在各阶段累计花费的秒数,带 phase 标签。各阶段瓜分循环的全部时间,因此某一阶段的速率就是它占墙钟时间的比例;除 fetch_wait 之外的阶段加起来为 1,意味着循环已没有任何空闲时间。(parked 和 probe 是等待而非工作,但这是循环无法用于处理消息的时间。)
phase | 循环正在 |
|---|---|
fetch_wait | 空闲,等待下一条消息或定时触发。 |
decode | 读取消息:序列检查、租户和载荷。 |
plan | 确定一条读数喂给哪些规则,并评估这些规则的条件。 |
apply | 用结果运行检测引擎。 |
publish | 把检查点的检测结果交给消息总线。 |
save | 在检查点把引擎状态写入数据库。 |
ack | 确认检查点已保证安全的消息。 |
gapfill | 读取消息总线声称已投递、而引擎从未收到的一段事件。 |
control | 消息之间的一切:规则、设备和阈值更新、租户删除,以及周期性维护。 |
parked | 关闭实时消息后的等待:要么卡在无法提交的挂钟推进之后,要么卡在无法读取的事件流缺口之后。这是引擎在重试成功前拒绝工作。 |
probe | 等待消息总线返回积压数量;引擎用它判断自己是否已追上进度并上报滞后。 |
积压时大部分时间处于 fetch_wait,说明瓶颈不在引擎:它在等待消息到达,增加引擎容量没有帮助。parked 或 probe 中的时间属于同类等待:等重试或等消息总线,而不是等引擎。大部分时间在 plan 和 apply,说明引擎本身就是上限。大部分时间在 publish、save 和 ack,说明代价在检查点,devicechain_eventprocessing_detect_checkpoint_duration_seconds 显示每次检查点的耗时。(devicechain_eventprocessing_detect_checkpoint_seconds 只保留最近一次。)
sum by (phase) (rate(devicechain_eventprocessing_detect_loop_seconds_total[5m]))
删除租户
检测引擎将租户状态保存在无法查询解释的不透明检查点中,因此租户删除直接要求引擎驱逐租户,并等待引擎确认驱逐已提交,而不只是修改内存。启用检测的实例必须确保引擎可访问,删除才能完成;引擎停止或不可访问时,删除保持未完成,不会在数据仍存在时宣布完成。