告警只触发一次,而不是每台服务器各触发一次
我们的一条运维告警——针对已发布应用的“积分不足”——本应每天最多送达一次。但有一天下午,它却来了五次。原因是:API 的每个副本都在自己的内存中记录已经发送过哪些告警,因此两台服务器各发了一次;而每次部署都会清空这份记忆,让它们再次发送。同样的模式还导致一个后台任务重复执行。现在,两者都通过共享存储进行协调;并且,如果该存储不可用,两者都会优先选择安全的一侧失败。
症状
当某件事需要我们关注时,Mythex 会向团队发送告警:服务宕机、任务停滞,或者某个构建者已发布应用的积分余额即将耗尽。每条告警都有一个去重窗口,即同一条告警在该时间段内只会发送一次。对于“积分不足”告警,这个窗口是 24 小时。
同一条告警在一个下午里出现五次,说明去重窗口没有生效。更糟的是,这些告警成对出现,每对之间相隔约 11 分钟。
原因
我们的告警包在内存中保存去重信息:一张“告警键 → 上次发送时间”的表,存在于正在运行的进程里。这在只有一个进程、且该进程从不重启的情况下是可行的。但生产环境两者都不满足:
- 两个 API 副本。 生产环境运行着两份 API。每份都有自己的表,因此各自发送了一次告警。这就是成对出现的原因。
- 部署。 每次部署都会用全新的进程和空表替换这两个副本。部署之后,告警就会再次发出,仿佛从未发送过一样。
每对告警之间约 11 分钟的间隔,来自触发该告警的任务。这个任务在两个副本上都会运行,并且会获取一把锁,因此两个副本轮流抢到锁,轮到哪个副本,哪个副本就触发一次告警。
同一段代码里还有一个更隐蔽的问题。为了防止表无限增长,它在表中有 500 个键时就会自我清理,删除所有超过 15 分钟的条目,而不管这些条目的去重窗口有多长。在高负载下,原本 24 小时的去重,可能只持续了四分之一小时。
修复方案
每条告警共享一条记录
现在,API 在发送告警之前,会先在 Redis 中认领该告警的去重键。Redis 是一个共享的内存存储,两个副本都会与它通信,并且不会随着 API 的重启而丢失数据。认领操作是一条原子指令:“仅当该键不存在时才设置,并在窗口结束后过期”。哪个副本先认领成功,哪个副本就发送告警;另一个副本发现该键已被占用,便保持沉默。部署之后的新进程也能找到这个键,因为它仍然存在。
键和窗口的计算规则,现在与告警包共用同一份代码,因此两层对于“什么才算同一条告警”不会产生分歧。
失败时选择放行,而不是拦截
最重要的决定是:当 Redis 无法连接时该怎么办。我们选择照常发送告警。如果 Redis 宕机,或者在 1.5 秒内没有响应,告警仍会发出。重复的告警只是噪音,而被丢弃的告警则意味着有人没有收到通知。
最严重的 P0 告警,则完全不会因为去重而被拦下。
每个条目保留自己的窗口
在告警包自身的内存去重中,每个条目现在都会记录其窗口结束的时间,清理时只删除窗口已经结束的条目。一个 24 小时的键会保留满 24 小时,无论有多少其他告警涌入。
同样的问题,出现在别处
同样的模式也让一项后台工作重复执行了:一个对账任务,负责让我们对各工作区状态的记录与沙盒提供商保持一致。每次运行都会列出账户下的每个沙盒——生产环境中有数千个,大约耗时五秒——以及每个正在运行或已暂停的项目。两个副本每两分钟都会运行一次。
它的写入是幂等的,也就是说运行一次和运行两次结果相同,所以没有造成任何故障,只是白白多做了一倍的工作。现在,每个两分钟的时间窗口都会像我们的用量计量那样,在 Redis 中被认领,只有认领成功的副本才会执行这次对账。如果无法连接 Redis,对账仍然会执行:重复做同一件事是无害的,而不做则不行。
测试方法
测试模拟了真实的部署环境。它们把告警模块加载两次,模拟两个独立的“副本”,并让两者都指向同一个共享存储,然后各自触发相同的告警;接着再加载第三个全新的副本,模拟“部署之后”的情况,并再次触发该告警。最终只发送一条告警。其他测试则检查:不同构建者的告警依然会发出,P0 告警会从两个副本各自发送,以及当存储宕机或超过超时时间无响应时,告警仍会被发送。清理修复也有对应的测试:一个 24 小时的键,在 20 分钟后又涌入 600 条其他告警时,依然保持有效。
对于对账任务,两个副本同时执行,再加上同一窗口内的第三次触发,最终只产生一次列表查询。
我们的收获
- 内存中的状态是按进程划分的。 任何需要在多台服务器之间保持一致的事情——“只发送一次”“只运行一次”——都必须存放在它们共享的地方。
- 部署就是重启。 只能存活到下一次重启为止的状态,撑不过一个正常的工作日。
- 明确故障时的处理方向。 对于告警来说,共享存储宕机应当意味着多发告警,而不是少发告警。
- 寻找模式,而不仅仅是单个实例。 告警是那个最显眼的案例;而重复执行的对账任务,是同一个问题,只是在悄悄地浪费资源。
这些内容在 Mythex 内部通常看不到。想了解我们如何保持平台稳定,请参阅我们如何把部署期间的 502 错误降到零,以及缺失的 swap 文件。如果你刚接触运行多个服务副本的概念,我们的什么是服务器指南会介绍基础知识。