ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Python自研OnCall值班系统:告警、排班与升级策略实战

2026/10/6 17:05:04 拓冰建站 浏览量
用Python自研OnCall值班系统:告警、排班与升级策略实战 做运维、做 SRE 的同学应该都有过这样的体验凌晨三点手机突然狂响挣扎着爬起来看一眼告警结果是个误报更惨的是真的出故障了你翻遍通讯录都不知道今天该找谁。OnCall 值班这件事听起来简单真正落地却非常考验工程能力。我自己被商业值班平台的报价劝退之后干脆决定用 Python 从零实现一套内部用的 OnCall 系统把告警接收、排班轮转、通知触达、升级策略全都理了一遍。这个项目前后花了大概两周的下班时间虽然做不到 PagerDuty 那么完善但核心链路已经能稳定跑在真实监控环境里。这篇文章就把我从需求拆解到代码落地的完整思路写出来希望对正准备动手做值班系统、或者只是好奇 OnCall 背后机制的同学有帮助。1. 先想清楚一套 OnCall 系统到底要解决什么问题很多同学拿到需求第一反应是这不就是个发通知的程序吗真做起来才发现不是那么回事。OnCall 系统的本质是一个事件驱动的事故响应协调器它的核心职责不是发消息而是把正确的人在正确的时机用正确的方式叫起来处理问题。1.1 值班场景里的三个核心痛点第一个痛点是找得到人。告警产生之后系统必须知道当前这一时刻该联系谁。听起来简单但涉及排班表、节假日、临时替班、多团队轮转这些规则叠加纯靠人肉查表根本维护不动。第二个痛点是控得住量。生产环境一天几千条告警很常见如果每条都发通知值班人的手机几分钟就会被刷爆真正重要的事故反而被淹没了。所以系统必须做去重、聚合、抑制和分级。第三个痛点是逼得动响应。告警发出去了值班人看到了但不处理怎么办没人看怎么办这时候需要升级机制15 分钟没确认通知二级值班人30 分钟还没人响应直接电话呼叫负责人。这些策略是商业 OnCall 平台的核心竞争力也是自研时最容易做崩的地方。1.2 为什么自研而不是直接买现成平台说实话如果预算充足直接采购成熟平台肯定是最优解。但很多团队面临的现实是内部监控体系已经非常定制化告警格式千奇百怪既有 Prometheus 的 webhook也有自研脚本输出的文本告警还有云厂商的云监控推送。商业平台接入这些五花八门的来源适配成本并不低。自研 OnCall 还有一个隐形收益它能倒逼团队把告警治理的流程理清楚。我在设计告警接入层的时候就得把各个监控源的告警字段统一成标准化模型这个动作本身就是一次很有价值的告警规范化治理。Python 在这种场景下特别合适生态里有 FastAPI、SQLAlchemy、APScheduler 这些成熟组件写起来快后续接 openpyxl 做报表、接 pandas 做值班统计也都方便。2. 整体架构与技术选型怎么用 Python 把系统拆明白2.1 按事件流拆解系统边界我不建议一上来就画系统架构图而是先画一条事件流告警从哪里来经过什么处理最后到哪里去。沿着这条线拆出来的模块每个都有明确的职责。事件流大概是这样的监控系统产生告警通过 HTTP webhook 或者消息队列推给 OnCall 的接入层接入层完成格式标准化和指纹计算把告警落库然后调度引擎根据配置的规则做去重、合并和级别评定接着路由模块拿到排班表找出当前值班人通知模块按策略把消息推到邮件、钉钉/企微群、短信等渠道系统启动一个延迟任务等值班人确认超时未确认就触发升级策略找下一级的人。按这条链路系统至少需要五个部分接入层、事件处理引擎、排班模块、通知模块、升级调度模块。数据层面还需要一个能存事件状态和排班关系的关系型数据库以及一个兜底用的任务队列。2.2 框架选型FastAPI SQLAlchemy APScheduler既然用 Python语言框架这块我选择了 FastAPI而不是 Django 或 Flask。原因很简单OnCall 系统的核心交互是大量的 HTTP webhook 写入和回调接口FastAPI 基于 ASGI天然支持异步 IO接收高并发的告警推送表现很好。它自带的 Pydantic 校验和 OpenAPI 文档对告警格式的规范化非常有帮助——每个监控源接入的时候我可以直接用一个 Pydantic model 定义它的合法格式校验失败立刻在文档里看到问题。数据访问层用 SQLAlchemy 2.0 的 ORM生产库选 PostgreSQL本地开发用 SQLite。排班轮转和事件查询这种关系型数据用 ORM 管理最省心后面做统计报表直接写 SQL 也顺。调度这一块我先用 APScheduler 扛着。它的 cron 触发和 date 触发足够覆盖排班计算、延迟升级这些场景。这里插一句APScheduler 的进程内调度在单实例部署下没问题等系统要横向扩容、变成多实例部署的时候就要换 Celery 或者外部任务队列这个后面我会专门讲。2.3 数据模型设计从信息流反推表结构数据模型不要一开始就设计一堆而是沿着前面的事件流反推。我最终落地的核心表大概是这几张team和member值班组信息以及组内的成员每个成员有邮件、手机号、IM 账号等联系方式。schedule和rotation排班规则和轮转配置比如第一组按周轮转每周一个值班人。incident事件主表每一条告警经过处理后在这里存一条记录带着状态。escalation_policy升级策略表定义不同告警级别的升级路径和时间阈值。notification_log通知日志表谁在什么时间收到了什么内容结果如何。为什么要单独拆escalation_policy而不是把升级规则写死在代码里因为不同业务系统的告警容忍度完全不一样核心支付链路的告警可能要求 5 分钟就必须确认内部工具平台的告警可能 30 分钟都无所谓。把这些规则做成配置而不是写进代码系统才有灵活性。我在尝到甜头之后又加了suppression_rule表专门做告警抑制规则配置效果非常明显。3. 核心模块实现从告警接入到通知触达3.1 告警接入层统一 Webhook 与指纹去重接入层的职责是把五花八门的告警来源变成一个标准事件。我为每个监控源写了一个 adapterPrometheus 的 webhook 转成内部模型自研脚本推送的 JSON 也转成内部模型。这一步做完后面的处理逻辑就不用关心告警是从哪来的了。标准化之后最关键的动作是计算指纹。指纹是告警去重的基础我用告警源、告警名称、目标主机、消息摘要这几个字段做一次哈希。需要特别注意指纹字段不能包含时间戳和随机值否则每条告警的指纹都不一样去重就形同虚设。我踩过的一个坑是把消息全文放进了指纹计算结果同一条告警因为 message 里带了每次不同的节点 IP直接生成了一堆新事件。后来改成提取源 名称 主机 故障类别这四个字段去重效果立刻正常了。接入层的幂等处理我也做了一层保障。每个监控源推送的 webhook 都允许重试OnCall 系统接收方用事件 ID 做主键重复插入时直接忽略并返回 200。这样上游监控系统重发告警时不会产生重复事件。3.2 排班轮转用一轮循环表解决今天该找谁排班模块看着不起眼做起来最容易出 bug。我最开始拿 Python 的日期函数直接算第 N 周该谁值班结果跨年的时候因为周数计算方式的问题排班表乱了一次。后来我把逻辑统一成基于循环列表 周期数取模的算法。设定一个轮转的起始日期每个轮转周期内按成员列表顺序值班当前值班人的下标等于从起始日期到今天的周期间隔数对成员人数取模。用周轮转举例就是from datetime import date def current_oncall(members, start_date, today, cycle_days7): elapsed_days (today - start_date).days cycle_index elapsed_days // cycle_days return members[cycle_index % len(members)]代码本身不复杂但有两处细节必须处理。一是所有时间计算统一用 UTC展示层再转本地时区否则协作者分布在多个时区时切班时间会出现偏差二是排班切换瞬间的并发问题建议在告警路由查询值班人的时候对排班结果做短时间的缓存不然告警高峰时每一条告警都触发一次查排班 算轮转的完整链路数据库会被打得很疼。3.3 通知触达Notifier 抽象与多渠道适配通知模块是整个系统里最直接接触用户的部分做得好不好直接影响值班人的体感。我设计了一个BaseNotifier抽象类核心就一个send方法不同渠道各自实现。class BaseNotifier: def send(self, target: str, subject: str, body: str) - NotificationResult: raise NotImplementedError class DingTalkNotifier(BaseNotifier): def send(self, target, subject, body): # 调用钉钉/企微机器人的 webhook ... class EmailNotifier(BaseNotifier): def send(self, target, subject, body): # 走 SMTP 发邮件 ... class SmsNotifier(BaseNotifier): def send(self, target, subject, body): # 调用短信网关 API ...这个抽象的价值在于后续增加新的通知渠道时只需要新写一个类不需要改动业务编排逻辑。通知发送的时候我会附加一个简单的重试机制和渠道级熔断。短信这类渠道对频率非常敏感我在发送队列里做了限流同类渠道每秒最多发多少条防止告警风暴时把短信通道打挂。实测下来这个限流动作非常关键有几次告警风暴就是靠它保住了短信通道的可用性。3.4 升级策略状态机 延迟任务升级策略模块是我花时间最多的地方。每条事件进入系统后有一个状态我用的状态集比较精简OPEN、ACKNOWLEDGED、SUPPRESSED、RESOLVED。升级动作只在OPEN状态下触发值班人确认后事件变成ACKNOWLEDGED升级任务自动取消。实现上APScheduler 提供的date触发器非常契合这个场景。事件创建时我根据升级策略算出下一次升级的时间点注册一个一次性任务。任务执行时会检查事件当前状态如果是OPEN且停留在当前级别已超过阈值就升级给下一级联系人同时再注册一个新的升级任务。这里我加了一道兜底逻辑除了依赖调度任务还会周期性扫描事件表找出那些状态还是 OPEN、但按策略早就该升级却没有任何升级日志的事件补触发一次升级。这个兜底帮我避免了多次因进程重启导致任务丢失的事故。4. 完整事件链路从告警触发到值班人确认4.1 一次告警的完整时序单独看每个模块容易一头雾水我把一条真实告警从进入到被确认的完整时序串起来讲一遍。假设 Prometheus 里的一个告警规则触发了webhook 推送到 OnCall 系统的/v1/alerts接口。接入层先完成格式标准化计算指纹然后将事件落库状态为OPEN。事件引擎检查指纹发现过去 30 分钟内已有相同指纹的事件于是判定为重复告警只更新原事件最后触发时间不创建新事件也不发通知。如果指纹不存在则进入路由环节查询当前排班拿到今天值班人的联系方式同时根据告警级别加载对应的升级策略。通知模块按策略优先级把消息推到 IM 群和邮件然后注册一个 15 分钟后的升级检查任务。值班人被消息吵醒点开系统里的确认按钮系统调用/v1/incidents/{id}/ack接口把这个事件置为ACKNOWLEDGED同时取消已注册的升级任务并记一条通知日志。到这里一次完整的值班响应闭环就完成了。4.2 状态管理与幂等保障这个链路里最容易出 bug 的是多个操作同时改事件状态。比如值班人点了确认同时升级任务正好触发两边同时更新同一条记录如果处理不当就可能出现确认失败但升级照发的情况。我的解决方案是使用条件更新UPDATE incident SET status ACKNOWLEDGED, acked_by :user WHERE id :id AND status OPEN在 SQLAlchemy 里就是update().where(Incident.status OPEN)update 语句返回的影响行数如果为 1说明确认成功如果是 0说明事件已经不是 OPEN 状态确认操作直接忽略。这个思路同样用在升级动作上保证升级和确认两个动作只有一个能成功。数据一致性是一个自研 OnCall 系统最基本的底线这里省事后面事故处理时会付出更大的代价。5. 实操中的常见问题与排查实录5.1 通知发不出去先看这几个地方通知发送失败是出现频率最高的问题是。HTTP 回调类的通知比如钉钉/企微机器人最常见的失败原因是目标 webhook 被平台限流。这类渠道通常有每分钟消息条数上限告警风暴时一旦触限平台直接拒绝请求。我在通知模块里加了渠道级计数器触发阈值后自动排队而不是立刻发送实测下来能明显降低被限流的概率。邮件通知最容易遇到的问题是进垃圾箱。公司内部 SMTP 服务器发出的邮件因为域名解析、反垃圾策略等原因经常会被归到垃圾邮件里。排查思路是先查 SMTP 日志确认发送成功再看收件人的垃圾箱。这个问题没有特别优雅的解决办法我的建议是优先用 IM 群机器人做主要通知渠道邮件做备份通知渠道。短信网关的问题则集中在签名审核上代码层面能做的只有确保请求参数完整、异常时重试并记录日志。5.2 排班切班与时间计算的那些坑排班模块我踩过的最大的坑是我前面提到的跨年计算。用 Python 的isocalendar()算周数时某些年份会有 53 周直接用自然周数取模会导致排班整体错位。后来我把所有轮转计算都改成基于起始日期的天数差除以周期天数彻底绕开了周数语义的问题。还有一个时区相关的坑如果值班人的日历上显示周一 00:00 切班而系统内部用 UTC 存储那本地时区为 UTC8 的团队实际切班时间是周一 08:00跟预期差了半天。我的建议是排班规则的周期边界统一用系统配置的默认时区来定义所有计算之前先把时间转换到那个时区再算天数差。这个规则要写在项目的配置文档里后面接手的人才知道为什么这么设计。5.3 告警风暴下如何保护通知渠道告警风暴是最考验 OnCall 系统健壮性的场景。我记得有一次某个服务发布异常监控直接产生了几千条告警如果没有做防护值班人的手机消息量会非常恐怖。我在系统里做了三层防护第一层是接入层的指纹去重和窗口内聚合同一类告警在 30 分钟内只发一条通知第二层是抑制规则比如某个服务已经标记为发布中对应的告警自动进入抑制状态不触发通知第三层是通知通道限流超过阈值就排队避免打爆渠道。这三层防护的效果很大实际告警风暴降下来的通知量大概只有原始告警的百分之一。等到事件真正被确认为事故时值班人看到的是已经自动聚合、包含影响范围的完整信息而不是几百条零散的消息刷屏。5.4 调度任务丢了的兜底方案APScheduler 的进程内任务存储在单实例场景下很可靠但进程一重启任务就没了。我先手动创建的标记为 OPEN 但一直没人确认的扫描任务就经常出现这种情况。后来我加了一个兜底逻辑每五分钟扫描一次事件表找出那些按升级策略早就该升级、但缺少对应升级日志的事件自动补触发升级。这个兜底逻辑虽然笨但在自研系统里非常实用它用数据库当了任务队列的持久化层保证了升级动作不会因为进程重启而遗漏。6. 最后分享几点真实体会这个项目从动手到现在我个人最大的收获不是代码量而是对可靠性有了更具体的理解。OnCall 系统本身是一个几乎不发通知的后台系统但它管的是真正出事故时的紧急通信链路所以它对自身的可靠性要求反而比很多业务系统要高。设计的时候要把这个组件挂了会发生什么作为每一个模块的默认问题。另外有条件的话一定要给 OnCall 系统做监控和告警我自己就给它加了健康检查接口和关键指标上报有一次接入层线程池耗尽就是靠健康检查提前发现的。这套系统到现在还在稳定跑着虽然简陋但已经是团队夜里值班最放心的那根安全绳。如果你也在考虑自研 OnCall我的建议是从最小闭环开始先跑通接入到通知再加排班和升级。先把链路走完再谈完善。