qimy1314524 commented on issue #4446:
URL: https://github.com/apache/hertzbeat/issues/4446#issuecomment-6095244554

   AI读取源代码后的分析及链路
   # HertzBeat 1.9.0 恢复通知重复:源码分析与修复建议
   
   ## 结论与边界
   
   已读取官方 `v1.9.0` 源码,提交 
`a85bd46c18d33130298f0e223bd84ea6e29a17de`。源码存在可明确推导的重复恢复通知路径:恢复成员发送后被从分组缓存删除;同一恢复再次到达会重新加入并立即发送;持久化层对“已恢复
 → 相同恢复”没有重复判断;通知分发层存储后再次提交通知。
   
   该路径能够解释用户截图:实例、开始时间、触发时间、恢复时间和告警次数均不变,而通知每两分钟发送一次。截图没有完整 labels 和 HTTP 
入站记录,因此不宣称已完成用户部署的端到端复现。
   
   本次仅分析,未修改产品源码、配置或部署。下载目录经过长路径检出修复后,`git status --porcelain` 为空。未运行 Java 
单元测试:项目要求 Java 25,当前 `java` 为 21,Maven 实际使用 Java 8。
   
   ## 实际调用链
   
   ```text
   AlertReportController.receivePrometheusServerAlert /api/v2/alerts
     → PrometheusExternAlertService.addExternAlert
     → AlarmCommonReduce.reduceAndSendAlarm / reduceAlarmTask
     → AlarmGroupReduce.processGroupAlert
     → processAlertByGroupDefine → sendGroupAlert
       (未匹配分组时为 sendSingleAlert)
     → AlarmInhibitReduce.inhibitAlarm
     → AlarmSilenceReduce.silenceAlarm
     → AlertNoticeDispatch.dispatchAlarm
     → DbAlertStoreHandlerImpl.store
     → AlertNoticeDispatch.sendNotify → sendNoticeMsg
   ```
   
   所有下列路径相对于 `hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/`。
   
   ## 关键证据
   
   ### 1. 接入层每次都创建恢复事件
   
   `service/impl/PrometheusExternAlertService.java:76–91`:若 `endsAt` 早于当前时间,设为 
resolved;映射 startsAt/endAt;每次均调用 reduceAndSendAlarm。没有已处理恢复事件判断。
   
   
`reduce/AlarmCommonReduce.java:100–127`:生成指纹后转交分组。指纹由排序后的标签生成,并排除若干时间标签。指纹生成本身不等于去重,没有“已通知”状态表。
   
   ### 2. 分组规则要求所有标签都存在
   
   `reduce/AlarmGroupReduce.java:201–223, 240–242`:对每条规则检查全部 
requiredLabels。没有任何匹配则 
sendSingleAlert,直接下送。策略名称不会按“磁盘告警”“监控系统告警”的文字语义筛选。若多个规则都匹配,会逐条处理,没有匹配第一条后 break。
   
   因此用户的 `service` 必须确实是原始 labels 的键,不能只存在于 annotations,也不能实际拼写成 
`services`。这是需要核对的配置条件,不是已证实的现场根因。
   
   ### 3. resolved 绕过 groupWait / groupInterval
   
   `reduce/AlarmGroupReduce.java:262–277`:将收到的事件 put 到分组成员 map 后,直接执行 
shouldSendGroupImmediately。
   
   `343–347`:该方法只检查当前成员是否全部 resolved,不检查 lastSendTime、groupWait 或 groupInterval。
   
   所以只有一个已恢复成员的组,每次收到恢复都可以立即发送,不经过定时检查中的 30 秒等待或 300 秒间隔。
   
   ### 4. repeatInterval 不限制纯恢复组
   
   `reduce/AlarmGroupReduce.java:285–306`:只有组状态是 firing 才检查 
repeatInterval;而且包含恢复成员时也绕过 firing 重复节流。纯 resolved 组不会受用户设置的 1800 秒限制。
   
   ### 5. 发送后清除恢复成员,没有已完成记录
   
   `reduce/AlarmGroupReduce.java:317–325`:调用 inhibitAlarm 后,从 map 中 
removeIf(resolved)。下一条相同恢复到来时,existingAlert 为 null,重新放入,再立即发送。
   
   注意 inhibitAlarm 返回并不表示机器人已成功收到消息。下游通知采用异步任务,且抑制层会捕获异常。因此不能在这里简单记录“通知成功”。
   
   ### 6. 数据库存储也没有挡住重复
   
   `notice/impl/DbAlertStoreHandlerImpl.java:65–94`:按 fingerprint 查数据库。对于 
resolved,即使已有记录也是 resolved,仍复制原记录 startAt、activeAt、triggerTimes,再执行 
save,加入返回列表。没有判断“相同周期的恢复已经处理过”。这解释了截图中触发时间、次数等字段保持不变。
   
   `notice/AlertNoticeDispatch.java:117–129`:store 返回后无条件 sendNotify,并执行插件、SSE 
推送。持久化记录复用同一个 ID 不等于通知去重。
   
   ## 修复建议
   
   ### 推荐:在持久化/通知边界识别恢复状态转换
   
   优先修改 `DbAlertStoreHandlerImpl`、`AlertStoreHandler` 的返回约定及 
`AlertNoticeDispatch`,将“需要保存的状态”和“需要新建的通知事件”区分开。
   
   1. 在覆盖 startAt 等字段之前,保留并规范化上游事件身份。对 Prometheus,可使用来源、规范化标签身份、原始 startsAt 和恢复 
endsAt 识别同一恢复。不要直接用未经校验的显示字段或整个消息正文作为键。
   2. 在同一原子处理范围内读取已有状态,判断首次恢复、重复恢复、新周期及过期事件。已有 resolved 且属于相同恢复事件,不能再次新建通知事件。
   3. 不要把“数据库状态为 resolved”一概当作重复:不同周期的恢复仍需要通知。旧周期的延迟恢复也不能覆盖新周期 firing。
   4. 返回显式的处理结果,例如“存储后的组”和“新增通知事件/需通知成员”。这些是拟议接口,不是现有 API。
   5. dispatchAlarm 仅针对需要通知的部分调用 
sendNotify;混合组只排除重复恢复成员,不能因为一个重复成员而丢掉整组。过滤后应重建通知视图的公共标签、注解、指纹和状态;不要直接改写持久化组的完整状态。
   6. 插件通知路径也要遵守同一事件判断;SSE 可以按状态同步需求单独决定,不能只修机器人路径而遗漏插件。
   
   ### 最小止重复补丁的限制
   
   只在存储层增加“已 resolved 且同一周期 → 
跳过”,并让分发层跳过空通知结果,可以减少当前重复,但存在首次通知失败后不再重试的问题。当前流程先保存、后异步发送,保存成功不等于通知成功。
   
   不能仅 `return null` 而不修改调用方:现有 dispatchAlarm 会继续 sendNotify、插件和广播。也不能仅在单条循环中 
continue 后,仍把空组发送出去。
   
   ### 保留失败重试的实现
   
   对需要可靠通知的环境,建议在状态落库时同时持久化一次通知任务,以恢复事件身份和通知目的地约束重复任务;重复输入复用既有任务。任务记录 
pending/成功/待重试等状态,渠道发送失败重试既有任务,不因重复输入创建新任务。
   
   多实例环境必须使用共享原子状态或数据库约束;当前 KEY_LOCKS 只提供单 JVM 
内的锁。远端渠道不支持幂等键时,“远端已接收但本地未记录成功”的崩溃窗口仍可能导致重复,不能承诺端到端严格 exactly-once。
   
   ### 不建议的修复
   
   - 不把 300/1800 秒无限调大:没有覆盖当前恢复立即发送分支。
   - 不让所有恢复事件服从 firing 的 30 分钟节流:可能吞掉真正的首次恢复。
   - 不仅保留 resolved 成员而不改其他代码:定时扫描仍可能再次发送这些成员。
   - 不只在 processAlertByGroupDefine 内去重:未匹配策略的 sendSingleAlert 路径会绕过。
   - 不仅用 fingerprint 永久过滤:会挡住后续故障周期的恢复。
   - 不以“接入时写内存缓存”为已成功通知:异常、静默、重启和多实例都可能使语义不成立。
   
   ## 必须覆盖的验证
   
   | 输入/场景 | 预期 |
   |---|---|
   | firing 后首次 resolved | 产生一次恢复通知任务 |
   | 相同 resolved 重放 5 次 | 不新增恢复通知任务 |
   | 30/300/1800 的匹配分组 | 同一恢复只创建一个通知事件 |
   | 无匹配分组、缺少 service | 同样能阻止重复恢复任务 |
   | 同一 fingerprint 新故障周期再恢复 | 新周期正常通知 |
   | 一成员 firing、另一成员首次恢复 | 保留首次恢复,不把整组错误标为恢复 |
   | 混合组含重复恢复和新的事件 | 仅过滤重复成员,新的事件正常发送 |
   | 并发重复输入 | 单次新建事件/任务 |
   | 旧周期恢复在新 firing 后延迟到达 | 不覆盖新周期状态 |
   | 首次通知失败,随后重复恢复输入 | 重试既有任务,不静默丢失、不新增重复任务 |
   | 多实例/重启后重放 | 持久化去重仍生效 |
   
   现有 AlarmGroupReduceTest 覆盖部分恢复与组状态、重复间隔内恢复,但没有对应的重复 resolved 重放用例。本次未执行或新增测试。
   
   ## 现场低成本确认
   
   `controller/AlertReportController.java:98` 已有 INFO 日志:
   
   ```text
   Receive prometheus server alert, content: ...
   ```
   
   在现有日志中检索这个字符串及目标实例,对照 15:16:47、15:18:47、15:20:47、15:22:47、15:24:47 的原始 
labels/startsAt/endsAt,即可确认每条渠道消息是否对应一次上游恢复重发。无需先修改日志代码。若 INFO 
关闭或日志已轮转,缺少记录不能证明未接收。
   
   ## 官方源码链接
   
   - [Prometheus 
接入](https://github.com/apache/hertzbeat/blob/a85bd46c18d33130298f0e223bd84ea6e29a17de/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/PrometheusExternAlertService.java#L76-L91)
   - 
[分组与恢复发送](https://github.com/apache/hertzbeat/blob/a85bd46c18d33130298f0e223bd84ea6e29a17de/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmGroupReduce.java#L244-L347)
   - 
[持久化处理](https://github.com/apache/hertzbeat/blob/a85bd46c18d33130298f0e223bd84ea6e29a17de/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/impl/DbAlertStoreHandlerImpl.java#L65-L94)
   - 
[通知分发](https://github.com/apache/hertzbeat/blob/a85bd46c18d33130298f0e223bd84ea6e29a17de/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/notice/AlertNoticeDispatch.java#L117-L153)
   - 
[入站日志](https://github.com/apache/hertzbeat/blob/a85bd46c18d33130298f0e223bd84ea6e29a17de/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java#L95-L109)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to