ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

零侵入监控:node_exporter、mtail 与 Prometheus、Grafana 组合实战

零侵入监控:node_exporter、mtail 与 Prometheus、Grafana 组合实战 1. 先统一认知node_exporter、mtail、Prometheus、Grafana 各管哪一段我见过很多团队做监控第一反应是“先装一套 Prometheus Grafana”装上之后发现只有 CPU、内存、磁盘这些机器指标业务到底跑得好不好完全看不见。后来有人开始折腾埋在代码里的 metrics 接口但老系统根本不敢动发一次版可能要排两周。直到我把 mtail 加进来才真正把这套方案拼完整。这套组合里四个组件各自解决的问题非常清晰node_exporter 负责机器层指标mtail 负责从日志中提取业务指标Prometheus 负责采集、存储、计算和告警Grafana 负责把数据变成能直接看懂的画面。它们不是替代关系而是流水线关系。1.1 一条请求日志到一张监控图的全过程拿一个典型的订单系统举例。某次用户下单后端服务打出一条日志2024-06-01 12:00:01 INFO order 1001 user 501 amount 99.80 status success cost_ms 120 region cn如果没有 mtail这条日志要么被 logstash 收走全文存储要么躺在本机没人看。引入 mtail 之后它做的事情是实时读日志文件用 mtail 程序里写的正则去匹配每一行把命中的字段提取出来累加到对应的指标里。比如匹配到statussuccess就把order_requests_by_status{statussuccess}这个计数器加一匹配到cost_ms120就把耗时观测值丢进直方图。mtail 本身会暴露一个 HTTP 端口默认3903Prometheus 每隔 15 秒来拉一次指标。数据进到 Prometheus 之后你可以写 PromQL 就算出五分钟内的 QPS、成功率、P95 耗时然后在 Grafana 里画曲线、做告警。整个过程不需要改一行业务代码这就是它最值钱的地方。node_exporter 则在另一条线上工作每台机器上跑一个进程采集 CPU、内存、磁盘、网络、文件系统、进程数、systemd 服务状态等几百个指标同样暴露 HTTP 端口Prometheus 来拉。所以最终你在 Grafana 上看到的监控大屏左手是机器负载曲线右手是业务 QPS 和错误率。这两类数据原本各自孤立现在因为都在 Prometheus 里就能放在一起做关联分析。1.2 为什么不是 ELK、不是埋点、不是 pushgateway很多人会问日志监控我有 ELK 了为什么还要 mtail这里的差异是本质的。ELK 做的是全文检索和日志归档是把日志原文存下来供查询而 mtail 做的是流式聚合日志读完就丢只保留统计结果。同样是 100 万条日志ELK 可能要占几百 MB 存储mtail 最终只贡献几个时间序列。Elasticsearch 再快查一条日志还是要秒级而 Prometheus 一条rate查询出来是毫秒级。二者不是二选一更常见的是并存ELK 留原文排障mtail 出指标做监控。为什么不用在业务代码里直接埋点暴露 metrics埋点当然是正路但现实是很多遗留系统、第三方组件、甚至是运维自己写的脚本根本不会给你留 metrics 口子。发版一次的成本非常高等埋点上线事故可能已经发生了。mtail 的价值在于零侵入、快速接入十分钟就能把新业务日志变成指标。它不完美但它能立刻解决问题。为什么不用 pushgatewayPrometheus 默认是 pull 模型也就是 Prometheus 主动去各 exporter 上抓取。一旦引入 pushgateway就变成了被动的 push 模型网关本身成为单点和数据造假的重灾区还要额外处理指标过期的问题。对于“机器负载 业务监控”这个场景没有理由用 push 方案。我整理过一张表方便你对照理解这四个组件到底谁负责什么组件数据来源默认端口关键指标示例常见部署方式node_exporter主机操作系统9100node_load1、node_cpu_seconds_total、node_memory_MemAvailable_bytes每台机器一个mtail业务日志文件3903从日志中统计的请求数、错误数、耗时等每台业务机器一个Prometheus各 exporter 的 /metrics9090采集到的所有时序数据中央或按机房部署GrafanaPrometheus API3000展示层数据不存业务时序单实例或集群一句话总结node_exporter 管机器mtail 管业务Prometheus 管存储和算数Grafana 管让人看懂。2. 从空机器开始四个组件的安装、注册与联调先说明我不打算讲 Docker Compose 一把梭。生产环境里我习惯用二进制 systemd 管理原因很简单二进制部署排错直观systemd 可以控制启动顺序、日志采集、崩溃重启也方便跟配置管理工具整合。如果你只是想本地快速验证手动起几个进程也完全没问题。2.1 目录与运行账号规划在动手之前先把目录和账号规划好。我见过最乱的部署就是所有东西一股脑 root 跑在/root下权限混乱不说exporter 一旦被利用攻击者直接拿到 root 权限。所以第一件事是建一个专用账号sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus然后规划目录我习惯用/data作为数据盘挂载点监控数据单独放在里面避免和系统盘抢空间sudo mkdir -p /data/prometheus sudo mkdir -p /data/mtail sudo mkdir -p /opt/{prometheus,node_exporter,mtail,grafana} sudo chown -R prometheus:prometheus /data/prometheus /data/mtail2.2 node_exporter 与 Prometheus 的 systemd 化从官方 releases 页面下载二进制包解压后把可执行文件放到/opt对应目录即可。node_exporter 启动参数通常不需要纠结默认的采集项已经覆盖大部分场景按需开启 systemd 采集器也很有用[Unit] DescriptionNode Exporter Afternetwork.target [Service] Userprometheus Groupprometheus Typesimple ExecStart/opt/node_exporter/node_exporter \ --web.listen-address:9100 \ --collector.systemd \ --collector.processes Restartalways RestartSec5 [Install] WantedBymulti-user.targetPrometheus 的 systemd 服务类似但存储路径和配置文件路径必须提前给好[Unit] DescriptionPrometheus Afternetwork.target [Service] Userprometheus Groupprometheus Typesimple ExecStart/opt/prometheus/prometheus \ --config.file/etc/prometheus/prometheus.yml \ --storage.tsdb.path/data/prometheus \ --web.listen-address:9090 Restartalways RestartSec5 [Install] WantedBymulti-user.target接下来是prometheus.yml这是整套监控的入口。我给出一个最小可用的版本global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: prometheus static_configs: - targets: [127.0.0.1:9090] - job_name: node static_configs: - targets: - 10.0.0.10:9100 - 10.0.0.11:9100 - job_name: mtail static_configs: - targets: - 10.0.0.10:3903这里的scrape_interval决定数据采样粒度15 秒是默认值监控业务波动比较大的系统可以调到 5 秒但要注意磁盘写入量会成倍增加。我先用 15 秒起步。2.3 mtail 与 Grafana 的接入mtail 的启动命令里-progs指定程序目录-logs指定要监控的日志文件可以带通配符。注意这里用的是 glob 表达式日志轮转时老文件和新文件都会被匹配到[Unit] Descriptionmtail Log Exporter Afternetwork.target [Service] Userprometheus Groupprometheus Typesimple ExecStart/opt/mtail/mtail \ -progs /etc/mtail \ -logs /data/logs/order/*.log \ -port 3903 Restartalways RestartSec5 [Install] WantedBymulti-user.target/etc/mtail目录下放 mtail 程序文件文件后缀通常是.mtail例如order.mtail。这个目录的内容在下一章详细讲。Grafana 我同样用二进制部署解压后有个bin/grafana server用 systemd 拉起来默认监听 3000 端口。首次登录使用admin/admin进去之后先改密码然后添加 Prometheus 数据源。数据源 URL 填http://127.0.0.1:9090访问方式选 ServerAccess 选默认的 Browser 或 Server 都可以我建议 Server 模式避免浏览器跨域问题。数据源保存之后你可以在 Explore 页面输入up查一下如果返回了多个值为 1 的序列说明采集链路已经通了。2.4 第一轮联调验证我每次装完不会急着看 Grafana而是先用 curl 把三层链路分别验证一遍node_exporter 是否存活curl -s http://localhost:9100/metrics | grep node_load1mtail 是否存活curl -s http://localhost:3903/metrics | grep order_Prometheus 是否抓到了目标curl -s http://localhost:9090/api/v1/targets第 3 步非常关键。很多人配置了scrape_configs但忘了开防火墙端口结果 Prometheus 侧 Target 一直显示 Down。如果你发现 node_exporter 和 mtail 的/metrics都能手动访问但 Prometheus 抓不到优先检查安全组和本机防火墙对 9100、3903 端口的放行规则。所有服务启动之后用systemctl status确认进程都处于 running 状态再用journalctl -u mtail -f看 mtail 日志确认它有没有真的在读业务日志文件。很多时候程序文件写错mtail 启动并不会报错只是默默不产生任何指标这一步踩坑概率非常高。3. 把业务日志变成指标mtail 程序设计与调试mtail 的价值全在程序文件里。它不是什么复杂语言本质上就是“正则匹配 计数器累加”。但越简单的东西越容易写糙我见过太多人把 mtail 程序写成垃圾正则堆砌跑两天内存飙升。所以这一章我不只给示例还会把匹配模型讲清楚。3.1 一个能直接抄的 mtail 程序假设订单日志的格式稳定成这样2024-06-01 12:00:01 INFO order 1001 user 501 amount 99.80 status success cost_ms 120 region cn 2024-06-01 12:00:02 INFO order 1002 user 502 amount 199.00 status failed cost_ms 80 region us在/etc/mtail/order.mtail里写# 请求总量按状态区分 counter order_requests_total counter order_requests_by_status by status # 销售额累计 counter order_amount_total # 耗时分布 histogram order_cost_ms # 最近一次耗时方便看瞬时值 gauge order_last_cost_ms # 核心匹配块 /status(?Pstatus\w) cost_ms(?Pcost\d)/ { order_requests_total order_requests_by_status[$status] order_cost_ms $cost order_last_cost_ms $cost } /order (?Pid\d) .* amount(?Pamount[\d.])/ { order_amount_total $amount }这里有几个关键点需要说清楚。第一个正则里(?Pstatus\w)是命名捕获组mtail 会把捕获到的内容作为标签值。order_requests_by_status[$status]的意思就是按status这个标签维度累加计数器。日志里出现statussuccess就往statussuccess的序列上加一出现statusfailed就生成另一个序列。如果在 Prometheus 里查order_requests_by_status你会看到按 status 分组的多个时间序列。第二个正则匹配整行里的amountxx这里我用了.*跳过中间其他字段但注意不要滥用。日志行越长正则的匹配成本越高后面我会专门讲性能问题。histogram是 mtail 里最容易理解错的一个类型。它并不是“存一个值”而是把观测值丢进预设的桶里。你不需要管桶边界具体是多少只要把order_cost_ms $cost写上去mtail 会自动生成order_cost_ms_bucket、order_cost_ms_sum、order_cost_ms_count这些序列。之后在 Grafana 里用histogram_quantile算 P95 时依赖的就是这套桶数据。3.2 mtail 的匹配模型与指标语义理解 mtail要先理解它的运行方式。每个日志行进来mtail 不会像一个普通脚本那样从头到尾顺序执行所有规则而是把所有正则片段编译成一个状态机。程序文件里的内容会被拆成若干条规则每条规则只关心自己需要的那个片段。比如我上面写的第一个规则只匹配status... cost_ms...这两个字段第二个规则只匹配amount...。日志行先走第一条规则如果匹配上了执行大括号里的累加逻辑然后再尝试第二条规则匹配上了则继续执行。换句话说同一行日志可以被多条规则命中多次贡献指标。这也是为什么我在第二个正则里没有再去匹配 status而是单独处理 amount避免重复累加。指标类型上mtail 提供三种类型语义典型操作counter只增不减的计数器适合请求数、错误数或gauge可增可减的瞬时值适合队列深度、最近耗时、在线人数直接赋值histogram观测值分布适合耗时、包大小把观测值丢进桶设计 mtail 程序时最容易犯的错误就是把只增不减的量写成 gauge。比如“当前排队数”应该是 gauge因为它会降下来但“累计处理请求数”必须是 counter因为它是单调递增的。一旦类型选错你在 PromQL 里用rate()算出来的速率就会忽正忽负画出来的曲线没法看。3.3 正则写不好时mtail 会怎么拖后腿正则匹配看起来没什么技术含量但 mtail 是实时处理大量日志的性能问题很快会在生产环境爆出来。第一不要用灾难性回溯的正则。比如.*.*.*嵌套、大量\d的交替分支在极端情况下会让匹配卡住日志处理吞吐直接归零。我见过一个团队在 mtail 里写了个非常复杂的 URL 解析正则某天日志里出现一条超长异常 URLmtail 进程 CPU 立刻打满所有业务指标停滞。第二能用前缀匹配就不用全量扫描。mtail 状态机本身会优化但如果你写的模式太宽泛比如/(.*)/那等于每条日志都要做大量无用功。我通常的做法是先定位一个有辨识度的前缀比如order_id、status再在附近提取字段。第三命名捕获组是开销但值得。使用(?Pname...)会产生标签维度每一条日志都要把这个值写入内存里的标签集合。低基数标签完全没问题高基数标签问题在第 5 章细说。写完程序之后怎么调试我建议先用 mtail 的-v 2参数把它跑起来让它实时读日志然后在/metrics页面盯着看指标有没有增长/opt/mtail/mtail -progs /etc/mtail -logs /data/logs/order/*.log -port 3903 -v 2如果你发现指标一直为 0先用tail -f看业务日志里有没有新行再用一个最简单的测试程序比如只匹配INFO一行看能不能出数。逐层缩小范围十次里有八次是日志格式和正则不匹配。4. 让业务指标和机器负载出现在同一张图查询设计与告警规则组件全部跑起来之后重头戏才刚开始。Prometheus 里现在有两类数据一类来自 node_exporter是node_开头的机器指标另一类来自 mtail是order_开头的业务指标。每一个指标都已经在时序库里了接下来就是问它问题。4.1 业务侧三个最常用的 PromQL我自己的习惯是给任何业务先做三张图流量、成功率、延迟。对应到 PromQL 分别是下面三个。第一个是 QPS也就是每秒请求数sum(rate(order_requests_total[5m])) by (instance)rate()计算的是每秒平均增量5 分钟窗口可以平滑掉毛刺。前面我说 order_requests_total 是 counter 类型PromQL 里所有对 counter 计算速率都必须用rate()或increase()不要直接拿 counter 原始值画折线图否则你看到的是一条永远向上的直线没有参考意义。第二个是成功率。注意错误率的分子分母都要用 rate否则窗口不一致会算出奇怪的值sum(rate(order_requests_by_status{statussuccess}[5m])) by (instance) / sum(rate(order_requests_total[5m])) by (instance)第三个是 P95 延迟。这个依赖 histogram 数据_bucket后缀的序列记录了累计分布histogram_quantile(0.95, sum(rate(order_cost_ms_bucket[5m])) by (le) )histogram_quantile是 Prometheus 专门算分位数的函数le是 histogram 的桶上界标签。这个公式在所有 Prometheus 环境里都是通用的你只需要把order_cost_ms换成自己的 histogram 指标名。在 Grafana 里我把这三个查询分别做成三个面板然后再加上一个总览面板把三个指标叠在同一张图上。面板时间范围选“最近 6 小时”刷新频率 30 秒基本就能覆盖日常巡检需求。4.2 把 node_load1 和业务 QPS 放在一起看单纯看业务 QPS 只能说明“业务忙不忙”机器负载放在另一张图上又不容易关联。真正的价值在于把两者叠加。我在 Grafana 里建了一个叫“负载与吞吐对照”的面板左边 Y 轴是node_load1右边 Y 轴是 QPS# 左轴归一化后的机器负载 node_load1 / on(instance) count(node_cpu_seconds_total{modesystem}) by (instance) # 右轴订单 QPS sum(rate(order_requests_total[5m])) by (instance)node_load1 / CPU核心数这一步很重要。同样负载 4在 4 核机器上是满载在 32 核机器上只是小浪花。归一化之后负载超过 1.0 就说明 CPU 已经饱和。count(node_cpu_seconds_total{modesystem}) by (instance)的作用就是算出每台机器的 CPU 逻辑核心数。两张曲线叠在一起之后你能看到几种典型状态业务 QPS 上升负载同步上升说明当前是正常的流量驱动扩容理由充分。业务 QPS 下降负载仍然高位说明有别的进程在吃 CPU或者系统在做大量 IO 等待、内存回收需要查非业务进程。负载很低但业务错误率上升说明问题不在资源而在依赖调用、网络、数据库连接数等外部因素。负载打满QPS 反而下跌典型的性能瓶颈请求被阻塞了。这一张对照图比分开看十张图都管用。我后来做容量评估时都在它上面拉一条参考线看到某条业务曲线逼近负载曲线就该提前扩容了。4.3 告警规则机器负载阈值要归一化业务告警要加护栏告警规则写在 Prometheus 配置文件里用groups、rules组织。我给出两个最值得抄的规则。第一个是机器负载告警注意阈值不是固定值而是归一化后的负载groups: - name: instance_alerts rules: - alert: HighLoad1m expr: | node_load1 / on(instance) count(node_cpu_seconds_total{modesystem}) by (instance) 0.8 for: 10m labels: severity: warning annotations: summary: 实例 {{ $labels.instance }} 负载持续偏高for: 10m表示负载连续 10 分钟超过阈值才告警过滤掉瞬时尖峰。告警的意义不是让你半夜爬起来重启机器而是让你在问题即将影响业务之前介入。第二个是业务错误率告警。这里我加了一个护栏条件如果请求量太低错误率的绝对值没有意义。比如 1 分钟只有 2 个请求失败 1 个错误率 50%但这不代表系统挂了。所以用and把 QPS 下限带上- alert: OrderErrorRateHigh expr: | ( sum(rate(order_requests_by_status{statusfailed}[5m])) by (instance) / sum(rate(order_requests_total[5m])) by (instance) ) 0.05 and sum(rate(order_requests_total[5m])) by (instance) 10 for: 5m labels: severity: critical annotations: summary: 订单错误率超过 5%当前业务存在异常这套规则我用了很久最大的感受是机器负载告警要防“突然”业务告警要防“持续”。机器负载抖动剧烈适合用较长for窗口业务错误率只要持续几分钟不降往往预示着大问题窗口可以短一点但一定要有流量护栏不然低峰期误报能把你烦死。5. 这套监控在真实环境里容易踩的五个坑部署完成、面板画好、告警配完这只是开始。真正让人头疼的是跑起来之后的那些“看起来没毛病但数据莫名其妙”的问题。我在多个环境里用过这套组合下面五个坑是出现频率最高的每一个都花过我不少时间去定位。5.1 日志格式一改历史对比直接失真业务日志是研发说了算的东西他们今天在status前面加一个字段明天把cost_ms改成cost你的 mtail 程序完全不知情。正则匹配不到新格式指标就会更新频率骤降甚至归零。更麻烦的是如果新旧格式混杂在同一个日志文件里统计出来的数据既包含新格式的贡献又包含旧格式的缺失趋势图会变得极其诡异。我的做法是给 mtail 程序加一个“兜底规则”如果一行日志匹配不到任何业务规则就累加到一个unmatched_lines计数器上。counter unmatched_lines_total // 放在程序末尾 /.*/ { unmatched_lines_total }注意这个规则必须小心因为每条日志都会命中这个.*所以它只能作为兜底不能跟其他规则混在一个计数口径里用。我在 Grafana 上给这个指标单独画了一个面板一旦它开始持续增长就说明日志格式变了马上去找研发对齐。5.2 标签维度失控从几百个序列涨到几十万mtail 的标签机制用起来很爽但也很容易失控。order_requests_by_status by status这种低基数维度没问题status 总共就那么几个值。但如果你写的是order_requests_by_order_id by order_id每天产生几十万订单这个时间序列量就会爆炸式增长。Prometheus 单机存储是本地 TSDB时序数量超过一定量级之后查询性能和磁盘占用都会恶化。标签高基数是整个 Prometheus 生态最经典的问题没有之一。我的经验是mtail 程序里只对枚举值类字段做标签分组比如 status、region、method、error_code。像 order_id、user_id、request_id 这种每行都变化的值绝对不要作为标签。如果你想看某个大客户的量可以单独用日志里的统计值在业务侧聚合或者另建一个按客户维度汇总的文件而不是让 mtail 为每个客户生成一个时间序列。5.3 日志轮转方式不对指标被重复累加业务日志通常按天或按大小轮转Linux 下常见的方式是logrotate。轮转策略有两种copytruncate和create。前者是把原文件复制一份然后清空后者是把原文件改名再新建一个同名文件。mtail 在监控日志文件时会记住自己读到了哪个位置。如果使用copytruncate文件被清空后mtail 可能无法正确感知会从偏移 0 开始重新读一遍导致计数器被重复累加QPS 看起来突然翻倍。反之使用create方式轮转新日志进入新文件mtail 会跟踪到重命名的旧文件和新文件通常表现更稳定。我在生产里被这个坑折腾过一次。某天凌晨订单 QPS 曲线突然出现一个两倍尖峰持续了十几分钟又恢复排查了很久才发现是当天凌晨 0 点日志轮转触发造成的重复读取。后来我把日志轮转配置统一改成create方式这种尖峰再也没有出现过。如果你用copytruncate有历史原因改不了至少要把这个问题写进排障手册。5.4 mtail 进程死了业务监控直接瞎眼node_exporter 挂了还会有机器层的基础指标Prometheus 自身也能监控up。但 mtail 是业务指标的入口它一挂业务曲线立刻变直线而你很可能根本不知道是业务挂了还是监控挂了。这很危险因为监控系统的可用性本身也需要被监控。所以我在告警规则里永远是先加一条- alert: MtailDown expr: up{jobmtail} 0 for: 2m labels: severity: critical annotations: summary: mtail 服务不可用业务监控数据中断另外同一个 mtail 实例如果负责解析多个日志文件程序文件写错了也不会影响进程存活只是某些指标不出来。我在实际运营里会设置一个“心跳文件”让业务定时往一个固定文件里写一行heartbeatmtail 把它变成一个 counter。如果在 Grafana 上看到心跳指标停了业务很可能是真的出了问题如果连这个文件都没人写了那就要区分是业务日志停了还是 mtail 没读到。5.5 本地存储的容量账要提前算Prometheus 本地存储是自带压缩的但时序数据的增长依旧很吓人。一个中等规模的集群机器指标加业务指标一天可能产生 2~3 GB 数据。按默认 15 天保留期算30 GB 到 45 GB 是起步。如果业务指标写得多一个月的保留期可能就需要上百 GB 磁盘。很多人一开始没在意直到 Grafana 查询变慢才发现 TSDB 所在磁盘快满了。我的建议是部署第一天就单独挂一块数据盘给 Prometheus并预留出 1.5 倍到 2 倍于预期的空间。--storage.tsdb.retention.time参数可以控制保留期我一般设 15 天太长的历史数据留给远程存储或对象存储去解决本地只保留能回答“最近两周发生了什么”的数据。还有一个容易忽略的点scrape_interval从 15 秒改成 5 秒后数据点是原来的 3 倍存储增长也是 3 倍。如果你对实时性没有那么苛刻15 秒完全够用。监控不是采样频率越高越好频率越高磁盘和查询压力越大而你要回答的核心问题往往 15 秒级别的数据就能覆盖。最后给一点个人经验。这套方案第一次落地时不要追求大而全先从最小的闭环开始一台机器装 node_exporter一条业务日志配一个 mtail 正则一张 Grafana 面板把负载和 QPS 画出来一条告警规则覆盖机器高负载。跑一周确认数据可信、告警无噪声再慢慢加指标、加机器。监控的关键从来不是工具数量而是当你半夜被叫醒时能不能在三十秒内判断出“现在系统到底好不好”。把这条链路做扎实了Prometheus、Grafana、mtail、node_exporter 这四个组件才能真正成为你的值班拍档。
RELATED READING

延伸阅读

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