Storforge
编码预计 45 分钟

可观测性:给自己的系统装仪表盘

你运维时抱怨过的「这系统什么都看不见」,现在轮到你来还债: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_secondshistogram,按 method/node 分 labelRED「慢在网络还是慢在盘」
forge_store_io_duration_secondshistogram,按 read/write/fsync 分RED「fsync 到底多慢」
forge_store_disk_errors_totalcounter,按 kind 分USE「坏盘前有没有前兆」
forge_rebuild_bytes_total / forge_rebuild_backlog_bytescounter / gaugeUSE「重建还要多久」
forge_raft_leader_changes_totalcounterRED「刚才是不是切主了」
forge_placement_versiongauge—「各节点放置表一不一致」

两条设计纪律,都是你被烂指标坑出来的经验:

  1. 延迟必须是直方图,不是平均值。 平均延迟 1ms 的系统可以有 2 秒的 P99, 你被这种「均值很美」的大盘骗过。用 histogram,bucket 边界按对数分布铺到秒级。
  2. 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 日志是给三个月后深夜排障的自己留的现场。

Checkpoint单选

P99 写延迟告警触发了。指标和 tracing 各自回答什么问题?

大盘与告警:把运维经验反哺给自己

Grafana 大盘怎么排,你比这节课有发言权。给 forge 1.0 的大盘定一个最小结构:

  • 第一行,服务视角(RED):集群总 QPS、错误率、读/写 P50/P99 —— 值班的人 5 秒钟判断「有没有事」
  • 第二行,资源视角(USE):每盘带宽与队列深度热力图、节点内存、EIO 计数
  • 第三行,forge 特有状态:Raft leader 与切主次数、重建 backlog 与速率、放置表版本一致性

告警只配三条,宁缺毋滥 —— 你太清楚告警疲劳是怎么把人训练成「看到告警先划掉」的:

  1. forge_rpc_errors_total 错误率 5 分钟均值大于 1% —— 用户已经受影响,P1
  2. 写 P99 大于 100ms 持续 10 分钟 —— 恶化趋势,工作时间处理
  3. forge_rebuild_backlog_bytes 持续 30 分钟不下降 —— 重建卡住了,冗余度在裸奔,这条最凶险
✓每条告警配一段 runbook

你收过最气人的告警是只有一句 "threshold exceeded" 的那种。给自己的三条告警各写一段 runbook:这条告警意味着什么、先看哪个面板、常见原因和处置命令。 写 runbook 的过程往往还能反过来发现指标缺口 ——「处置时我需要看 X」而 X 还没埋点。

⚡AI 结对:给 forge 织入可观测性pair with ai

review 的重点不在代码而在设计遗漏:拿着你自己的运维经验过一遍指标清单, 「当年排障时我还想看什么」—— 至少补一条 AI 没想到的指标。 另外让它把 label 基数数给你看(做成表格:指标 × 每个 label 的取值个数 × 组合数), 再核对 histogram 分桶上限(10s 封顶,不然真出事时毛刺会被最后一个 bucket 吃掉)。 链路那块别去读代码,让它把一次写请求的 span 时序画成一张表给你 —— 哪几段、谁是谁的父级、跨节点在哪里交接 —— 你核对完,再用注延迟的实验验证它没说谎。

目标:把 forge 从黑盒变成看得见的系统 —— Prometheus 指标、OpenTelemetry 风格的链路追踪、慢 IO 日志、Grafana 大盘和告警规则,一次配齐,让值班的人能在 5 秒内判断有没有事、在 5 分钟内定位到是哪一层慢。

行为规格:
1. 每个节点暴露一个 /metrics 端点(监听地址可配),指标清单按下面这份来,名字和 label 维度一个字都不要改:
 - forge_rpc_duration_seconds,直方图,label: method、node
 - forge_rpc_errors_total,计数器,label: method
 - forge_store_io_duration_seconds,直方图,label: op(取 read / write / fsync)
 - forge_store_disk_errors_total,计数器,label: kind
 - forge_rebuild_bytes_total,计数器;forge_rebuild_backlog_bytes,仪表盘量
 - forge_raft_leader_changes_total,计数器
 - forge_placement_version,仪表盘量
2. 延迟直方图的分桶从 0.1ms 对数铺到 10s(0.0001 / 0.0005 / 0.001 / 0.005 / 0.01 / 0.05 / 0.1 / 0.5 / 1 / 5 / 10)。所有 label 的组合总数必须小于 200,把你算出来的组合数写在文档里给我看。
3. 日志要能按级别过滤,并且能在人类可读和 JSON 两种格式之间切换。
4. 给写路径接上链路追踪:从 FUSE write 到条带切分、到每个节点的 RPC、到 EC 编码、到落盘 fsync,每一段是一个 span;跨节点时 trace id 随请求带过去,服务端接上,保证一条链路横跨三台机器仍是完整的一棵 span 树。
5. 采样策略是「慢的全留,快的抽样」:一棵 span 树总耗时超过 50ms 就整树落到慢 IO 日志(JSON,一棵树一条),否则按 1% 抽样。阈值和采样率可配。
6. 产出 Grafana 大盘定义,三行:第一行 RED 总览(集群 QPS、错误率、读写 P50/P99),第二行 USE 资源(每盘带宽与队列深度热力图、节点内存、EIO 计数),第三行 forge 特有状态(Raft leader 与切主次数、重建 backlog 与速率、放置表版本一致性)。
7. 产出 Prometheus 告警规则,只要三条:错误率 5 分钟均值大于 1%;写 P99 大于 100ms 持续 10 分钟;重建 backlog 持续 30 分钟不下降。每条告警必须带一段 runbook 文本:这条告警意味着什么、先看哪个面板、常见原因和处置命令。
8. 计时取的必须是那个可替换的时钟,不能绕过去直接读真实时钟 —— 否则上一课的模拟测试会失效。用什么库你定,最后告诉我为什么选它。

交付:埋点改造 + 大盘定义文件 + 告警规则文件 + 三段 runbook + 一份指标字典(每个指标:含义、类型、label 维度、组合数、它回答什么排障问题)。
你的验收动作自己跑、自己看,不用读代码
  • 3 节点起集群,跑 5 分钟压测负载,curl 任一节点的 /metrics,确认清单里每个指标都在、直方图每个桶都有数,少一个就打回去
  • 把大盘定义导入 Grafana,对着三行面板看一遍:能不能在 5 秒内判断集群有没有事;哪一格你看不懂,就让 AI 解释到你懂
  • 人为给一个节点注入 200ms 磁盘延迟,确认慢 IO 日志里出现完整 span 树,并且树里 fsync 那一段确实是耗时大头 —— 这是验证链路追踪没白埋
  • 手工把某个节点打成错误率超 1%(比如让它的盘持续报错),确认第一条告警真的触发;不触发就是告警规则写错了,不是运气好
  • 让 AI 把 label 组合数算给你看,再让它举一个「如果按 inode 或客户端 IP 分 label 会怎样」的估算,确认它明白基数爆炸的后果
必须问到听懂的问题答不上来就别合并
  • 这套指标在什么情况下会骗我?举一个「大盘全绿但用户已经在投诉」的具体场景
  • 慢 IO 日志按 50ms 阈值留,会漏掉哪一类问题?如果一次故障里所有请求都慢到 45ms,我从哪儿看出来
  • 三条告警各自的误报来源是什么?哪一条最容易把值班的人训练成看到告警先划掉

小结

  • 可观测性是还债:你骂过的黑盒系统什么样,你的系统就不许什么样
  • 指标设计用两套视角:USE 看资源(盘、内存、队列),RED 看请求(QPS、错误、延迟); 延迟一律直方图,label 基数管在百级
  • metrics + prometheus exporter 一行埋点;tracing span 树串起跨节点链路, 慢请求全留、快请求抽样,慢 IO 日志是留给未来的现场
  • 大盘三行:RED 总览、USE 热力图、forge 状态;告警只配三条且各带 runbook, 别把自己训练成划告警的人
  • 系统看得见了,下一节就敢动刀:带着 flamegraph 做一轮真正的性能调优