ElIauk

云原生可观测性三大支柱

三根柱子撑的是同一件事 指标、日志、链路追踪经常被当成三套独立系统,各自采购、各自接入,最后出了故障还是靠人肉翻。它们真正的分工是回答不同粒度的问题:指标告诉你“什么时候、哪个服务、多严重”,链路告诉你“这一个请求慢在了哪一跳”,日志告诉你“那一刻到底发生了什么”。 云原生环境下还多了一层约束:Po

评论
云原生可观测性三大支柱

三根柱子撑的是同一件事

指标、日志、链路追踪经常被当成三套独立系统,各自采购、各自接入,最后出了故障还是靠人肉翻。它们真正的分工是回答不同粒度的问题:指标告诉你“什么时候、哪个服务、多严重”,链路告诉你“这一个请求慢在了哪一跳”,日志告诉你“那一刻到底发生了什么”。

云原生环境下还多了一层约束:Pod 随时重建,IP 不再是身份。所以三根柱子的公共坐标系不是主机名,而是 service.name、k8s.pod.name 和 traceId。没有统一语义约定,三套数据就无法关联。

指标:先定指标面,再谈面板

服务侧建议按 RED 铺:请求速率、错误率、耗时分布。资源侧按 USE:使用率、饱和度、错误。别一上来就接几十个中间件 exporter,先把一个接口的 P99 做准。

最容易出问题的是耗时指标。Prometheus 的直方图桶是静态的,用 Micrometer 默认桶看 P99,误差可能大到没有参考价值。桶应该按 SLO 划:如果 SLO 是 300ms,就至少要覆盖 50ms、100ms、200ms、300ms、500ms、1s 这些边界,否则 P99 只能内插出一个数字,看起来精确,其实不可信。

management:
  metrics:
    distribution:
      percentiles-histogram:
        http.server.requests: true
      slo:
        http.server.requests: 50ms,100ms,200ms,300ms,500ms,1s
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,metrics

另一个红线是高基数标签。把 userId、orderId、原始 URL 直接打成 tag,Prometheus 的时序数会指数爆炸,几天就把内存吃光。路径要归一化(/orders/{id} 而不是 /orders/10086),真正的定位靠日志和链路。

日志:结构化只是及格线

JSON 日志的价值不在好看,而在能被采集侧直接检索。比格式更重要的是把 traceId、spanId 注入每一行,否则日志和链路永远是两张皮。用 OpenTelemetry 的 Logback appender,或者在 MDC 里手工放,两种都行,关键是全链路都要有。

public final class TraceIdFilter implements Filter {

    private static final Logger log = LoggerFactory.getLogger(TraceIdFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String traceId = httpRequest.getHeader("traceparent");
        if (traceId == null || traceId.isBlank()) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("traceId");
        }
    }
}

MDC.remove 必须放在 finally,线程池复用线程时,不清理的 MDC 会把上一个请求的 traceId 带到下一个请求,日志里就会出现“两个不相干的请求共享同一个 traceId”的怪现象。

日志级别也要有纪律:生产环境 INFO 起步,异常打 ERROR 且必须带堆栈,业务校验失败打 WARN 不带堆栈。每条日志都应该是“有人在半夜会看”的,否则只是磁盘消耗。

追踪:采样策略决定了成本

全量链路的存储成本非常高,通常按 1%–10% 头部采样。但头部采样会漏掉长尾故障,更实用的做法是在 Collector 里做尾部采样:先全收,按“有错误或耗时超过 2 秒”决定保留。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 75
  batch:
    timeout: 5s
    send_batch_size: 1024
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow
        type: latency
        latency: { threshold_ms: 2000 }
      - name: baseline
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }

exporters:
  otlphttp/traces:
    endpoint: http://tempo.observability:4318
  prometheus:
    endpoint: 0.0.0.0:8889

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlphttp/traces]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [prometheus]

尾部采样要在同一个 Collector 实例里看到完整的 trace 才能决策,所以采集侧必须保证同一 trace 的所有 span 落到同一个实例(按 traceId 做负载均衡)。这一点没做对,尾部采样会退化成随机丢弃。

排查步骤

一次线上告警的标准动作是“指标定位、链路下钻、日志坐实”:

  1. 从告警面板确认时间窗和受影响的服务、接口。
  2. 在链路系统里按服务 + 错误码筛,找一条典型慢 trace,看哪一跳的耗时占大头。
  3. 用那条 trace 的 traceId 去日志系统检索,把应用日志、网关日志、中间件日志串起来。
  4. 如果链路里根本没有这条请求,说明采样漏了或者埋点没覆盖,先补埋点再谈定位。
# 快速验证某个服务是否真的在吐指标
kubectl port-forward -n shop-prod svc/order-api 8080:8080
curl -s localhost:8080/actuator/prometheus | grep -c 'http_server_requests'

常见坑

三套系统各用一套 ID。 网关生成一个 requestId、应用生成一个 traceId、日志里又是另一个,关联不起来,等于白采。

只在入口采样。 上下游采样率不一致,链路断成几截,看得到入口看不到下游。

把 trace 当审计日志用。 追踪系统不适合长期存储全量明细,保留期一到数据就没了,需要审计的信息应该进日志或专门的存储。

标签里塞用户输入。 除了基数爆炸,还可能是注入风险。指标标签只放枚举值和归一化路径。

只监控不告警,或只告警不写处置手册。 告警没有对应的 runbook,半夜被叫起来的人只能从零开始排查,这会直接决定故障恢复时间。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者