先交代一句项目代号只有rea三个字母我第一次在代码仓库里看到这个目录名时第一反应是有人提交的时候把名字截断了。等真正跑了一轮才发现这是一个完整的轻量级资源环境分析工具的代号全称是 Resource Environment Analysis。它的核心作用只有一件事在本地服务器或开发机上快速采集 CPU、内存、磁盘、关键进程与服务存活状态自动判断是否存在异常最后输出一份既能给人看也能给程序用的报告并在超过阈值时触发告警。适合后端开发、运维工程师以及那些手头管理着几台云主机但不想为此搭一套完整监控平台的个人开发者。这年头监控方案很多从开源的完整链路到商业的一体化平台选择多到让人眼花。但很多小场景根本不需要那么重的设施。你可能只是想知道某台机器当前负载如何、磁盘还剩多少、某个服务是不是又挂了。这种需求用一个内部代号为rea的小工具就能覆盖。下面我按一个真实可落地的项目来拆解从设计思路到实现细节再到我在实际使用中踩过的坑一次性讲透。1. 项目整体设计与思路拆解动手写代码之前最值得花时间的是把定位想清楚。如果你的目标只是收集指标并展示那直接装现成方案就行。但如果你的目标是在一个受限环境里快速做一轮体检并且不想引入额外的中间件和存储那自己写一个薄工具就非常划算。rea 的存在理由就在这。1.1 为什么用rea这个代号内部工具起名讲究短、好敲、不撞车。rea 这个写法在目录里一眼就能辨认在 shell 里敲三个字母加回车也很顺手。全称 Resource Environment Analysis 是后来为了写文档硬凑的实际上我更喜欢把它理解成一个快速资源体检器。开发阶段用代号代替全称一方面节省沟通成本另一方面也避免过早被业务语义绑架。很多内部工具都是这么演进的刚开始只是一个测试脚本后面功能越加越多一个直观的短代号反而更容易在各个脚本间统一引用。1.2 工具形态单文件脚本优先我给 rea 定的第一个原则是尽量单文件尽量少依赖。为什么因为这类工具要往多台机器上分发依赖越少适配成本越低。用 Python 3 内置的 os、subprocess、json 模块做基础采集再引入一个 psutil 处理 CPU 和内存细节依赖面已经很小了。到了实在不能装 psutil 的极简环境还可以退回到解析 /proc 文件系统保证裸奔环境也能跑。很多人在这一步会纠结要不要封装成服务常驻要不要把历史数据存到数据库我的建议是分阶段先是按需运行的命令行工具等真的需要持续观察时再叠加服务化或定时任务。过早引入常驻进程会拉高维护成本而且单文件方式可以直接配合系统 crontab 使用效果并不差。1.3 数据源怎么选rea 的采集对象包括四类CPU 负载、内存占用、磁盘使用率、关键服务的存活状态。CPU 数据在 Linux 上主要读 /proc/loadavg 和 /proc/stat在 macOS 上则依赖 sysctl 和 ps。内存优先用 psutil.virtual_memory()它会直接给出 available 字段比单纯计算 free 靠谱得多。磁盘用 os.statvfs 或 psutil.disk_usage 都能拿到总量、已用、可用和挂载点信息。服务存活则用 subprocess 调用 systemctl status 或直接检查进程名是否存在。选择这些数据源的核心逻辑是不追求秒级实时不依赖外网服务所有数据都从本机操作系统获取。这样工具既可以在公网服务器上跑也可以在内网隔离环境里跑甚至在容器内只要 /proc 和 /sys 挂载正常也能拿到大部分指标。1.4 设计上回避的问题设计阶段最重要的决定其实是不做什么。rea 不去做长时间序列存储因为它解决的是此刻这台机器是否健康的问题rea 不去做 Web 面板因为大多数使用场景是 SSH 上去执行一下rea 也不去对接复杂的权限体系和多租户因为内部使用场景中往往只有一个操作系统用户。把这些不要做的边界划清楚整个项目就不会失控。2. 核心细节解析与实操要点很多看似简单的指标真正读取和分析的时候都有细节。这一部分我把 rea 逐项拆开讲重点说明为什么这样设计以及有哪些坑是需要提前避开的。2.1 CPU 负载的读取与换算CPU 负载不能只读一个瞬时值。我通常的做法是先读取 /proc/loadavg 里的 1 分钟、5 分钟、15 分钟三个数值再乘以 CPU 核心数来判断相对高低。举个例子一台 8 核机器负载 6.0 不算高但换到 2 核机器上负载 6.0 基本已经接近饱和。rea 在输出报告的时候会同时显示核心数并计算一个百分比load1 / cpu_count。关于 CPU 使用率百分比比较稳妥的方式是用 psutil.cpu_percent(interval1) 做一次短采样。注意这个接口第一次调用往往返回 0.0因为内部需要建立基线更准确的做法是先调用一次做预热隔一秒再取真实值。rea 这里会直接丢弃第一次返回的 0.0这也是很多人写采集脚本时容易忽略的小坑。2.2 内存指标available 才是有效值如果你习惯用 free 命令里的 free 列来评估内存余量很可能会误判。Linux 会把未使用的内存用作缓存这导致 free 列数值很低但系统其实并不缺内存。正确做法是看 available它估算的是在不触发交换的情况下还可分配的内存大小。psutil.virtual_memory().available 直接在大多数平台上给出这个值用它除以 total 得到剩余比例比任何人工计算都准。rea 在告警逻辑里用的是 available 比例而不是已用比例。比如当 available 低于 10% 才算紧张这个阈值在有 64GB 内存的机器上可以适当放宽到 5%但在 2GB 的小机器上 15% 就该告警了。阈值必须结合实际内存容量动态调整不能写死。2.3 磁盘检查inode 是个隐雷磁盘检查如果只看空间使用率你会漏掉另一类故障inode 耗尽。inode 是文件系统用来记录文件元数据的数据结构当小文件数量过多时即使磁盘空间还有剩余也会出现No space left on device的报错。特别是在跑日志、消息队列、临时缓存目录的机器上这种情况非常常见。rea 使用 psutil.disk_usage 获取空间数据使用 os.statvfs 获取 inode 数据。判断逻辑是双重的空间使用率超过设定的 warn 值或者 inode 使用率超过设定的 warn 值都会触发告警。另外分区检查不能粗暴地遍历所有挂载点需要过滤掉 proc、sysfs、devtmpfs 等虚拟文件系统只检查真实磁盘分区。这一条我放在配置项里允许按挂载点前缀排除。2.4 服务存活检查的节奏感服务存活检查的核心是如何避免误报。如果只用一句进程名在不在进程列表里判断那一个多实例服务的健康状态没法区分。rea 的做法是支持两层探测第一层是进程匹配第二层是端口探测。进程匹配用 pgrep -f 关键字端口探测用 Python 的 socket.connect_ex 尝试连接指定端口超时控制在 3 秒内。这里有几个要点探测超时不能太长否则一条服务挂了的告警会被拖到 10 秒之后但也不能太短否则服务正常启动但尚未监听端口时就会误报。端口探测的默认超时我设为 3 秒配合进程匹配一起使用误报率会明显降低。另外检查脚本自身不应该去探测那些暂时不需要的服务所有服务列表都应该显式配置避免噪音。2.5 输出格式与人机可读性输出报告不能只有给人看的内容还必须给程序留操作空间。rea 的标准输出是一份纯文本摘要可以直接在终端阅读同时也支持 --json 参数输出结构化数据。JSON 结构里有采集时间、主机名、平台信息、各项指标数值、健康状态和告警列表这样后续不管是接入日志采集、上报监控系统还是做自动化触发都非常方便。纯文本摘要则追求一眼能看懂。每条告警会单独列出正常项用普通文本异常项会在行首打上有问题标记。不需要花哨的图表渲染因为工具的目标是快速告知结论。3. 工具选型解析与实操步骤下面我给出一个可以直接复刻的 rea 实现。这里选用 Python 3 作为主语言原因是跨平台兼容性好、内置库丰富、在服务器上基本是标配。如果你所在的机器连 Python 3 都没有那 bash 也可以实现大部分功能但输出结构化数据和阈值计算会比较别扭。3.1 环境准备我建议在虚拟环境中安装依赖避免污染系统 Python。不过在实际部署时我发现很多用户喜欢直接用系统环境跑因为只需要 psutil 这一个第三方库安装成本很低。下面是核心命令python3 -m venv venv source venv/bin/activate pip install psutil装好后把 rea 脚本放在 /usr/local/bin/ 或者任意 PATH 目录里并赋予可执行权限chmod x rea.py sudo ln -s $(pwd)/rea.py /usr/local/bin/rea到这里rea 已经可以用。3.2 核心代码结构与关键函数rea 的代码结构分成四块参数解析、数据采集、分析告警、输出报告。我用一个简化版来说明核心实现你可以直接拿它当底子继续扩展。首先是采集模块import os import json import time import socket import subprocess from datetime import datetime try: import psutil except ImportError: psutil None def get_cpu_info(): if psutil: cpu_count psutil.cpu_count() load1 float(os.getlogin()) if False else None # 从 /proc/loadavg 读取负载 with open(/proc/loadavg, r) as f: parts f.read().split() load1, load5, load15 float(parts[0]), float(parts[1]), float(parts[2]) cpu_count os.cpu_count() or 1 percent None if psutil: psutil.cpu_percent(intervalNone) time.sleep(0.5) percent psutil.cpu_percent(intervalNone) return { cpu_count: cpu_count, load1: load1, load5: load5, load15: load15, load_percent: round(load1 / cpu_count * 100, 2), percent: percent }这里我特意保留了 /proc/loadavg 的读取即使没有 psutil 也能拿到负载数据。psutil 只负责 cpu_percent 这种需要跨平台计算的指标。然后是内存和磁盘def get_memory_info(): if psutil: vm psutil.virtual_memory() return { total: vm.total, available: vm.available, used_percent: round((vm.total - vm.available) / vm.total * 100, 2) } with open(/proc/meminfo, r) as f: lines f.readlines() total int([x for x in lines if x.startswith(MemTotal:)][0].split()[1]) * 1024 available int([x for x in lines if x.startswith(MemAvailable:)][0].split()[1]) * 1024 return { total: total, available: available, used_percent: round((total - available) / total * 100, 2) } def get_disk_info(exclude_prefixes(/proc, /sys, /dev)): disks [] for part in psutil.disk_partitions(allFalse): mountpoint part.mountpoint if any(mountpoint.startswith(p) for p in exclude_prefixes): continue usage psutil.disk_usage(mountpoint) st os.statvfs(mountpoint) inode_total st.f_files inode_used st.f_files - st.f_ffree disks.append({ device: part.device, mountpoint: mountpoint, fstype: part.fstype, total: usage.total, used: usage.used, free: usage.free, percent: usage.percent, inode_percent: round(inode_used / inode_total * 100, 2) if inode_total else 0.0 }) return disks内存部分同时支持 psutil 和 /proc/meminfo 两种取值路径这样在极简容器环境里也能运行。磁盘部分用 disk_partitions 枚举挂载点再配合 statvfs 拿到 inode 数据。注意 disk_partitions 的 allFalse 能过滤掉一部分等价分区但虚拟文件系统的过滤还是得自己做。服务检查的函数def check_service(name, portNone, process_patternNone): alive False if process_pattern: result subprocess.run( [pgrep, -f, process_pattern], capture_outputTrue, textTrue ) alive result.returncode 0 if port and not alive: sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) try: sock.connect((127.0.0.1, port)) alive True except OSError: alive False finally: sock.close() return {name: name, alive: alive, port: port}这个写法配合两级判定先匹配进程匹配不上再尝试连接端口。如果端口能通说明进程可能以其他名字在跑服务实际上还存活不算误报。3.3 告警阈值怎么配置我建议把阈值放到配置字典里避免散落在业务代码中。rea 的默认阈值如下指标正常区间警告值说明CPU 负载比例0~70%超过 85%按 1 分钟负载除以核心数计算CPU 使用率0~75%超过 90%短时采样需要先预热内存可用比例大于 20%低于 10%使用 available 计算磁盘空间使用率低于 75%超过 90%按真实磁盘分区统计inode 使用率低于 75%超过 90%小文件目录场景重点关注服务存活状态存活探活失败进程与端口双重判断阈值这里的思路是警告值不代表系统已经不可用而是提醒你该关注了。真正需要紧急处理的阈值可以更高比如磁盘空间超过 95% 才触发紧急告警。rea 里用 info 级别统一打印磁盘、内存和负载指标再用 warning 级别标注需要关注的项目最后用 critical 级别标注连接都失败的强告警项。3.4 输出报告代码报告输出分为两个函数一个生成 JSON一个生成纯文本。下面是一个关键的输出片段def generate_text_report(data): lines [] lines.append(f主机名: {data[hostname]}) lines.append(f采集时间: {data[timestamp]}) lines.append(fCPU核心数: {data[cpu][cpu_count]}, 负载: {data[cpu][load1]}/{data[cpu][load5]}/{data[cpu][load15]}) lines.append(fCPU负载比例: {data[cpu][load_percent]}%) lines.append(f内存: 已用 {data[memory][used_percent]}%, 可用 {data[memory][available] / 1024**3:.2f}GB) for disk in data[disks]: lines.append(f磁盘 {disk[mountpoint]}: 已用 {disk[percent]}%, inode {disk[inode_percent]}%) if data[warnings]: lines.append(警告项:) for w in data[warnings]: lines.append(f - {w}) return \n.join(lines)JSON 输出的结构可以保持同样的字段名方便下游解析。整体跑完后的命令行调用方式是rea --text rea --json如果想把告警推送出去可以在命令行加一个 --webhook 参数把 JSON 内容 POST 到指定地址。要不要接入第三方通知平台取决于使用环境这里点到为止。需要说明的是内网环境推送时要考虑这个地址本身是否可达否则会引入新的故障点。3.5 接入定时任务完成单个命令行的采集之后把它接入定时任务是最自然的下一步。假定无人值守的服务巡检频率是每五分钟一次crontab 配置如下*/5 * * * * /usr/local/bin/rea --json --output /var/log/rea/report.json这里有两个注意点--output 指定的目录要事先建好脚本里不会自动创建追加告警推送的话为避免每次收集都推送应在报告里过滤出存在 critical 级别的项再触发推送。我在实际部署时还会在 crontab 里加一个简单的锁避免上一轮任务还没跑完下一轮就启动了*/5 * * * * flock -n /tmp/rea.lock -c /usr/local/bin/rea --json --output /var/log/rea/report.jsonflock 是 Linux 上很好用的短小工具如果没有可以改用 mkdir 锁或用 Python 代码内部做文件锁。核心目的只有一个保证同时只有一个 rea 进程在执行采集避免高负载时多个进程互相打架。4. 常见问题与排查技巧实录共用代码跑起来并不难难的是边缘情况。我把实际运行中遇到的典型问题整理成一个速查表方便你快速定位。症状可能原因排查方法第一次调用 cpu_percent 返回 0.0接口需要建立基线先调用一次sleep 后再取真实值内存告警一直触发但系统实际很流畅误用了 free 而不是 available改用 MemAvailable 或 psutil.virtual_memory().available磁盘使用率正常但创建文件时报磁盘满inode 耗尽检查 df -i以及 os.statvfs 的 f_files 与 f_ffree服务明明在运行检测结果却是不存活进程匹配关键字不准确先用 pgrep -f 测试关键字如果服务监听在非 127.0.0.1 上端口探测也要换成对应地址crontab 里跑出来的 JSON 为空PATH 环境不一致在 crontab 中使用绝对路径并在脚本开头重新装载环境变量脚本里中文字符输出乱码服务器 locale 非 UTF-8脚本头部声明 # -- coding: utf-8 --终端用 UTF-8 字符集或干脆全英文输出4.1 磁盘分区重复识别在一次部署中我发现同一块磁盘的不同挂载点被重复统计导致磁盘使用率看起来偏高。原因在于 disk_partitions(allFalse) 也会返回多个 bind 挂载点。解决方法是增加一个 deduplicate 逻辑同一设备只统计最外层的挂载点。判断依据是挂载点路径更短的优先同时排除掉 /proc、/sys、/dev 等虚拟路径。代码里加一个简单的去重mount_map {} for part in psutil.disk_partitions(allFalse): if part.device in mount_map: existing mount_map[part.device] if len(part.mountpoint) len(existing.mountpoint): mount_map[part.device] part else: mount_map[part.device] part disks [get_usage(p) for p in mount_map.values()]4.2 告警风暴的抑制如果某台机器负载一直居高不下而 rea 又设了每 5 分钟跑一次你可能会收到大量重复告警。我建议在配置里加一个连续触发次数的概念只有当指标连续 N 次超过阈值时才推送告警。N 一般设为 2 或 3。这样能滤掉瞬时抖动又不会让告警滞后太久。4.3 时区与时间格式服务器时间如果配置不对JSON 里的 timestamp 会误导排障。建议消息体里使用带时区的 ISO 8601 格式比如 2025-06-20T10:30:0008:00。Python 3 可以直接用 datetime.now().astimezone().isoformat()不需要额外依赖。4.4 没有 psutil 时的降级方案某些精简容器里没有 psutil也没法用 pip 安装时需要走降级路径。rea 在实现上已经做了这种降级CPU 负载读 /proc/loadavg内存读 /proc/meminfo磁盘用 os.statvfs。此时唯一缺失的是 CPU 使用率百分比但负载比例已经能反映趋势基本够用。4.5 报告文件滚动归档时间一长/var/log/rea/report.json 会越来越大。建议按天滚动例如文件名加日期戳。rea 里没有内置日志滚动我一般用外部 logrotate 配置或者在 crontab 命令中加一段 find 清理find /var/log/rea/ -name report-*.json -mtime 30 -delete这样一个月前的报告自动清理不会占用太多磁盘。5. 扩展方向与使用心得rea 这个工具接下来可以往三个方向扩展。第一个方向是持续记录与历史对比把每次采集的 JSON 按时间戳存档然后让 rea 在输出时自动对比昨天同一时刻的指标给出变化趋势提示。这样就能察觉到内存使用率正从 50% 稳步爬到 80%这类渐进式恶化问题。第二个方向是容器环境适配通过判断是否在容器内来决定读取哪些文件例如使用 /sys/fs/cgroup 下的数据来近似估算容器级用量。第三个方向是引入配置分发把服务列表、阈值参数和 Webhook 地址放进一个外部配置文件这样不同的机器可以套用不同的策略而不需要改代码。就我个人这几轮用下来的体会这类轻量巡检工具最大的价值不是监控本身而是提供了一种低成本的感知能力。你不用登到每台机器上去敲 free、df、systemctl status一条命令就能把所有关键信息汇总起来。内部工具保持短平快反而更实用不用一开始就想象它能替代重型监控系统。如果哪天真的需要采集海量历史数据再把数据往正经时序库里导也不迟脚本层面留好 JSON 输出格式就行。最后分享一个小技巧把 rea 的文本输出粘进自己的 SSH 登录欢迎信息里每次登录服务器第一眼就能看到当前机器是不是健康。这比任何面板都直观也几乎零成本。我在自己的机器上就是这样配置的一个小工具改变了日常巡检习惯。