如果你也见过那种“项目名只有三个字母、需求文档只有一句话”的场景那么你多半知道最难的部分从来不是最后那几行代码而是怎么把一个模糊到几乎没有任何信息的名字变成一个能落地、能验收、能产生实际价值的工具。这篇文章讲的就是我自己参与的一个内部工具项目代号恰好叫 rea我后来把全称填成了 Resource Efficiency Analyzer。简单说它是一个资源效率分析工具职责是持续采集一批机器的 CPU、内存、磁盘、网络等基础指标算出谁在真正被用到、谁在长期闲置并且在资源快要耗尽的时候提前发出提醒。当时触发我做这件事的直接原因是一批服务器的资源成本一直在涨但没有一个人能说清楚钱到底花在了哪里哪些实例常年利用率低得离谱。业务方的诉求很朴素给我一个能自动看数据、自动算指标、超阈值能通知到人的东西。这篇文章会完整讲一遍我是怎么从“rea”这个代号出发把需求拆到可以直接开发再一路落到采集、存储、聚合、告警、可视化这条链路上的全过程。适合正在做内部工具、云资源治理、成本分析相关工作的同学参考也希望给那些拿着一个很抽象项目名不知道从哪下手的读者一些可复用的思路。1. 一个只有三个字母的项目名藏着什么1.1 拿到“rea”之后先别急着写代码如果你是一个技术背景很强的人看到“rea”这样的项目名第一反应大概率是“这能做什么”我当时也一样。但我的习惯是先不急着写代码而是拿着这个名字去反向找业务锚点。项目名短不是问题问题是它字面上什么都看不出来所以必须靠业务输入把含义补全。我整理了三个可能的候选方向第一个是 runtime environment assistant偏运维自动化管理运行时环境第二个是 resource efficiency analyzer偏资源数据分析和效率判断第三个是 report evaluate assistant偏汇报和评估。每一个候选方向都对应完全不同的产品形态也对应完全不同的工期和团队配置。如果当时我先入为主凭个人偏好选了 runtime environment assistant最后大概率会做成一个运维平台三个月都收不了尾。后来我找了几个最关心成本的人聊了一圈。他们反复提到的痛点是“账单在涨但不知道是哪些机器在浪费资源”“高峰期前完全没有感知等到线上出问题才去临时加资源”。这就很明确了核心矛盾不是环境管理而是资源效率的可见性和预警能力。所以最终我把 rea 定位成 resource efficiency analyzer一个成本与资源效率分析工具。这里有个小经验项目名越含糊越不能用技术惯性去猜必须把业务方口中的“痛点”翻译成可验证的数据指标。1.2 需求边界怎么定从三个候选方向收敛确定方向之后我做的第一件事是写一页纸的定义把所有内容限定到能直接指导开发的程度。这页纸里最重要的不是功能清单而是三件事目标用户是谁、核心场景是什么、成功之后能回答哪些问题。我先列了一张很简单的表格把边界框住维度内容目标用户负责资源运维和成本控制的开发、运维人员核心场景每周成本复盘、季度容量规划、故障前的资源预警主要输入每台机器的 CPU、内存、磁盘、网络等基础指标主要输出资源报表、TOP 消耗排行、超阈值警戒通知非目标不做自动扩容、不做应用性能追踪、不采集业务日志最后一行“非目标”我认为比“目标”更重要。把它写进文档需求的蔓延才挡得住。否则开发到一半业务方很可能会说“既然你能看 CPU能不能顺便帮我看一下某个接口的响应时间”这种需求不是不好而是会把一个本来就模糊的项目越拖越大。资源分析就是资源分析应用性能追踪是另一个系统该做的事这个边界从第一天就要明确。1.3 应用场景到底能解决什么问题边界定了还得让业务方相信这个东西有实际价值。我当时画了三个最典型的使用场景全都是他们一听就懂的那种。第一个场景是每周自动出一次成本复盘报表。报表里明确列出哪些实例在过去七天 CPU 平均使用率低于 10%、内存长期空闲标记为“可回收资源”。实际跑下来我们发现有一批开发机群存在很严重的冷热不均三台机器长期接近 100% 使用率另外五台长期低于 5%。重新分配之后业务没有新采购任何硬件就把高峰期的压力扛下来了。这就是资源效率分析最直接的价值不是省了多少成本而是让“该花钱的地方花得明白不该花钱的地方一眼看到”。第二个场景是高峰期前的预警。在业务大促或集中任务开始前系统会按照历史趋势判断资源水位。如果 CPU、内存或磁盘正在逼近阈值相关人第一时间收到提醒还可以赶在出问题之前去做扩容或任务调度调整。第三个场景是容量规划。季度末做预算时不再靠拍脑袋估算未来需要多少资源而是直接拉出过去几十天的历史曲线结合业务增长趋势推算增量。这个功能现在看起来稀松平常但在没有工具辅助的时候完全靠人工翻控制台和表格效率完全不是一个量级。2. 整体技术方案与选型思路2.1 总体架构一条从采集到展示的链路定完需求接下来就是技术方案。这个项目没有用任何复杂架构整条链路可以用一句话讲清楚采集端定时把指标发到后端后端写入时序存储查询时走聚合接口另有一个定时任务负责扫描阈值并触发通知。更细一点分成五个部分采集端部署在被监控机器上通过 crontab 定时执行只做指标读取和上报。接收端一个很薄的 Web 服务负责接收数据、校验合法性、写入存储。存储端时序数据库按时间组织数据天然支持保留策略和聚合查询。计算端定时任务扫描最近一段时间窗口的数据做阈值判断产生告警事件。展示端一个轻量网页提供曲线图和排行列表供人查看。选择这种链路的思路很简单端到端只有五个环节任意一个环节出问题最多是当天报表缺一块不会拖垮整个系统。对于内部工具来说“可以快速定位哪里坏了”比“功能多强大”更重要。2.2 为什么选择定时轮询而不是常驻采集代理第一版方案里采集端到底做成什么样我纠结过一阵子。常驻 Agent 的优点是采集粒度可以很细、可以实时推送但代价也不小需要做进程守护、自动升级、日志采集、批量分发还要考虑 Agent 自身占用的资源。对一个目标是“两三个人用碎片时间做出来”的内部项目来说这些复杂度会拖慢核心价值的交付。所以我最终选择了定时轮询加 HTTP 上报。被监控机器上只放一个独立脚本由 crontab 每分钟触发一次脚本读取系统指标然后通过 HTTP 请求把数据推给后端。这样做有至少三个明显好处。第一个好处是维护成本低。脚本是独立文件逻辑简单不会常驻内存升级直接替换文件即可。第二个好处是权限边界清晰。脚本只做只读操作不监听端口、不开放额外服务即使出了问题影响面也控制在“今天数据没采到”不会对线上业务造成额外风险。第三个好处是迁移空间保留。后端的接收接口从一开始就设计成“采集端只管上报后端不关心采集端是谁”以后如果机器规模涨到几百台需要换成标准采集代理后端几乎不用改。2.3 存储选型为什么用时序库而不是关系库存储这块很多人的第一反应是“数据量不大用关系库不就行了”我的答案是不建议。原因要从数据形态说起。资源指标是典型的时间序列数据。每台机器每 60 秒产生一个点假设有 50 台机器、每个周期采集 6 个指标一天下来就是 50 乘 6 乘 1440大概 43 万个点。关系库当然也能存下但问题不在“能不能存”而在“好不好查”。要按 5 分钟窗口算平均值和峰值SQL 会写得很啰嗦要删除 30 天前的旧数据得自己写定时清理任务要在同一张表里按主机、指标、时间三个维度做聚合索引设计稍不注意就变慢。时序库把这些事情变成了内置能力数据按时间分片存储写入和查询天然高效保留策略可以自动清理过期数据聚合查询可以直接按时间窗口分组。同样的需求用关系库可能要写五六十行复杂的 SQL在时序库里就是一次标准的窗口查询。我当时给数据设计得很简单一个 measurement 叫 resource_metrictags 里放主机名和指标名fields 里放具体数值时间戳统一使用整数秒。这样的设计在查询时非常顺手不管要按主机拆还是按指标拆都能直接走 tag 过滤。2.4 控制复杂度的取舍这个项目能快速交付还有一个关键原因是主动砍掉了很多看起来“以后会用到”的东西。我没有引入消息队列没有引入分布式任务调度也没有做微服务拆分。原因很简单当前的数据量级采集端每秒产生的数据量远低于后端的消费能力队列只会让故障排查多一个环节几十台机器的定时任务系统自带的 crontab 和单进程调度完全够用。架构上的取舍要跟阶段匹配。业务没有涨到那个量级之前复杂组件不是资产而是负债。rea 第一版的目标就是“用最简单可靠的方式把链路跑通”而不是“搭建一个可以无限扩展的框架”。3. 核心模块拆解与实操要点3.1 采集模块指标定义与代码要点采集模块是整条链路的最上游数据质量决定后续所有环节的可靠性。我第一版只采集了 6 个最核心的指标指标名类型单位采集方式cpu_usage百分比percent读取 /proc/stat 计算差值mem_used_percent百分比percent读取 /proc/meminfodisk_used_percent百分比percent读取文件系统使用情况net_rx_bytes流量bytes读取网络接口累计流量net_tx_bytes流量bytes读取网络接口累计流量proc_alive进程状态count检查关键进程是否存在采集周期我定为 60 秒。这个粒度对于资源分析和容量规划完全够用存储压力也不大。脚本逻辑很简单核心是读系统文件拿原始数据然后批量上报。import os import json import time import urllib.request def read_cpu_percent(): # 简化示意真实场景应读取两次 /proc/stat # 取两次间隔的差值才能算出一段时间内的平均使用率 with open(/proc/stat, r) as f: fields f.readline().split() total sum(int(x) for x in fields[1:]) idle int(fields[4]) return round((1 - idle / total) * 100, 2) def read_mem_percent(): with open(/proc/meminfo, r) as f: lines f.readlines() mem_total int(lines[0].split()[1]) mem_avail int(lines[2].split()[1]) return round((1 - mem_avail / mem_total) * 100, 2) def collect(): return [ {metric: cpu_usage, value: read_cpu_percent(), unit: percent}, {metric: mem_used_percent, value: read_mem_percent(), unit: percent}, ] def report(host, token, metrics): data {host: host, ts: int(time.time()), metrics: metrics} req urllib.request.Request( http://server.local/api/ingest, datajson.dumps(data).encode(), headers{Content-Type: application/json, X-Token: token}, ) return urllib.request.urlopen(req, timeout5).read()这里要特别提醒一个坑cpu_percent 这类指标如果只读一次瞬间值会有很大噪声正确的做法是取两个时间点的差值再计算。同样脚本首次运行时建议先预热一次否则第一份数据可能在采集间隔内失真。上报请求一定要设置超时一般 3 到 5 秒就够避免采集端因为网络抖动卡住整个流程。3.2 指标归算与聚合逻辑从原始点到时间窗口原始数据是 1 分钟一个点但展示和告警的时候我并没有直接用原始点而是先归算到 5 分钟窗口。原因有两个一是降低数据噪声单分钟的 CPU 尖峰很常见不代表真的需要扩容二是减少计算量5 分钟窗口把数据量缩到原来的五分之一前端展示和告警判断都更轻。归算逻辑在查询时完成而不是在写入时就覆盖原始数据。我保留了全部原始点这样万一后续要排查某个时刻的精确波动还能追溯回去。查询时按主机和指标分组统计每个 5 分钟窗口内的平均值、最大值、最小值和采样数。select time_bucket(5 minutes, ts) as bucket , host , avg(value) as avg_value , max(value) as max_value , min(value) as min_value , count(*) as sample_count from resource_metric where metric cpu_usage and ts now() - interval 24 hours group by bucket, host注意这里我同时保留了 avg 和 max。均值反映整体利用率峰值反映瞬间压力。判断一台机器能不能回收不能只看均值。有些机器均值很低但每天固定时段会有很高的峰值这种机器直接回收是很危险的。把两个值放在同一行结果里做容量判断时就能一眼看全。3.3 阈值判断与告警少报警、准报警告警模块是这个项目里最容易做砸的地方。一开始我设过很敏感的规则比如 CPU 超过 80% 就告警结果一天能收到上百条消息业务方直接把通知群屏蔽了等于告警系统彻底失效。所以我后来总结出一条原则告警的价值不在于数量而在于每一条都值得关注。我给告警分了两个级别warning 和 critical。预警规则全部采用“持续窗口 阈值”的双重判断而不是瞬时值触发。具体规则是这样的告警规则触发条件告警级别静默时间CPU 使用率持续高连续 3 个 5 分钟窗口均值 85%warning6 小时内不重复通知内存使用率持续高连续 2 个 5 分钟窗口均值 90%warning6 小时内不重复通知磁盘使用率过高任意 5 分钟窗口 90%critical1 小时内不重复通知数据采集缺失某主机连续 15 分钟无新数据点critical1 小时内不重复通知为什么 CPU 要连续 3 个窗口才告警因为业务机器经常会出现短暂的负载尖峰持续 15 分钟的均值超过 85%才真正说明资源快要撑不住了。为什么磁盘只要 1 个窗口就告警因为磁盘空间的增长通常是单向的涨上去不会自己掉下来等它“持续”的时候可能已经没那么从容了。这种差异不是拍脑袋定的而是在跑了一个星期数据之后根据真实曲线的形状调出来的。告警通知方面我用的是通用群机器人 Webhook。发送时带上主机名、指标名、当前值、趋势描述和跳转链接让收到通知的人不用去翻系统就能判断要不要处理。每个告警都加了静默时间同一主机同一指标在静默期内不重复轰炸恢复之后才重置计数。这一条相当重要否则凌晨三点连续收到 20 条告警正常人都会疯。3.4 可视化面板最快速度让人看到效果内部工具最怕什么怕业务方看不到价值然后不用。所以我第一版没有追求完整复杂的 Dashboard只做了一个单页上面有两个核心视图一个按主机和指标筛选的曲线图一个按平均值排序的 TOP 5 资源消耗排行榜。光这两样就已经能把业务方说服了。曲线图展示最近 6 小时的数据默认粒度 5 分钟一次查询只返回 72 个点前端轻、加载快。你可以通过下拉框切换主机和指标也可以直接切换到 24 小时、7 天的时间范围。TOP 5 排行榜按所选时间范围内每个主机的平均 CPU 或平均内存排序一眼就能看出哪些机器最忙、哪些机器最闲。这个排行榜后来成了每周成本复盘的核心依据比任何花哨图表都实用。前端实现没有用重型框架就是一个静态页面加少量脚本通过普通接口调后端数据。这里有个细节查询接口必须支持聚合参数不能把原始点全量传给前端。否则页面一打开就要拉几千个点图表又卡又难读。3.5 数据规范与单位处理还有一个容易在后期爆发的问题就是单位不统一。网络流量如果有的地方写 bytes、有的地方写 MBps磁盘容量有的地方写 GB、有的地方写 KB报表和告警会乱成一团。我在写入端就强制约定百分比的指标统一写 percent网络流量统一按 bytes 存储展示时再转换为 Mb/s 或 MB/s磁盘容量统一按字节存储展示时转成 GB。每种指标还带一个 unit 字段后续前端不管怎么换都能按标准单位做转换。这个习惯帮我避开了很多潜在的返工。数据规范这种事越早定越好越晚改越麻烦。等所有历史数据都已经写入存储之后再去改单位那一周的报表可能全部要重出。4. 从开发到落地完整实操过程4.1 环境准备与项目骨架动手开发之前我先定好了部署形态。被监控机器是几台内部 Linux 服务器另外有一台独立的部署机专门跑后端的接收服务、时序存储和网页。所有东西都用容器编排统一拉起因为内部工具需要的是“一条命令起全套环境”而不是在一台机器上手动装一堆服务。整个项目目录结构非常简单rea/ collector/ # 采集端脚本 server/ # 后端接收服务和查询接口 web/ # 静态页面和前端脚本 deploy/ # 容器编排和配置文件 docs/ # 需求定义和上线清单把采集端单独放一个目录是为了便于在没有后端的环境里单独测试也为以后切换到标准采集代理留出空间。后端服务和前端页面分开放部署的时候用容器编排一起启动。4.2 采集端联调先跑通一条指标很多人做这类项目会犯一个错误上来就把所有指标都写好然后一次性联调。发现问题的时候根本分不清是 CPU 指标写错了、还是网络指标的单位换算错了。我建议反过来先只跑通一条指标验证完整链路之后再扩展其他指标。我的第一步是只采 CPU。先手工执行采集脚本确认请求能收到 200 响应然后回到后端查一下存储里有没有刚写入的数据点接着在网页上刷新曲线看到 CPU 曲线在跳动最后才配置 crontab 每分钟执行一次。整个链路从“数据产生”到“数据可见”走通之后再往采集脚本里加内存、磁盘和网络指标每个指标加完都立刻到前端确认数据正常。crontab 的配置看起来简单但有一个容易踩的坑环境变量。通过 crontab 运行时PATH 往往跟登录 shell 不一样如果脚本里用相对路径或者依赖某个不在 PATH 里的解释器就可能出现“手工跑正常定时跑没数据”的情况。我最后统一使用绝对路径并把日志重定向到固定文件* * * * * cd /opt/rea /usr/local/bin/python3 collector/main.py logs/collector.log 21另外crontab 本身没有做并发保护如果前一次脚本因为网络卡住还在跑、后一次又被触发就会出现重复上报。我在脚本里加了一个简单的单实例锁用 pid 文件方式实现进程还在就跑了个空逻辑直接退出。4.3 后端接口与前端页面串起来后端是整条链路的中枢但接口不需要多我个人第一版只写了三个POST /api/ingest接收采集端上报的数据校验合法后写入存储。GET /api/query按主机、指标、时间范围返回聚合后的曲线数据。GET /api/summary返回当前时间范围内的 TOP 资源消耗排行。ingest 接口的伪代码逻辑大概是这样的app.post(/api/ingest) def ingest(): data read_json_body() if data[token] ! expected_token: return unauthorized() for metric in data[metrics]: validate_metric(metric) storage.write( measurementresource_metric, tags{host: data[host], metric: metric[name]}, fields{value: metric[value]}, timestampdata[ts], ) return {ok: True}这里有个安全问题必须提一句内部接口虽然部署在内网但同样需要加共享 token 鉴权。否则一旦某个机器被误扫描到或者有人不小心调了这个接口就可能写入虚假数据污染整张告警报表。真实生产环境里我还限制过部署机的防火墙只开放必要的端口给采集端和目标访问者。query 接口要支持 start、end、host、metric、interval 五个参数interval 就是前面说的聚合窗口默认 5 分钟。前端拿到数据后把时间戳和值分别映射到坐标轴画曲线非常快。4.4 上线前后的校验清单工具开发完不代表能上线我列了一个上线前校验清单每项都实际跑过一遍检查项检查方法通过标准数据连续性查看最近 1 小时各主机数据点数每分钟至少一个点主机名唯一性扫描 tags 中的 host 字段无同名、无空格告警规则生效临时调低阈值触发一次告警通知能收到恢复后能再收到数据保留策略确认存储自动清理任务已配置原始数据保留 15 天前端默认范围打开页面确认默认展示最近 6 小时图表正常加载采集端并发保护手动连续触发两次脚本第二次不会被重复执行上线之后的第一周重点是观察两件事数据缺失率和告警频率。如果某台机器每小时告警超过五次说明阈值设得太低或者采集本身有问题需要立刻调整。我踩过一次比较深的坑是告警规则测试的时候用的是临时低阈值测试完忘记调回来导致一台机器疯狂触发 CPU 告警。所以我在配置里把“测试临时阈值”和“生产阈值”分开存上线检查清单里专门加了一条“确认当前使用阈值不是测试值”。另外内部工具很容易出现“上线即无人用”的结局。为了避免这一点第一周我主动给相关业务方推送了一份周报基于 rea 数据整理出的实例使用率排行、闲置资源清单、本周出现的告警事件汇总。报表比任何演示都有说服力业务方看到真实数据之后才会真正开始依赖这个工具。5. 常见问题与排查技巧实录5.1 典型问题速查表这个项目运行了大半年各种奇奇怪怪的问题基本都遇到过。我把最典型的整理成一张速查表大家可以先收藏现象可能原因排查步骤解决办法曲线大面积断层采集脚本崩溃或宿主机休眠查看采集日志有无异常补日志轮转异常时触发通知所有指标延迟数分钟采集端时间与服务器不同步检查系统时钟接入统一时间同步CPU 曲线一直是 0首次采集未预热检查启动时序启动后先采集一次废弃告警轰炸缺少持续窗口和静默期看告警日志分析频率加按时间窗判断和静默磁盘统计不准根分区和数据分区不一致查看挂载点列表改为采集多个挂载点网页卡顿前端请求了原始粒度看接口返回点数强制走聚合接口5.2 四个让我印象深刻的实际案例第一个案例是 crontab 里脚本看上去在跑但数据那一整段全空。我在部署机上手工执行没有任何问题但走到定时任务就失败。查到最后发现是 crontab 的环境变量跟 shell 不一致python3指向了一个不存在的路径。从那以后我所有定时任务里的解释器和脚本路径全部写绝对路径。第二个案例是某个主机名带了空格导致所有基于这个 host 的分组和排序全部异常。在常规配置里大家默认主机名不会带空格但真实环境真的有可能出现。我在 ingest 接口里加了严格的合法性校验只允许字母、数字和横线不合法的直接丢弃并记日志。第三个案例是网络流量指标早期全部是乱数。原因是同一台机器的不同网卡采集逻辑不一致有的算的是带宽有的算的是计数器单位混在一起完全没法看。后来我在上报前加了一层范围检查比如百分比必须在 0 到 100 之间网络流量必须是在合理区间内的非负整数超出范围的直接丢弃。这个“脏数据入口拦截”帮后续的报表省了很大麻烦。第四个案例最有代表性。某个版本上线后凌晨三点某个业务方连续收到 20 条同类型告警。原因是我只做了“达到阈值就发通知”没有做持续窗口判断也没有做静默期。那台机器在高峰期 CPU 反复跨越阈值通知就一遍遍发。修复方案就是前面提到的两个连续 N 个窗口都超过阈值才告警同一告警在 6 小时内只通知一次只在恢复后重置计数。5.3 关于权限与健壮性的提醒最后聊一下权限控制。资源指标虽然不像业务数据那么敏感但单位置如果暴露在公网或者内网不受控区域很容易被写入垃圾数据污染统计。我在第一版就坚持了两点所有接口都要求共享 token部署机的防火墙只开放采集上报和人员访问需要的端口。内部工具不一定需要引入复杂的权限模型但最基本的认证和最小权限原则不能省否则后面任何一次数据异常排查都会非常痛苦。另外一个容易被忽略的点是采集脚本自身的健壮性。脚本要做到“遇到任何错误都不能把异常抛到 crontab 日志里直接中断”核心逻辑外面要包一层异常捕获上报失败时记录错误但不堆积线程单次运行必须保证在采集周期内结束。它的角色只是“尽力上报”稳定性更多靠后端的数据补齐逻辑和缺失告警来兜底而不是靠采集端死扛。我在整个 rea 项目里最深的体会是项目名短不是问题问题是业务目标太长。任何一个名字抽象的项目都要先回答三句话——为谁解决什么问题、用什么数据判断成功、哪些功能明确不做。只要这三句话落地了哪怕项目代号真的就叫 rea它也会是一个值得用的工具。如果你正拿着一个很模糊的项目名在纠结我建议你先别打开编辑器先把那三句话写下来再说。