监控告警里有个容易被忽略的现实:监控数据采集得再漂亮、大盘画得再炫,最后一步“把问题告诉该知道的人”如果做不好,整套监控体系的最终价值就打了折扣。我在团队里维护过一段时间基于 Prometheus 和 Grafana 的监控体系,node-exporter 负责采机器指标,Grafana 出图,告警规则也配了不少,但大家反馈最多的却是“告警没收到”“收到的时候已经过去了半小时”。后来我花了一个周末,用 C# 写了约 800 行代码的独立邮件告警服务,专门解决监控告警触达的最后一公里问题。这篇文章把整个实现过程中的设计思路、关键代码、部署方式和踩过的坑完整复盘一遍。
这个系统不是什么大而全的监控平台,它解决的核心问题非常聚焦:把分散在各处的监控事件统一收拢,经过规则判定后,以邮件形式把告警准确、及时、不轰炸地送到负责人手里。适合同样被困在“告警触达”环节的运维和开发同学参考,也适合想了解 C# 后台服务、定时调度、邮件发送、简单规则引擎怎么落地的人。
1. 一个独立邮件告警服务要解决什么:不只是发信这么简单
1.1 监控链路里最容易被忽视的“触达层”
大多数监控系统的数据流长这样:被监控对象暴露指标,采集器定时抓取,时序数据库存储,告警规则引擎根据阈值条件算出是否触发,然后交给通知渠道。这里面 Prometheus + Alertmanager 已经能覆盖绝大多数场景,Alertmanager 内置了邮件、Webhook、Slack 等通知方式,理论上也能直接发邮件。但在实际落地中,我遇到几个很现实的问题:
- 内网环境为了安全,出站邮件往往需要走专用的 SMTP 中继,而这些中继对发件域名、发信频率、邮件格式有自己的要求,Alertmanager 那一层要做得够灵活,配置会越堆越复杂。
- 告警链路往往不止一套。Kubernetes 集群里有事件告警,业务系统有自定义的健康检查,数据库有单独的监控脚本,这些东西各自有各自的告警输出方式,全部统一到 Alertmanager 里需要改造每个采集端。
- 收件人、值班表、告警级别、静默时间段这些运营层面的需求,变化极其频繁。每次都改 Alertmanager 配置再 Reload,能扛住,但体验并不好,而且容易出现改错导致告警失联。
所以我的想法是:写一个轻量级的独立服务,它不负责采集指标,也不负责存储时序数据,它只做一件事——接收各类监控事件输入,按照本地规则判断算不算告警,该不该发邮件,发给谁,用什么样的文案发出。相当于给监控体系加了一个“告警触达层”。
1.2 为什么用 C# 写而不是继续堆 YAML
选型时有人问过我:为什么不直接在 Alertmanager 里配 route 规则?为什么不用 Python 写个脚本扔在 crontab 里?
我当时的判断是这样:
- Alertmanager 的 route 和 receiver 配置确实强大,但它本质上是为 Prometheus 生态设计的,接入非 Prometheus 事件时需要额外包一层 Webhook 接收器,调试起来比较割裂。而且规则复杂到一定程度,YAML 会变得难以维护和测试。
- Python 脚本放 crontab 是最快能跑起来的方案,但定时精度、并发控制、异常恢复、优雅退出这些都需要自己处理。C# 基于 .NET 的后台服务模型(BackgroundService)天然支持优雅启停、依赖注入、结构化日志,做长驻进程比脚本稳定得多。
- 更重要的一点:团队里 C# 的技术栈积累更厚,后续如果有人要加功能,比如加个企业微信通知、加个 Webhook 转发、接个内部值班系统,用 C# 都顺理成章。技术选型不能只看当前能不能跑,还要看三年后谁在维护。
800 行这个规模不是刻意为之,而是这个需求本身的复杂度决定。再少代码就会开始缺边界情况处理,再多代码就说明你可能在重复造轮子。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统设计:800 行代码如何拆出清晰的边界
2.1 模块划分:两个后台服务加三个核心组件
整个项目是标准的 .NET 6+ Worker Service 模板,我把它拆成两个 BackgroundService 和一个核心处理管道。
第一个后台服务是 事件接收监听器。它监听两类输入:一个轻量级 HTTP 接口,用来接收来自 Jenkins 脚本、自定义监控脚本、K8s Webhook 等外部系统推过来的事件;另一个是定时触发器,每隔 N 秒轮询一组配置好的 HTTP 数据源(比如某个内部服务暴露的健康检查 JSON),把数据源的状态变化转换为事件。
第二个后台服务是 调度扫描器。它按固定周期扫描当前处于活跃状态的告警,检查是否满足恢复条件、是否需要升级、是否已经超时未确认。这个服务是告警状态机的驱动引擎。
三个核心组件分别是:规则引擎(把原始事件映射为告警)、告警状态仓库(在内存中维护当前告警的状态与生命周期)、邮件通知器(负责渲染模板、投递、重试)。
整体数据流是这样的:
code复制外部事件源 内部服务
| |
| HTTP POST
