可观测性:给自己的系统装仪表盘
你运维时抱怨过的「这系统什么都看不见」,现在轮到你来还债:prometheus 指标、tracing 链路、慢 IO 日志,一次配齐。
学完这节你能做到
- 设计 forge 的指标体系:USE + RED 两套视角
- 接入 tracing,一条写请求全链路可见
- 搭出 Grafana 大盘与三条告警规则
还债的时刻到了
运维时你一定骂过某些系统:出问题只会吐一句含糊的日志,想看内部状态没有接口, 延迟高了不知道卡在哪一层,最后只能重启了事。「这系统什么都看不见」—— 这句抱怨你说过多少遍,自己心里有数。
现在你是那个「系统的作者」了。forgefs 在 L4 已经能挂载、能扛节点故障, 但此刻它对外就是个黑盒:P99 毛刺来了,你能回答「时间花在哪」吗?不能, 就轮到未来的运维(可能还是你)骂你。这节课把债还清:指标、链路、慢 IO 日志,一次配齐。
好消息是,这是整条路线里你最有主场优势的一课:别人是学着设计指标, 你是把十年看盘经验反着写进系统。
指标设计:USE + RED 两套视角
别上来就 emit 指标,先想清楚谁在什么场景下看什么。两套经典框架正好覆盖存储系统的两侧:
- USE(Utilization / Saturation / Errors) —— 看资源,回答「机器还有没有余量」。 对 forge:每块盘的带宽利用率、IO 队列深度(饱和度)、EIO 计数; 每个节点的内存 buffer 池占用、tokio 任务积压。
- RED(Rate / Errors / Duration) —— 看请求,回答「用户体验怎么样」。 对 forge:每类 RPC 的 QPS、错误率、延迟直方图;FUSE 层每类操作 (read/write/lookup)同样一套。
按这两套视角过一遍 forge 的架构,得到第一版指标清单(节选):
| 指标 | 类型 | 视角 | 你当年想看却看不到的 |
|---|---|---|---|
forge_rpc_duration_seconds | histogram,按 method/node 分 label | RED | 「慢在网络还是慢在盘」 |
forge_store_io_duration_seconds | histogram,按 read/write/fsync 分 | RED | 「fsync 到底多慢」 |
forge_store_disk_errors_total | counter,按 kind 分 | USE | 「坏盘前有没有前兆」 |
forge_rebuild_bytes_total / forge_rebuild_backlog_bytes | counter / gauge | USE | 「重建还要多久」 |
forge_raft_leader_changes_total | counter | RED | 「刚才是不是切主了」 |
forge_placement_version | gauge | — | 「各节点放置表一不一致」 |
两条设计纪律,都是你被烂指标坑出来的经验:
- 延迟必须是直方图,不是平均值。 平均延迟 1ms 的系统可以有 2 秒的 P99, 你被这种「均值很美」的大盘骗过。用 histogram,bucket 边界按对数分布铺到秒级。
- label 基数要管住。 按 method、node、盘分 label 没问题(几十个组合); 按 inode、客户端 IP 分就是灾难 —— 百万级序列能把 Prometheus 拖死, 这个事故你可能亲手救过。
metrics crate 与 prometheus exporter
Rust 侧用 metrics 门面 + metrics-exporter-prometheus,埋点一行,导出全自动:
use metrics::{counter, histogram};
pub async fn handle_put(&self, req: PutRequest) -> Result<PutReply, RpcError> {
let start = self.clock.now(); // 注意:走 Clock trait,模拟测试里也能跑
let result = self.do_put(req).await;
let elapsed = self.clock.now().duration_since(start);
histogram!("forge_rpc_duration_seconds", "method" => "put").record(elapsed.as_secs_f64());
if result.is_err() {
counter!("forge_rpc_errors_total", "method" => "put").increment(1);
}
result
}
exporter 在 forge-node 启动时拉起,直接暴露 /metrics:
use metrics_exporter_prometheus::PrometheusBuilder;
pub fn init_metrics(listen: SocketAddr) -> Result<(), MetricsError> {
PrometheusBuilder::new()
.with_http_listener(listen)
// 延迟 bucket:0.1ms 到 10s,对数铺开 —— P99 毛刺就藏在高位 bucket 里
.set_buckets(&[1e-4, 5e-4, 1e-3, 5e-3, 1e-2, 5e-2, 0.1, 0.5, 1.0, 5.0, 10.0])?
.install()?;
Ok(())
}
然后就是你闭着眼都会的部分:Prometheus 加一条 scrape job 抓所有 forge 节点,
15s 间隔。第一次 curl <你的节点>:9100/metrics 看到自己系统吐出的指标流,
这一刻和 L4 第一次 mount 成功同样值得截图。
tracing:一条写请求的全链路
指标回答「整体怎么样」,回答不了「这一个请求为什么慢」。 一次 FUSE write 在 forge 里要穿过:FUSE 层 → 客户端条带化 → 多个节点的 RPC → EC 编码 → forge-store 落盘 → fsync。任何一段都可能是那 200ms 的元凶。
tracing crate 用 span 把这条链串起来:
use tracing::{info_span, instrument, Instrument};
#[instrument(skip(self, data), fields(ino, offset, len = data.len()))]
pub async fn write(&self, ino: u64, offset: u64, data: &[u8]) -> Result<u32, FsError> {
let stripes = self.map_to_stripes(ino, offset, data)?;
for stripe in stripes {
// 子 span:每个条带的提交单独计时
self.commit_stripe(stripe)
.instrument(info_span!("commit_stripe", stripe_id = stripe.id.0))
.await?;
}
// ...
Ok(data.len() as u32)
}
跨节点的部分靠上下文传播:客户端把 trace id 塞进 tonic 请求的 metadata,
服务端取出来接上,于是一条链路横跨三台机器也是完整的一棵 span 树。
输出接 tracing-subscriber,开发时打人类可读日志,联调时导出到 Jaeger 看瀑布图 ——
「这 200ms 里 fsync 占了 180ms」一眼可见。
日常运行不可能全量记 trace(开销和存储都受不了),用你配采样时的老规矩: 慢的全留,快的抽样。forge 里做成一个简单的 tail 采样:span 树完成时 总耗时超过阈值(比如 50ms)则整树落盘到慢 IO 日志,否则按 1% 抽样。 慢 IO 日志是给三个月后深夜排障的自己留的现场。
P99 写延迟告警触发了。指标和 tracing 各自回答什么问题?
大盘与告警:把运维经验反哺给自己
Grafana 大盘怎么排,你比这节课有发言权。给 forge 1.0 的大盘定一个最小结构:
- 第一行,服务视角(RED):集群总 QPS、错误率、读/写 P50/P99 —— 值班的人 5 秒钟判断「有没有事」
- 第二行,资源视角(USE):每盘带宽与队列深度热力图、节点内存、EIO 计数
- 第三行,forge 特有状态:Raft leader 与切主次数、重建 backlog 与速率、放置表版本一致性
告警只配三条,宁缺毋滥 —— 你太清楚告警疲劳是怎么把人训练成「看到告警先划掉」的:
forge_rpc_errors_total错误率 5 分钟均值大于 1% —— 用户已经受影响,P1- 写 P99 大于 100ms 持续 10 分钟 —— 恶化趋势,工作时间处理
forge_rebuild_backlog_bytes持续 30 分钟不下降 —— 重建卡住了,冗余度在裸奔,这条最凶险
你收过最气人的告警是只有一句 "threshold exceeded" 的那种。给自己的三条告警各写一段 runbook:这条告警意味着什么、先看哪个面板、常见原因和处置命令。 写 runbook 的过程往往还能反过来发现指标缺口 ——「处置时我需要看 X」而 X 还没埋点。
review 的重点不在代码而在设计遗漏:拿着你自己的运维经验过一遍指标清单, 「当年排障时我还想看什么」—— 至少补一条 AI 没想到的指标。 另外盯住 label 基数(让它数给你看)和 histogram bucket 上限 (10s 封顶,不然真出事时毛刺会被最后一个 bucket 吃掉)。
为 forge 接入完整的可观测性,分四步: 1. 在 forge-util 新增 obs 模块:封装 metrics-exporter-prometheus 的初始化(可配监听地址,delay bucket 从 0.1ms 对数铺到 10s),以及 tracing-subscriber 初始化(EnvFilter 控制级别,JSON 与人类可读两种格式可选); 2. 在 forge-node 与 forge-fs 埋点,指标清单:forge_rpc_duration_seconds(histogram, label: method/node)、forge_rpc_errors_total(counter, label: method)、forge_store_io_duration_seconds(histogram, label: op, op 取 read/write/fsync)、forge_store_disk_errors_total(counter)、forge_rebuild_bytes_total(counter)、forge_rebuild_backlog_bytes(gauge)、forge_raft_leader_changes_total(counter);label 组合总数写进注释并保证少于 200; 3. 给写路径接 tracing:从 FUSE write 到条带提交到 store 落盘的 span 链,tonic 客户端把 trace id 放进请求 metadata,服务端 interceptor 恢复上下文;span 树总耗时超过 50ms 时以 JSON 整树写入慢 IO 日志文件,否则按 1% 采样; 4. 生成 Grafana dashboard 的 JSON(三行:RED 总览、每盘 USE 热力图、重建与 Raft 状态)和 Prometheus 告警规则 YAML(错误率大于 1% 持续 5 分钟、写 P99 大于 100ms 持续 10 分钟、rebuild backlog 30 分钟不降),每条告警的 annotation 里附一句处置提示。 验收:3 节点起集群,跑 5 分钟 forge-bench 负载,curl 任一节点 /metrics 能看到全部指标且直方图有数据;人为给一个节点注入 200ms 磁盘延迟后,慢 IO 日志里能找到完整 span 树。计时打点一律通过现有 Clock trait 取时间,不要直接调 Instant::now,保持模拟测试可用。
小结
- 可观测性是还债:你骂过的黑盒系统什么样,你的系统就不许什么样
- 指标设计用两套视角:USE 看资源(盘、内存、队列),RED 看请求(QPS、错误、延迟); 延迟一律直方图,label 基数管在百级
metrics+ prometheus exporter 一行埋点;tracingspan 树串起跨节点链路, 慢请求全留、快请求抽样,慢 IO 日志是留给未来的现场- 大盘三行:RED 总览、USE 热力图、forge 状态;告警只配三条且各带 runbook, 别把自己训练成划告警的人
- 系统看得见了,下一节就敢动刀:带着 flamegraph 做一轮真正的性能调优