ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自建轻量级服务器监控系统:OpenMontage从设计到部署全指南

自建轻量级服务器监控系统:OpenMontage从设计到部署全指南

直接切入正题。一般聊到批量管机器,要么是十几台虚拟机,要么是几台物理服务器,用面板点一点还行,真到几十台、上百台,还要兼顾不同机房、不同系统版本时,一个个 SSH 上去查负载、看日志、调参数,纯属浪费时间。我断断续续折腾过好几套方案,最后自己动手组了一套基于开源组件、自己定制采集逻辑的统一管理与远程监控系统,起了个名字叫OpenMontage。它解决的问题很朴素:一台跳板机,把所有机器的 CPU、内存、磁盘、进程、关键日志、以及需要定时执行的命令结果,统一汇总到一个网页仪表盘里,再配上报警和自动化处理。适合正在维护小规模服务器集群、或者帮朋友托管一堆业务机的个人开发者、运维爱好者,也适合小团队在没有商业监控平台预算时快速建一套可用的监控底座。

这套东西我不打算讲得玄乎,直接把它拆成“思路设计”“落地部署”“经验技巧”“问题排查”“延伸玩法”五块来讲。你把它看成一份能直接照着敲的工程笔记即可,核心是让你明白每个环节为什么这么做,踩到坑时有个排查方向。

1. 整体思路与设计拆解

1.1 先想清楚要监控什么,再决定怎么搭

很多人在搭监控系统时容易犯的一个毛病是“先装一堆组件,再慢慢研究能监控什么”。OpenMontage 的做法反过来,先盘清楚自己到底要解决哪些问题,再决定采集什么数据。就我个人而言,最高频的需求基本集中在这几类:

  • 机器是否在线,网络有没有抖动,SSH 是否能连。
  • CPU、内存、磁盘、网络流量的实时数值与历史趋势。
  • 某些核心进程是否存活(比如 Nginx、MySQL、Redis)。
  • 关键日志里有没有出现报错关键字(比如 OOM、Connection refused)。
  • 定期要跑的运维脚本有没有正常结束、输出结果对不对。

明确了这五类需求后,整个系统的采集端就不用设计得很复杂,轻量级 agent 定时上报即可,服务端负责收数据、算阈值、出图表。它本质上是一个“自采自用”的轻量级监控闭环,而不是像商业产品那样把数据模型做得大而全。这样做的最大好处是:你完全知道每一条数据从哪来、经过哪条链路、有什么用,出了任何问题都能快速定位,不会被庞大的黑盒逻辑绕晕。

1.2 OpenMontage 的组件分层与数据流

按照我的习惯,这种工具型项目一定分三层设计,避免将来想扩展时牵一发而动全身:

  • 采集层:部署在各业务机器上的轻量 agent。负责执行预定义的采集脚本,把结果组织成 JSON 格式上报。采集脚本可以是一个统一的 bash/python 脚本,也可以由服务端下发的配置决定。
  • 传输层:负责把采集数据从业务机送到服务端。我用的方案是 HTTP POST + 心跳机制,agent 每隔 N 秒调一次服务端 API,既上报数据又当心跳保活。这个设计比 agent 主动连服务端的长连接方案简单,容忍断线重连,部署时不用维护复杂的连接状态。
  • 展示与处理层:跑在跳板机上的服务端,包含一个 API 网关、一个时序数据存储、一个告警判定模块和一个 Web 仪表盘。

实际数据流是这样的:agent 在业务机上跑完采集脚本,把 CPU 负载、内存占用、磁盘使用率、进程状态等包装成一个 JSON,通过 HTTP POST 到服务端的/api/report接口,服务端把数据写入存储,同时触发告警规则判断。Web 前端通过/api/query读取历史数据并渲染图表。这套模型后来证明非常稳,因为它把“数据从哪里来”和“数据展示成什么样”彻底解耦了。

1.3 为什么不直接用现成的商业监控平台

我相信很多人看到这里会问:直接用现成平台不就行了吗?确实,成熟平台功能强大,但有两个问题让我最终决定自建轻量方案:

  • 部署成本高。不少商业方案全家桶动辄需要好几台机器、数据库集群、消息队列,为了监控几台业务机,反而比业务机本身还重。
  • 定制不灵活。我想在仪表盘上加自己关心的指标,比如某个业务目录的备份文件大小、某个压测脚本的最新一次跑分,这类业务定制指标在通用平台里经常要写插件甚至改源码。

OpenMontage 的价值不在于“把监控这件事做得比别人更复杂”,而在于把核心链路做得很薄、很直接,给后续定制留下充足空间。如果你也处在“需要监控,但不想花一整周部署一个重型平台”的阶段,这套思路会很契合。

2. 服务端核心实现与采集端配置

2.1 服务端开发环境与目录结构

我用的是 Python 3.10 + FastAPI + SQLite 起步,SQLite 在监控数据量不大的阶段完全够用。当数据量涨到每天几十万条记录时,再平滑迁移到 PostgreSQL 或者 TimescaleDB。先看目录结构:

openmontage/ ├── server/ │ ├── main.py # FastAPI 主程序,路由与启动入口 │ ├── models.py # 数据模型定义,对应 SQLite 表 │ ├── reporter.py # 上报接口处理逻辑 │ ├── alerter.py # 告警规则判定 │ └── web/ │ ├── index.html # 前端仪表盘页面 │ ├── app.js # 图表渲染与轮询逻辑 │ └── style.css ├── agent/ │ ├── agent.py # 采集端主程序 │ ├── collector.py # 内置采集函数库 │ └── config.ini # agent 配置文件 └── scripts/ ├── deploy_agent.sh # 一键批量部署 agent 的脚本 └── init_db.py # 初始化数据库

服务端启动命令很简单:uvicorn main:app --host 0.0.0.0 --port 8000。需要说明的是,这里所有服务都跑在跳板机上,业务机只安装 agent,不开额外端口,从安全角度看暴露面小很多。

2.2 数据表设计与上报接口

在models.py里,我为最核心的三张表设计了这样的结构:

  • hosts表:存机器基本信息,包括主机名、IP、分组、上线时间、agent 版本。
  • metrics表:存时序监控数据,字段有 host_id、metric_name、metric_value、record_time。
  • alerts表:存告警记录,字段有 host_id、rule_name、level、message、status、create_time。

上报接口的实现并不复杂,核心代码如下:

@app.post("/api/report") async def report(data: dict): host_key = data.get("host_key") metrics = data.get("metrics", []) host = check_host(host_key) if not host: return {"code": 1, "msg": "unknown host"} save_metrics(host.id, metrics) check_alerts(host.id, metrics) return {"code": 0, "msg": "ok"}

这里有个容易忽略的细节:check_host一定要按host_key做鉴权,而不是只看 IP。因为 agent 可能通过 NAT 出去,服务端看到的源 IP 不是业务机真实内网 IP。我在 agent 的配置里为每台机器生成一个唯一的 host_key,相当于机器的“身份证”,服务端只认 key。这个设计在后续批量接入时省了不少麻烦。

2.3 采集脚本如何设计得既通用又灵活

采集端 agent 的核心是collector.py,一个函数对应一类指标。通用指标我直接复用 psutil 库,几行代码就能拿到绝大多数系统数据:

import psutil def collect_cpu(): return { "cpu_percent": psutil.cpu_percent(interval=1), "load_avg": psutil.getloadavg(), "cores": psutil.cpu_count() } def collect_memory(): mem = psutil.virtual_memory() return { "total": mem.total, "used": mem.used, "percent": mem.percent } def collect_disk(): result = [] for part in psutil.disk_partitions(all=False): usage = psutil.disk_usage(part.mountpoint) result.append({ "mount": part.mountpoint, "total": usage.total, "used": usage.used, "percent": usage.percent }) return result def collect_process(process_names): alive = [] for proc in psutil.process_iter(["name"]): if proc.info["name"] in process_names: alive.append(proc.info["name"]) return {"processes": list(set(alive))}

自定义指标用配置化的方式扩展。我会在config.ini里加一个custom_scripts小节:

[custom_scripts] backup_size = du -sh /data/backup | awk '{print $1}' nginx_error = tail -n 20 /var/log/nginx/error.log | grep -c "crit"

agent 启动时会读取这些配置项,把命令丢给系统 shell 执行,然后把输出作为自定义指标上报。这种方法有一个明显的优点:不需要为了加一个新监控项而重新发布 agent 进程,改配置再重启就能生效。也正因如此,OpenMontage 很容易从“监控系统”延伸成“批量命令执行平台”。

2.4 心跳与超时判定逻辑

在线状态不能只靠“上次上报时间是否在最近 N 秒”来猜,这会出不少误判。我采用的是心跳独立上报,agent 每 30 秒发一次心跳包,服务端每收到一次心跳就更新hosts.last_seen。前端展示在线状态时,把last_seen距今超过 90 秒的机器判为离线。这个 90 秒阈值不是随手拍的,它考虑了 agent 上报周期、网络抖动容忍、以及告警延迟三者的平衡。如果阈值设太短,网络一抖动就误报;设太长,机器真挂了会有很长的盲区。最终 30 秒上报 + 90 秒判定在实测中既灵敏又稳定。

3. 仪表盘与告警规则配置

3.1 前端图表如何直接抄作业

仪表盘部分我没引入重型前端框架,用了纯 HTML + ECharts。ECharts 图表库按需引入,只加载折线图、饼图需要的模块,这样页面的加载速度快,维护成本也低。关键逻辑在app.js里:每隔 10 秒向/api/query拉一次最新数据,更新图表和表格。这样做虽然理论上比 WebSocket 实时推送“笨”一点,但胜在链路简单,不会出现连接断开后图表不更新的问题。

绘制 CPU、内存趋势图的代码大致是这样的:

function renderCpuChart(hostId) { fetch(`/api/query?host_id=${hostId}&metric=cpu_percent&range=1h`) .then(res => res.json()) .then(data => { cpuChart.setOption({ xAxis: { type: 'time', data: data.timestamps }, yAxis: { type: 'value', name: 'CPU %' }, series: [{ name: 'CPU', type: 'line', smooth: true, data: data.values, areaStyle: { opacity: 0.2 } }] }); }); }

要提醒的是,数据库里存的时间戳统一用 UTC 存储,前端展示时再转本地时区。这个习惯能避免很多因为服务器时区设置不一致导致的时间错乱问题。我被这个问题坑过不下三次,现在所有项目一律 UTC 存储,只在展示层做转换。

3.2 告警规则怎么设才不烦人

告警规则放在了alerter.py,用简单的 JSON 规则描述来管理,不用复杂的表达式引擎。举个例子:

{ "rules": [ {"name": "cpu_high", "metric": "cpu_percent", "operator": ">", "threshold": 85, "cooldown": 600}, {"name": "disk_full", "metric": "disk_percent", "operator": ">", "threshold": 90, "cooldown": 1800}, {"name": "process_down", "metric": "process", "operator": "not_contains", "threshold": "nginx", "cooldown": 300} ] }

每个规则都带一个cooldown冷却期。这个设计非常关键,它保证同一规则在冷却期内不会重复触发告警。比如 CPU 持续 10 分钟超过 85%,系统只在第一次超阈值时告警,之后 10 分钟内不再重复发,避免“告警轰炸”。告警时我接入了 Server 酱和邮件两个通道,逻辑上先发 Server 酱即时通知,再发汇总邮件兜底。如果你不想要这层通知,也可以直接加一个 action 字段,把告警变成执行某条修复命令,比如发现 Nginx 挂了就自动拉起。

3.3 告警风暴的降噪技巧

实际运行中,最让人头疼的不是告警太多,而是告警里夹杂着大量无效信息。为此我加了一个“告警聚合”机制:同一台机器同一规则多次触发时,不会生成多条独立告警,而是在同一条告警记录上更新触发次数和最后触发时间。只有恢复后再次触发,才会新建一条告警。这样再看告警列表时,每条都基本对应一个真实问题,不会出现首页被几十条相似告警刷屏的情况。

阈值设置上的心得是:越接近资源上限,越要给告警留出响应时间。比如磁盘使用率,85% 就该提醒,等到了 95% 很可能你连 SSH 进去清日志都要卡半天。CPU 告警我不看瞬时值,而是用最近 3 分钟的平均值来判断,因为瞬时高 CPU 很可能是某个定时任务在跑,平均超过阈值才说明真的有压力。这个平均值可以在采集端算好,减少服务端计算量。

4. 多机部署流程与一键接入经验

4.1 从单机测试到批量部署的平滑过渡

我在本地先跑通单机 agent 没问题后,才开始批量部署。批量部署用的是deploy_agent.sh脚本,它读取一个hosts.txt清单,逐台执行以下步骤:

  • 上传agent目录到目标机器的/opt/openmontage/agent。
  • 在目标机器生成或写入host_key。
  • 配置 systemd 服务,设置开机自启。
  • 启动 agent 并确认心跳上报成功。

这里有一个非常重要的技巧:不建议在批量部署时临时用密码登录,统一用 SSH Key 认领会稳得多。在跳板机上生成密钥对后,把公钥分发到各业务机的authorized_keys,然后批量脚本全程免密执行。整个过程大概这样:

#!/bin/bash while read host; do echo "deploying to $host" scp -r ./agent "$host:/opt/openmontage/agent" ssh "$host" "cd /opt/openmontage/agent && python3 -m venv venv && ./venv/bin/pip install psutil requests" ssh "$host" "sudo cp openmontage-agent.service /etc/systemd/system/" ssh "$host" "sudo systemctl daemon-reload && sudo systemctl enable --now openmontage-agent" done < hosts.txt

如果只部署一台机器,这个脚本看起来像是绕远路;但当机器数量到了 30 台以上,这个脚本能帮你把半小时的机械操作压缩成 1 分钟。实践下来,整批部署最容易出问题的地方不是脚本逻辑,而是机器之间的环境差异,比如某些机器没有联网安装 pip 的权限、某些机器 Python 版本过低。我的对策是在脚本里先检测环境,不满足条件就不继续部署,而不是等到 agent 起不来了才排查。

4.2 agent 服务配置文件细节

agent 的config.ini我给出一个标准参考配置:

[server] endpoint = https://monitor.example.com/api/report heartbeat_interval = 30 host_key = auto-generate [collect] interval = 30 cpu = on memory = on disk = on process = nginx,redis-server,mysqld [custom_scripts] backup_size = du -sh /data/backup | awk '{print $1}' nginx_error = tail -n 20 /var/log/nginx/error.log | grep -c "crit" [logging] level = INFO log_file = /var/log/openmontage-agent.log

这里host_key = auto-generate表示第一次启动时,agent 根据机器 ID + 随机数自动生成一个 32 位十六进制 key,并保存到本地文件。下次启动读取同一个 key,保证身份稳定。如果你希望统一管理 key,也可以在批量部署时由脚本生成并写入,这样服务端甚至可以直接“预注册”所有机器。

服务端 API 地址这里的https://不是摆设。如果采集数据只是内网传输,用明文 HTTP 问题不大;但只要跨公网传输,就必须在跳板机前面加一层 HTTPS(比如 Nginx + Let's Encrypt 证书),否则机器信息、host_key 都暴露在网络上,风险极高。

4.3 服务端如何区分环境分组

机器多了以后,没有分组的管理页面基本没法用。我在hosts表里加了一个group_name字段,把机器分成“生产”“测试”“办公室”“客户项目”等不同分组。前端仪表盘支持按分组筛选,告警规则也能指定只对某个分组生效。比如测试环境的磁盘告警阈值可以放宽到 95%,生产环境则必须 85% 就开始报警。分组这个看似简单的功能,在多项目混跑的机器上特别有用,它能避免测试机器产生的噪音告警干扰生产监控。

分组信息是部署时写在 agent 配置里的,agent 上报数据时带上group_name,服务端自动创建不存在的分组。这样后面新机器上线时,只要在config.ini里写对组名,被采集端改动一次,根本不需要到服务端后台去手动添加机器。

5. 常见问题与排查技巧实录

5.1 机器明明活着,却显示离线

这是我最常遇到的问题。排查时不要急着怀疑服务端,先按下面顺序逐层确认:

  • agent 进程是否还在运行。用systemctl status openmontage-agent查看。
  • agent 日志里有没有报连接错误。重点看ConnectionError、Timeout这类关键字。
  • 服务端能不能手动收到上报。直接在业务机执行curl -X POST -d @test_report.json https://monitor.example.com/api/report,如果返回ok,说明链路通,问题多半出在 agent 里的周期任务挂了。
  • 时间是否同步。agent 上报的数据如果带了错误的本地时间戳,服务端按时间排序后,很可能会把新数据当成旧数据过滤掉。务必在所有机器上启用 NTP 时间同步。

我自己遇到过一次很隐蔽的情况:业务机的/var/log目录被日志写满,agent 写自己的日志文件时失败,异常退出。而因为 systemd 配置里没有设置Restart=always,agent 挂了之后一直没有被自动拉起。这个坑的教训就是:agent 自身的日志目录一定要单独指定,并且不要和系统日志放同一个无上限的目录里。

5.2 采集数据有断档,图表出现空窗

图表断档排第一的原因,是采集间隔和存储精度不匹配。比如 agent 30 秒上报一次,但查询图表时按 1 分钟聚合,如果某个点刚好在聚合窗口边界处缺失,就会出现小断档。我的处理方式是在写入数据时做“时间戳对齐”:把记录时间按 30 秒取整(整分或整半分),这样同一时间窗口内的数据必然落在同一个坐标上,聚合时不会因为几毫秒的偏差导致空窗。

另一个常见原因是业务机的防火墙拦了 POST 请求。有些发行版默认防火墙只放行 80/443,如果你的 API 跑在 8000 端口,记得在防火墙里放行,或者在跳板机前置 Nginx 做反向代理,让 agent 都走 443 端口出去。

5.3 磁盘监控显示数值与实际对不上

这类问题大多是因为采集端用了不同的磁盘单位。psutil 返回的字节数要除以 1024/1024 转成 MB/GB,某些自定义脚本里df -h输出的是人类可读格式,混在一起比较时数值就会非常诡异。我的解决办法是:统一在采集端把所有指标都转成标准单位(内存和磁盘用字节,CPU 用百分比),存储层不关心单位,只存数值;前端展示时再根据指标类型决定格式化方式。这样虽然多了一步转换,但整个系统对单位的处理就清晰了。

5.4 告警恢复了,但没有收到恢复通知

恢复通知听上去是小事,真正出问题才知道难受。最开始我的告警规则只检测越界,不检测恢复。后来发现机器磁盘满了告警,你清完日志,系统不会再发一条“已恢复”,如果你没盯着面板,根本不知道问题是否已经解决。为此我在alerter.py里加了状态机逻辑:每条规则都有一个当前状态,当指标从越界恢复为正常时,自动生成一条recovered类型记录。前端用绿色标识恢复状态,Server 酱通知里也会带一句“XX 指标已恢复”。这样整个告警闭环才算完整。

5.5 常见问题速查表

问题现象优先排查项处理手段
机器显示离线agent 进程、心跳日志配置 systemd 自动重启
上报失败API 地址、防火墙、证书在业务机手动 curl 测试
图表空窗时间戳对齐、聚合精度统一取整 30 秒
告警轰炸冷却期、聚合逻辑设置 cooldown、合并同类告警
没收到恢复通知规则状态机增加恢复判定逻辑
磁盘数值异常单位换算统一存储字节单位

6. 从监控延伸到自动化运维的几条路子

6.1 把告警动作从“通知”升级成“修复”

整个系统跑顺之后,你会发现最有价值的不再是监控图表本身,而是它带来的自动化想象空间。我现在已经在部分生产机器上,把“磁盘使用率超过阈值”的告警直接对接到一条自动清理命令,比如删除超过 30 天的滚动日志备份。具体做法是在告警规则里加action字段,用一个执行器模块去调用目标机器上的预置脚本。当然,自动操作必须谨慎,我会在脚本里做好白名单限制,只允许删指定目录下指定模式的文件,并且每次执行都要留下日志审计记录。

6.2 把采集数据变成日报和周报

数据积累一段时间后,我加了一个小报表模块:每天凌晨汇总各机器的平均负载、磁盘增长趋势、告警次数排行,生成一份 HTML 日报。这个需求用 SQL 就能轻松完成,比如“过去 24 小时 CPU 平均超过 50% 的机器 Top 10”这类视图。日报的价值不只是给运维自己看,还可以发给团队或客户,作为服务稳定性的客观依据。相比拿着截图一张张贴到聊天记录里,自动报表让人省力太多。

6.3 与日志平台、内网穿透组合使用

如果手头机器上有现成的日志收集服务,OpenMontage 的自定义脚本里可以直接把日志查询结果作为指标带回来,比如“过去 5 分钟 ERROR 级别日志条数”。这样你不需要再单独登录日志平台,就能在监控面板看到应用级健康度。它更像是一个胶水层,把你已有的各种零散工具粘在一起。很多像我一样做小规模运维的开发者,到最后真正需要的不是一套庞大平台,而是一个能按自己节奏演化、相互打通的小生态。OpenMontage 在我这边的形态就是这样,今天加一个指标,明天加一个自动动作,它始终不会喧宾夺主,但离开它之后,我又会觉得手里缺了点什么。

我个人实际用下来的体会是:别指望一个工具解决所有问题,也别一上来就设计一个三个月都搭不完的大系统。先用最简单的代码跑通采集、存储、展示这条主链路,用起来之后再根据真实痛点去迭代。很多最初以为必须要有的功能(比如复杂的权限管理、动态拓扑图),等真正用上之后才发现优先级很低;反而是一些不起眼的小细节(时间戳对齐、恢复告警、分组筛选)才是日常使用中真正能帮你省时间的部分。这套 OpenMontage 的架构和代码量都不大,但它教会我最重要的一件事:好的运维工具不是功能多,而是链路清晰、自由可控、能随时按自己的需求去改。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进