项目名叫 rea,最早源自我的一次临时救火:某天凌晨,三台服务器的磁盘使用率轮流触顶,我一个人在终端里反复切换、逐台排查,折腾了大半夜才定位到真正的问题。那种"明明每台机器都能登录,却看不清整体状态"的憋屈,让我决定写一个能把资源指标和事件信息汇总到一起的小工具。它就是 Rea,一个轻量级的资源事件聚合器。
也许你已经拥有一套完整的监控体系,或者团队规模大到有专职的运维平台,这篇文章对你的参考价值有限;但如果你像我一样,面对的是十几个实例、几十个服务,想要用很少的维护成本把"机器状态、服务状态、业务事件"放进同一个视图,Rea 的设计思路和踩坑记录应该能帮你省不少时间。
1. Rea 的起点:为什么不能只靠现成方案凑一个出来
1.1 当时的真实场景:数据分散到让人崩溃
接手那批节点时,每台机器上都有一套独立的管理脚本,日志散落在各自的目录,系统指标则被一个简单的循环任务写进了文本文件。表面上一切都"能看到",实际上要在几分钟内回答"现在到底有没有问题、问题在哪一台"这件事,必须挨个登录执行命令,再把屏幕上的输出自己拼起来。
我印象最深的是一次容量评估:需要统计一周内各服务的最大内存占用。按理说半小时能搞定,结果因为数据格式不统一,有的输出是 KB,有的是 MB,有的干脆是百分比,我硬是花了一下午清洗数据。从那时起我就意识到,缺的不是采集手段,而是一个统一的事件入口——让所有机器、服务和业务状态都变成同一类消息,后面无论做聚合、告警还是排查,都只面对一个模型。
1.2 评估现成方案时的三个否决点
我也认真考虑过部署一套完整的监控平台,毕竟现成方案听起来更省事。但仔细列了需求之后,发现三个问题绕不过去。
第一,部署链路太长。一套完整的监控平台通常要拆成采集端、存储端、告警端、展示端,再加上配套的配置管理,对我们这种两人兼职维护的小团队来说,光是版本升级和存储调优就可能成为新的负担。
第二,数据模型偏指标化。现成方案擅长处理数字指标,比如 CPU、内存、QPS,但我们的场景里还有大量"事件"型数据,比如某个服务重启、某个任务重试、某次发布完成。把这些东西硬塞进指标模型里,要么丢失上下文,要么到处打标签,后续查询反而更麻烦。
第三,告警规则和内部流程对接困难。我们希望告警不止发到一个群,还要附带故障时间线、相关节点列表、上次变更记录。现成方案的告警模板可以做,但每改一次都要研究它的 DSL,成本比我自己写一个简单的触发逻辑要高。
所以最终结论是:与其去迁就一个重型系统,不如做一个贴合自己使用习惯的小工具。Rea 的定位从一开始就很明确——不做大而全的监控平台,只做"把分散的事件汇总成可操作的信息"这一件事。
2. Rea 的核心设计:一条事件消息走到底
2.1 事件模型:统一的三层标签结构
Rea 最早用的就是"指标名+数值+时间戳"这种最小模型,但很快发现不够用。比如上报一条"磁盘使用率 87%",如果不知道是哪台机器、哪个服务、归属于哪条业务链路,这条事件基本没有意义。后来我把模型固定成了带三层标签的结构:
{ "host": "app-node-01", "service": "api_server", "biz": "order", "kind": "metric", "name": "disk_used_percent", "value": 87, "ts": 1710000000, "seq": 1024 }host表示物理或虚拟节点,service表示进程或服务名,biz表示它所属的业务分组。kind字段区分metric、event、alert三个类型,seq是本机单调递增的序号,用来做去重和排序。
你可能觉得这不就是加几个标签吗?差别其实在思维方式上。之前是"为每种数据写一段处理逻辑",现在变成了"先决定它是一条什么事件,然后所有下游都围绕事件处理"。采集端只要按照这个格式上报,聚合端就不需要关心数据来自哪个脚本;告警端也只需要按标签过滤,不用为每种指标单独开发规则。
2.2 为什么坚持"先落地缓冲、再异步聚合"
很多人在做类似工具时,第一步想到的是引入消息队列,让采集端把数据发到队列,再由消费者处理。逻辑上没毛病,但对我们这种规模来说,多维护一个消息队列意味着多一个故障点:队列挂了,整个采集链路就断了,还得处理队列本身的磁盘和吞吐问题。
Rea 选择了更笨但更稳的做法:采集端先把事件写在本机的结构化文件里,聚合端定期来拉取,拉取成功后删除或标记偏移量。这样相当于用本地磁盘做了一次缓冲,网络抖动、聚合端重启都不会丢数据。聚合端只要能访问所有节点的文件接口,就能以一个全局进度把事件汇总起来。
这个设计带来两个额外好处:
- 天然支持重放。如果某天发现某个时段的聚合结果有问题,我只需要关掉"删除已拉取事件"的逻辑,再把所有历史事件重新推演一遍。
- 背压简单。聚合端处理不过来时,它自己会变慢,采集端不会被打爆,也不会因为发送超时报错。相比 TCP 直推模式,这种"拉取"模式天然具备流量控制能力。
代价是实时性会差一点,从事件产生到聚合端可见,通常有一到两秒的延迟。对于资源监控和故障告警这个场景,两秒完全可接受。
3. 关键实现细节:协程、幂等和配置分离
3.1 采集端:为什么是协程而不是线程
Rea 的采集端运行在每个节点上,负责读取本机的系统状态、服务健康信息,以及接收业务侧主动上报的事件。刚开始我图省事,用多线程模型,每类采集任务开一个线程。节点少的时候挺正常,节点多了之后问题就来了:线程占的内存不小,而且大量时间花在等系统调用返回上,上下文切换开销完全浪费在 IO 等待上。
后来我改成了 Python 标准库自带的async/await协程模型,采集任务全部在一个事件循环里调度:
async def collect_once(): tasks = [ read_cpu_usage(), read_disk_usage(), read_service_health(), ] results = await asyncio.gather(*tasks, return_exceptions=True) await append_to_spool(results)这么做的直接收益是单节点的采集端内存从 80MB 降到了不到 20MB;更重要的收益是,IO 操作变成了"挂起,等通知,再继续",当某个采集目标响应慢的时候,其他采集任务完全不会被拖累。
如果你也在考虑类似方案,我的建议是:面对大量短时 IO 操作,协程的并发效果优于线程;但如果你的采集任务里存在明显的 CPU 密集型计算,比如复杂的日志解析,那需要额外考虑把这类任务丢到线程池里,避免阻塞事件循环。
3.2 聚合端:幂等写入与断点恢复
聚合端的核心职责是把散落在各节点的事件统一落盘并做初步汇总。因为采集端的数据是"至少一次"语义——节点写完本地文件,聚合端拉取后可能还没来得及确认就重启了,于是同一批事件会被再次拉到——聚合端必须处理重复。
Rea 的去重键很简单:host + service + name + ts + seq。其中seq是每个节点内部单调递增的序号,所以同一节点永远不会产生两对完全相同的ts和seq。聚合端拿到一条事件后先查这个键是否已经存在,存在就跳过。为了避免查重拖慢速度,我在内存里维护了一个最近 10 万条事件的键集合,老事件定期按时间窗口清理。
断点恢复方面,聚合端每处理完一批事件,就更新本地的游标文件,记录每个节点已经拉取到哪一条。这里有一个容易偷懒的地方:有人图省事,等到全部处理完才写一次游标,结果进程一崩溃,恢复后从很旧的位置重新拉,下游被重复数据打得很惨。正确的做法是每批处理完立刻同步落盘游标,虽然多了一点 IO 开销,但换来了"最多重复一个批次"的可靠边界。
3.3 告警规则:阈值只是配置,触发逻辑才要进代码
Rea 的告警模块最初和聚合逻辑耦合在一起,每个规则都要写一段代码,后来我把规则改成声明式配置,只有触发动作留在代码里。一个规则的示例大致是这样的:
rules: - name: 磁盘高水位 expr: disk_used_percent > 85 for: 5m group: infra notify: [default]expr是简单的比较表达式,for表示持续多久才触发。之所以坚持只用"阈值+持续时间"这种简单规则,是因为复杂规则如"连续三次上升且与前二十四小时同环比超过 20%"虽然强大,但调试成本高,容易误报。实际用下来,大部分故障都可以用简单的阈值加持续时间覆盖,真正需要复杂判断的场景,Rea 会选择把原始事件转发到外部分析,而不是在聚合端内置一个规则引擎。
配置化的另外一个好处是,业务同学可以自己提"我要在订单量低于某值时收到告警",而不用等我们去改代码。触发逻辑一旦固定成模板,新增规则和维护规则都变得非常轻。
4. 上线后的三个真实事故,以及我怎么把它们修好的
4.1 跨天窗口的统计缺了一截
Rea 上线第二周,有人反馈某天的错误率统计比平时少了一截,大概少了晚上十点到凌晨两点这段。我一开始怀疑是采集端漏采了,检查日志发现数据明明都进来了。
后来才发现问题是时间窗口的处理方式:聚合端按"本地日"做天级分桶,而事件里面的ts使用的是标准时区的时间戳。当采集端和聚合端不在同一时区,或者系统时区配置不一致时,靠近日界线的数据会被划分到错误的桶里,看起来就像缺了一段。
修复方案有两步。第一步,所有事件统一以标准时区的时间戳存储,展示时再由前端转换成本地时区;第二步,天级统计不再使用"今天零点到明天零点"这种固定边界,而是用滑动的最近 24 小时窗口。这样即使某个节点的时钟有一点偏移,也不至于把整段数据丢掉。
这个坑给我最大的教训是:时间戳的语义必须在事件模型建立时就定死,不要在存储层和展示层用各自的本地时间拼拼接接,否则排查问题时会非常痛苦。
4.2 重启后出现大量重复事件,顺序还乱了
还有一次,聚合端因为内存占用过高被自动重启。重启过后,下游统计面板出现大量重复计数,而且部分事件的顺序明显错乱——旧的错误率数据后出现,新的反而先到。
定位后发现是本地缓冲文件的重放逻辑有问题。聚合端重启时从上一次记录的游标位置继续拉取,但拉取回来的事件没有按seq做内存排序,直接按文件读取顺序写入了。各节点上报到缓冲文件时本身就是并发的,顺序本来就无法保证;一旦重放,原来已经被消费的记录再次进来,而新的记录又混在其中,最后就是乱上加乱。
修复分三层:
- 游标每批落盘,把可能重复的数据范围控制在最小;
- 聚合写入前按
seq排序,保证同一个节点的事件永远按序号递增落盘; - 查重键加上
seq,让重复记录不只靠时间戳判断。
经过这次之后,我再也不敢把"进程崩溃后还能否恢复出正确数据"当成默认成立的事情,任何有状态的模块都必须明确定义恢复语义。
4.3 告警风暴把通知通道打挂
告警上线后不久就迎来了第一次大考:某次存储节点连续故障,一个小时内,粘连在磁盘、延迟、错误率上的十几条规则轮番触发,通知通道直接被打满了频率限制,真正重要的故障反被淹没。
现在回看,这个问题本质上是没有区分规则触发和用户通知。正确的流程应该是:规则触发产生一条"告警事件",进入一个待通知队列,再由通知模块统一去重、合并、限频,最后发给用户。
Rea 现在的通知策略是三条简单的约束:
- 同一个规则在 10 分钟内最多通知 1 次;
- 同一个节点上的不同规则在 5 分钟内合并成一条汇总消息;
- 如果同时段有多个节点告警,只发一条包含节点清单的消息,明细留到页面上看。
这三条约束消灭了绝大多数重复打扰。从那以后,告警从"夜里手机被打爆"变成了"夜里只响一次,但每次都能定位到问题范围"。
5. Rea 现在的运行表现和接下来的想法
5.1 一组来自实际运行的参考数字
目前 Rea 管理着大约六十个节点,每分钟会产生三万条左右的事件,很多是业务侧主动上报的操作记录。采集端的常驻内存维持在 60MB 上下,聚合端单实例能稳定处理每秒 5000 条事件的峰值,从事件到达聚合端到在查询页面上出现,p99 延迟不到两秒。存储层使用文件按天分片,配合定期合并压缩,三十天的历史数据整体体量很小,专门维护存储集群的压力完全没有出现。
这个数据不算漂亮,但对我们来说是够用的。Rea 的目标始终不是处理海量日志,而是把资源指标和关键事件整理成"一打开就知道有没有问题"的视图。如果你也需要处理非常高的吞吐,那么把这套事件模型换成更底层的实现完全可行,但核心设计——统一事件格式、本地缓冲、幂等写入、配置化告警——是不用改的。
5.2 下一步:把事件相关性做起来
现在的 Rea 更像一个"汇总+告警"工具,离我理想中的"排查助手"还差一步。我接下来的计划是在事件模型之上构建一层相关性分析:当某个节点的错误率升高时,自动关联同一节点的磁盘事件、网络事件、最近发布记录,甚至把同一业务分组下其他节点的状态一并列出来。
这件事的底层能力其实已经有了,只差一个分析引擎。因为事件全部带了host、service、biz三层标签,做初步的链路关联并不需要在存储层做复杂计算,只需要在查询时把标签匹配和时序对齐做好。我计划先做一个最小版本:在一张展示页上,把一次故障的告警事件、相关指标变化、时间线内的所有操作记录放在一起,减少排查时切换上下文的时间。
我现在的习惯是每个月去翻一遍告警记录,看看哪些规则长期没触发、哪些规则触发太频繁,然后顺手调整阈值或通知策略。工具在一个团队里真正被接受,靠的往往不是功能数量,而是它能在你需要的时候刚好给出有用的信息。Rea 还在慢慢往这个方向演进。