主题
可观测性三支柱:日志、指标与链路
本文是微服务系统学习系列的 L2 核心篇。前置:31. API 网关设计。 学完可以配合面试题食用:07. 分布式追踪、08. 日志与 ELK、09. 监控
为什么需要三个支柱,不是一个
微服务架构里一个请求会跨 5-10 个服务。出了故障,光看日志没法知道"哪个环节慢";光看指标不知道"具体哪个订单挂了";光看链路不知道"磁盘是不是快满了"。三个维度各自解答不同的问题:
- 日志:某个具体请求报了什么错,SQL 是什么,参数是多少。个体视角。
- 指标:这 5 分钟接口平均延迟多少,错误率多少,CPU 用了多少。聚合视角。
- 链路:一个请求从网关到某个服务到数据库,每步花了多少毫秒。路径视角。
三者互补,缺一个就瞎一只眼。下面分别讲落地方式。
结构化日志:别再用 println 了
日志的进化路径:System.out -> 按格式拼接 -> 结构化 JSON。大多数人停在第二步。
非结构化的痛点
java
log.info("Order {} processed, user={}, amount={}", orderId, userId, amount);输出是 Order 10086 processed, user=9527, amount=199.0。要搜索"金额超过 100 的订单"得写正则,要按用户 ID 聚合得先提取子串——ELK 里没法直接做字段查询。
结构化的解法
Logback 配置 JSON encoder(依赖 logstash-logback-encoder):
xml
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>false</includeContext>
<customFields>{"app":"order-service","env":"prod"}</customFields>
</encoder>
</appender>输出变为:
json
{"@timestamp":"2025-11-21T10:30:00.123+08:00","level":"INFO","message":"Order processed","app":"order-service","logger":"com.example.OrderService","order_id":"10086","user_id":"9527","amount":199.0}直接搜 amount > 100 或者按 user_id 聚合都行。traceId 必须每条日志都带,这是日志和链路关联的桥梁。
采样策略
- ERROR 以上全量保留(异常不常见)
- INFO 按 1:10 采样(量大的业务日志)
- DEBUG 只保留最近 30 分钟,滚动覆盖
指标体系:RED 和 USE 就够了
指标不是越多越好。每增加一个指标面板,排查时多一个分心项。两个方法论覆盖绝大多数场景:
RED 法(服务视角):
Rate - 每秒请求数(QPS/TPS)
Errors - 错误率(5xx/4xx/业务异常占比)
Duration - 延迟分布(P50/P90/P99)
USE 法(资源视角):
Utilization - 利用率(CPU/内存/磁盘 IO)
Saturation - 饱和度(线程池队列长度/连接数)
Errors - 错误数(磁盘坏道/超时重试次数)用 Prometheus 暴露指标就一个注解:
java
@RestController
public class OrderController {
private final Counter orderCounter = Counter.build()
.name("orders_created_total")
.help("Total orders created")
.labelNames("status")
.register();
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable String id) {
orderCounter.labels("success").inc();
// ...
}
}配合 Grafana 大盘,一个页面看全服务的 RED 指标。告警不要只配阈值,用基于趋势的异常检测(比如错误率 5 分钟滚动比前 30 分钟翻倍触发),避免运维半夜被误报整醒。
分布式追踪:OpenTelemetry 统一标准
在混乱的链路追踪工具里,OpenTelemetry(OTel)已经成了事实标准——把 Jaeger/Zipkin/SkyWalking 的协议统一了。
核心概念
请求 -> 网关生成 TraceId: abc123
└─ SpanId: 001(网关处理)
├─ 调用服务A: SpanId 002, ParentSpanId 001
│ └─ 查数据库: SpanId 003, ParentSpanId 002
└─ 调用服务B: SpanId 004, ParentSpanId 001TraceId 贯穿整条调用链,SpanId 标记每个节点,ParentSpanId 拼出调用树。上下文通过 HTTP 头的 traceparent 字段传播:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01格式:版本-traceId-spanId-traceFlags。这是个 W3C 标准,Spring Boot 3.x 的 RestTemplate/WebClient 自动携带。
OTel 接入
Spring Boot 3.x 引入 opentelemetry-javaagent.jar 就能自动埋点——零代码改动。启动参数加一行:
bash
-javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://jaeger:4318框架层面的 HTTP 请求、数据库调用、消息队列操作都会自动生成 Span。需要自定义埋点就用 @WithSpan 注解:
java
@WithSpan("calculate-discount")
public BigDecimal calculateDiscount(Order order) {
// 这个方法会被追踪
}落地架构:选型对比
| 组件 | 选型 | 理由 |
|---|---|---|
| 日志收集 | Loki(而非 ELK) | 和 Prometheus 同一家公司,标签查询一致,资源占用低于 ES |
| 指标存储 | Prometheus | 社区成熟,Grafana 原生支持,Pull 模式避免集群压力 |
| 链路存储 | Jaeger | 原生支持 OTel 协议,部署简单,查询 UI 够用 |
| 可视化 | Grafana | 统一面板,日志/指标/链路可以关联跳转 |
Grafana 面板可以用 Explore 模式直接查 PromQL 和 LogQL,快速定位故障。每张面板配一个关键信息就够了,别堆叠。
json
{
"title": "服务 RED 面板",
"panels": [
{"title": "QPS", "expr": "rate(http_requests_total[1m])"},
{"title": "错误率", "expr": "rate(http_requests_total{status=~\"5..\"}[1m]) / rate(http_requests_total[1m])"},
{"title": "P99 延迟", "expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[1m]))"}
]
}常见误区与小结
- 日志不统一格式:不同服务用不同日志格式,排查时没法联合查询。统一 JSON 格式是底线。
- 指标打太多标签:每个标签组合增加一个时间序列,
user_id这种高基数标签会让 Prometheus 内存爆炸。高基数场景用日志查。 - 链路只接一个服务:链路追踪的价值在于跨服务关联,只接一个服务等于白接。
- 告警一锅端:阈值告警太多反而降低响应速度。用 SLO 驱动的告警——先定目标(99.9% 的请求 200ms 内返回),再配告警。
- 缺了成本视角:全量采集一天 TB 级日志,只采到下个月预算就超了。明确保留周期和采样率。
小结:可观测性在微服务里不是"要不要"的问题,是"怎么做得可维护"的问题。三支柱各司其职,通过 traceId 串联。下一篇 33. 微服务治理实战 讲灰度发布、混沌工程和 K8s 落地。
参考
参考:OpenTelemetry 官方文档(opentelemetry.io)、Google SRE 手册 Monitoring 章节、Prometheus 官方最佳实践