很多团队都经历过这样的阶段:
服务上线时只有日志,出问题靠 grep;后来接入监控,开始看 CPU、内存和 QPS;再后来补上链路追踪,以为终于可以“一眼定位问题”。
结果是:仪表盘越来越丰富,日志平台越来越贵,告警也越来越多,但真正的故障排查仍然很慢。
一个典型的线上场景可能是这样:
- 用户反馈下单变慢;
- 监控显示某台机器 CPU 很高;
- 日志里没有明显报错;
- 链路追踪只看到几个耗时较长的 Span;
- 最后排查半天,发现是某个下游接口在特定租户数据下出现了慢查询。
这不是工具不够多,而是可观测性链路之间存在断层。
可观测性不是“把 Metrics、Logs、Traces 都接进来”,而是让工程师在不知道问题是什么时,仍然能够提出假设、缩小范围并找到证据。
下面聊聊最常见的 4 个断层。
断层一:指标很多,但没有业务语义
最常见的指标面板通常长这样:
CPU 使用率
内存使用率
磁盘 IO
请求 QPS
线程池大小
连接池数量
这些指标当然有价值,但它们回答的是“系统资源怎么样”,不一定能回答“用户受到了什么影响”。
CPU 从 30% 涨到 60%,可能完全不影响业务;而 CPU 只有 20%,也可能因为线程阻塞导致所有请求超时。
所以,第一层可观测性应该围绕用户可感知的信号建立。
常用的四类 Golden Signals 是:
- Latency:请求耗时;
- Traffic:请求流量;
- Errors:错误率和错误类型;
- Saturation:资源饱和度。
但在业务系统里,还应该补充业务 SLI,例如:
支付成功率
订单创建成功率
消息投递延迟
库存扣减成功率
搜索无结果率
只有先定义“什么算好”,后面的指标才有解释意义。
例如,延迟看平均值很容易被大量正常请求稀释。更有价值的是分位数:
histogram_quantile(
0.95,
sum by (le, route) (
rate(http_request_duration_seconds_bucket{service="order-api"}[5m])
)
)
这条查询回答的是:过去 5 分钟,95% 的请求在多长时间内完成?
还可以继续回答:
- P95 变慢发生在哪个 route?
- 是所有租户都慢,还是某个租户慢?
- 是服务端处理慢,还是客户端等待久?
- 错误率是否和延迟上升同时发生?
指标如果没有业务语义、维度和时间窗口,最终只会变成“看起来很专业的背景墙”。
断层二:有日志,但没有统一上下文
很多故障排查卡住,不是因为日志少,而是因为无法把同一次请求的日志串起来。
例如:
request start
user query start
db timeout
request failed
这四行日志看起来能读懂,但系统里同时可能有几千个请求。没有统一的关联字段,就只能靠时间戳猜。
可观测性日志至少要满足三个条件:
- 结构化;
- 带关联标识;
- 记录关键上下文,但不包含敏感数据。
一个最小可用的日志上下文可以设计成:
type LogContext = {
traceId: string;
spanId?: string;
requestId: string;
service: string;
route?: string;
tenantId?: string;
errorCode?: string;
};
在请求入口生成上下文,然后在整个调用链中透传:
logger.info({
event: "order_create_started",
traceId,
requestId,
service: "order-api",
route: "POST /orders",
tenantId,
}, "order create started");
关键是字段名要统一:
- 不要今天用
trace_id,明天用traceID; - 不要把完整身份证、手机号、Token 写进日志;
- 不要在低基数指标里塞用户 ID;
- 不要只记录“失败”,要记录失败类型和可检索的错误码。
日志的价值不在于数量,而在于:
从一个错误现象出发,能否在几分钟内找到对应的请求、调用链和业务上下文。
断层三:有链路追踪,但没有真正打通
Trace 工具最容易产生“我们已经可观测了”的错觉。
打开链路面板,能看到一串 Span,不代表它就能帮助定位问题。
常见问题有三类。
第一,Span 命名没有业务信息。
GET
GET
POST
不如:
OrderController.create
OrderService.validateStock
PaymentClient.createPayment
第二,异步任务丢失上下文。
很多请求进入消息队列或定时任务后,traceId 没有继续传递,导致主请求和后台任务变成两段互不相关的链路。
第三,采样策略只考虑数量。
如果只按固定比例采样,很可能恰好丢掉那些真正需要排查的慢请求和错误请求。更合理的方式是:
- 正常请求低频采样;
- 错误请求优先保留;
- 慢请求按延迟阈值保留;
- 关键业务链路提高采样率;
- 敏感请求脱敏后再保留。
Trace 不只是调用耗时列表,它应该回答:
- 这次请求经过了哪些服务?
- 时间主要花在哪个环节?
- 哪个下游依赖先出现异常?
- 数据库查询和外部调用分别耗用了多少时间?
如果 Trace 和 Logs、Metrics 之间没有 ID 关联,那么它们只能各自提供局部证据,无法形成因果链。
断层四:告警很多,但没有人能立刻采取行动
告警是另一个特别容易被“数量优化”误导的地方。
有些团队把告警阈值调得非常灵敏,结果每天收到大量通知,但大部分都是:
- 瞬时抖动;
- 单实例异常但流量已经切换;
- 资源水位升高但业务仍正常;
- 同一个根因触发了十几个重复告警。
告警越多,工程师越容易产生告警疲劳。真正的线障反而被淹没在噪音里。
一个有效的告警至少要包含四层信息:
alert: CheckoutLatencyHigh
expr: |
histogram_quantile(
0.95,
sum by (le) (
rate(http_request_duration_seconds_bucket{route="/checkout"}[5m])
)
) > 2
for: 10m
labels:
severity: page
team: payments
annotations:
summary: "Checkout P95 latency is above 2s"
dashboard_url: "https://grafana.example.com/d/checkout"
runbook_url: "https://wiki.example.com/runbooks/checkout-latency"
其中最重要的不是表达式,而是:
for:持续多久才真正告警;severity:是需要立刻处理,还是工作时间内处理;team:谁负责;runbook_url:第一步该做什么;dashboard_url:从哪里开始查。
告警应该面向用户可感知的症状,而不是所有内部资源指标。
例如:
- 支付成功率低于目标,值得 Pager;
- 单个 CPU 短时升高,可能只需要记录;
- 磁盘容量 7 天后会满,可以提前工单;
- 某下游错误率持续上升,并且影响核心链路,才需要升级。
告警不是一个“阈值集合”,而是一套行动协议。
如何把四层信息真正连起来
可观测性建设可以采用一个很朴素的目标:
从用户反馈的一个现象,沿着请求链路走到最终根因。
具体可以按下面的顺序推进。
1. 先定义业务 SLI
不要从工具开始,而要从“用户在意什么”开始:
下单成功率
支付成功率
搜索成功率
消息送达延迟
2. 建立统一上下文
所有服务至少透传:
traceId
requestId
service
route
errorCode
统一字段命名,并在入口处生成或接收上游上下文。
3. 让日志、指标、链路互相引用
理想状态是:
告警
-> Dashboard
-> 异常时间窗口
-> 代表性 Trace
-> 对应日志
-> 明确错误码和调用链
只要其中一个环节断裂,排查时间就会成倍增加。
4. 对高基数和敏感信息做边界设计
不要把所有东西都放进 Metrics。用户 ID、订单 ID 这类高基数数据适合放日志或 Trace,不适合直接成为指标标签。
日志中也要避免记录:
- Token、Cookie、密码;
- 完整手机号、身份证号;
- 支付凭证;
- 用户上传的原始内容。
可观测性越强,越需要明确数据边界。
5. 用复盘验证可观测性,而不是只验证系统恢复
一次故障结束后,除了问“怎么修好的”,还应该问:
- 能否在 5 分钟内定义影响范围?
- 能否找到一个代表性 Trace?
- 能否通过日志还原关键业务状态?
- 告警是否足够具体?
- 下一次是否可以用同样路径更快定位?
如果复盘只改代码,不改观测能力,下一次故障很可能只是换一种方式重演。
最后
可观测性不是三个工具的缩写。
Metrics 解决“整体趋势和异常检测”,Logs 解决“具体发生了什么”,Traces 解决“时间花在哪里、请求经过了谁”。
只有它们通过统一的业务语义、关联 ID 和行动协议连接起来,工程师才可能从“知道系统出问题了”,走到“知道为什么出问题”。
工具可以采购,拓扑可以画出来,但真正决定排障效率的,是系统能不能围绕一次用户请求,提供一条连续、可信、可操作的证据链。
如果你也在做可观测性建设,也欢迎在微信公众号搜索「长安米粒贵」继续交流。