可观测性

多语言系统最痛的时刻,是线上出问题时「不知道发生了什么」:Go 网关报 500,Java API 日志里什么都没有,Python Worker 在悄悄重试。容器与 K8s 让服务可以随时被重建,却也让「登上机器看日志」的传统手段失效。可观测性不是装一个监控工具,而是让系统主动回答三个问题:出了什么问题(日志)、系统健康吗(指标)、这个请求慢在哪(追踪)。本章从多语言视角统一三支柱的数据约定与工具链,目标是任何语言的服务都遵循同一套日志、指标、追踪规范,排障时能用一个 trace_id 串起全部证据。


1 三支柱与它们回答的问题

graph TB
    Q["线上问题"] --> L["日志 Logs<br/>「发生了什么?」"]
    Q --> M["指标 Metrics<br/>「系统现在健康吗?」"]
    Q --> T["追踪 Traces<br/>「这个请求慢在哪?」"]
    L --> L1["离散事件、错误堆栈、审计"]
    M --> M1["聚合数值、趋势、告警"]
    T --> T1["请求全链路、跨服务耗时"]
    L -.->|"trace_id 关联"| T
    M -.->|"exemplar 关联"| T
支柱数据形态成本回答的问题不擅长
日志离散事件,可全文检索高(量大)具体错误与上下文全局趋势、聚合统计
指标数值时间序列,聚合健康度、容量、告警单个请求的细节
追踪调用链 Span跨服务延迟与依赖长期历史、高频全量采集

三者的关系不是替代而是互补:指标发现异常,追踪定位到具体服务与调用,日志给出根因细节。多语言系统的挑战在于三种数据由不同语言的库产生,必须先统一数据格式与关联字段,否则工具装了也串不起来。

1.1 统一约定(全语言必须遵守)

约定内容
日志输出一律 JSON 到 stdout,不写文件
时间格式UTC,RFC3339 带毫秒,如 2026-09-12T10:15:30.123Z
必填字段timestamplevelserviceversionmessage
关联字段trace_idspan_id(有则必填)
指标命名<namespace>_<subsystem>_<name>_<unit>,如 http_server_requests_seconds
资源属性service.nameservice.versiondeployment.environment
敏感信息不记录密码、token、完整身份证号;必要时脱敏

2 结构化日志

2.1 JSON 日志格式

{
  "timestamp": "2026-09-12T10:15:30.123Z",
  "level": "ERROR",
  "service": "order-api",
  "version": "1.4.2",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "message": "failed to create order",
  "order_id": "10023",
  "error.type": "java.sql.SQLTransientConnectionException",
  "duration_ms": 3021
}

约定字段名的理由:timestamp/level/message 是绝大多数采集器的默认识别字段;trace_id/span_id 是 W3C TraceContext 的标准字段,可直接与追踪系统关联;自定义字段用扁平命名(order_id 而非嵌套对象),便于 Loki/ES 查询。

2.2 各语言日志库

语言推荐组合结构化输出方式备注
JavaSLF4J + Logbacklogstash-logback-encoder 输出 JSON生态最成熟,MDC 放 trace_id
Pythonstructlog 或 logging + python-json-loggerstructlog.processors.JSONRendererstructlog 的 contextvars 天然适配异步
Golog/slog(1.21+)或 zapslog.NewJSONHandler标准库已够用,zap 性能更高
Node/TSpino默认 JSON比 winston 快,注意序列化错误对象
Rusttracing + tracing-subscriberjson featurespan 可自动带字段
Dartlogging + 自定义 JSON formatterLogger 输出 JSON服务端日志库生态较弱,常手写 formatter
C++spdlogspdlog::sinks::json_sink 或自定义注意多线程写入与异步 sink

Python structlog 示例:

import structlog
import logging
 
# 统一配置:JSON 输出 + 时间戳 + trace_id 注入
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,          # 注入请求级上下文
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso", utc=True),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer(ensure_ascii=False),
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
    cache_logger_on_first_use=True,
)
 
log = structlog.get_logger(service="recommend-worker", version="1.4.2")
log.info("task consumed", task_id="t-100", duration_ms=42)

Go slog 示例:

// 统一 JSON 日志;trace_id 从 context 中取出后附加
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
logger := slog.New(handler).With("service", "gateway", "version", version)
 
logger.Info("request completed",
    "method", r.Method,
    "path", r.URL.Path,
    "status", status,
    "duration_ms", time.Since(start).Milliseconds(),
    "trace_id", traceIDFromContext(r.Context()),
)

2.3 日志级别与采样

级别使用场景生产默认
ERROR需要人介入的失败全部保留
WARN可自愈但需关注全部保留
INFO关键业务事件(订单创建、任务完成)保留,注意控制量
DEBUG排障细节默认关闭,按需动态开启
TRACE逐帧细节禁止在生产默认开启

高流量服务(网关、Worker)的 INFO 日志可能达到每秒数万条。两个手段:动态日志级别(通过配置中心或 actuator 在排障时临时开 DEBUG)与采样(对成功请求按比例记录,错误请求全量记录)。注意:日志采样不能替代追踪采样,两者的目的不同。

2.4 日志收集架构

graph LR
    subgraph 节点
        P1["Pod: Java<br/>stdout JSON"]
        P2["Pod: Go<br/>stdout JSON"]
        P3["Pod: Python<br/>stdout JSON"]
        FB["Fluent Bit<br/>DaemonSet"]
        P1 --> FB
        P2 --> FB
        P3 --> FB
    end
    FB --> LOKI["Loki<br/>日志存储与查询"]
    FB --> ES["Elasticsearch<br/>(可选)"]
    LOKI --> GF["Grafana<br/>统一查询与仪表盘"]
    ES --> GF
采集方案适用不适用
Fluent Bit(DaemonSet)K8s 标准方案,资源占用低需要复杂处理逻辑(用 Fluentd)
Fluentd插件丰富、复杂路由与转换资源受限的节点
Vector高性能、Rust 实现、可做聚合团队无运维能力时
应用直推 Loki无节点权限的托管环境需要统一采集策略的集群
Promtail与 Loki 配套、配置简单需要多目的地输出的场景

K8s 中的关键实践:应用只写 stdout,采集器以 DaemonSet 方式部署(概念见 03 K8s 核心概念),从 /var/log/containers/ 读取并附加 Pod 标签(namespacepodcontainerapp)。不要在应用里自己写文件再挂卷采集——容器重建后文件随之消失,且多了一层维护。


3 指标

3.1 Prometheus 数据模型

每条时间序列由指标名 + 标签集唯一确定,样本是 (timestamp, value)

http_requests_total{service="gateway", method="GET", path="/api/orders", status="200"} 1027
指标类型语义适用不适用
Counter只增不减的累计值请求数、错误数、处理字节数会减少的值(如队列长度)
Gauge可增可减的瞬时值内存使用、连接数、队列长度需要求速率的总量
Histogram分桶统计分布请求延迟、响应大小需要精确分位数的场景(用 Summary)
Summary客户端计算分位数单实例精确分位需要跨实例聚合分位数

延迟指标优先用 Histogram:它可以在服务端聚合后计算全局 P99,而 Summary 的分位数无法跨实例合并。

3.2 各语言客户端库

语言客户端库与框架集成
JavaMicrometer(+ Prometheus registry)Spring Boot Actuator 自动暴露 /actuator/prometheus
Pythonprometheus_clientFastAPI/Flask 需手动中间件或 instrumentator
Goprometheus/client_golangpromhttp.Handler(),gRPC 有拦截器
Node/TSprom-client需自行暴露 /metrics
Rustmetrics + metrics-exporter-prometheus需手动初始化
Dartprometheus_client(社区)生态较弱,常自建简单 exporter
C++prometheus-cpp需手动注册与暴露

统一约定:每个服务暴露 /metrics(HTTP)或 :9090(非 HTTP 服务),由 Prometheus 抓取。Java 的 Actuator 默认包含大量 JVM 指标,其他语言需要自己补充运行时指标(GC、goroutine 数、事件循环延迟)。

3.3 RED 与 USE 方法

方法适用对象指标
RED请求驱动的服务(API、网关、Worker)Rate(速率)、Errors(错误率)、Duration(延迟分布)
USE资源(CPU、内存、磁盘、网络)Utilization(使用率)、Saturation(饱和度)、Errors(错误)

多语言服务的监控基线:所有服务都要有 RED;所有节点与中间件都要有 USE。只有 CPU 使用率而没有错误率和延迟,等于没有监控。

3.4 PromQL 入门

# 网关每秒请求数(按路由)
sum by (path) (rate(http_requests_total{service="gateway"}[5m]))
 
# 5xx 错误率(百分比)
100 * sum(rate(http_requests_total{status=~"5.."}[5m]))
    / sum(rate(http_requests_total[5m]))
 
# P99 延迟(基于 Histogram)
histogram_quantile(0.99,
  sum by (le, service) (rate(http_request_duration_seconds_bucket[5m])))
 
# 服务是否在线(up=0 表示抓取失败)
up{job="polyglot"}

告警规则示例:

groups:
  - name: polyglot-slo
    rules:
      # 5 分钟内 5xx 占比超过 5% 持续 10 分钟
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m]))
          / sum(rate(http_requests_total[5m])) > 0.05
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "服务 {{ $labels.service }} 错误率过高"
          runbook: "https://wiki.example.com/runbooks/high-error-rate"
      # P99 延迟超过 1 秒
      - alert: HighLatencyP99
        expr: |
          histogram_quantile(0.99,
            sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))) > 1
        for: 10m
        labels:
          severity: warning

告警规则的原则:告警必须可行动CPU 超过 80% 不是告警,是噪音;错误率超过 SLO 且错误预算消耗过快 才是告警。Alertmanager 负责分组、抑制、静默与路由(邮件、Slack、PagerDuty)。


4 链路追踪

4.1 Trace、Span 与上下文传播

一次请求从 Go 网关进入,调用 Java API,再触发 Python Worker,这条完整路径是一个 Trace;其中每次调用(HTTP、gRPC、数据库、消息消费)是一个 Span。跨服务传播靠 HTTP header 中的 traceparent(W3C TraceContext 标准),格式为 版本-trace_id-span_id-采样标志

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
字段示例值长度说明
版本002 位十六进制当前标准固定为 00
trace_id4bf92f3577b34da6a3ce929d0e0e473632 位十六进制全链路唯一 ID,由入口服务生成
span_id00f067aa0ba902b716 位十六进制当前调用的 span ID,每次调用新建
采样标志012 位十六进制01 采样、00 不采样,决定是否上报后端
sequenceDiagram
    participant U as 用户
    participant G as Go 网关
    participant J as Java API
    participant P as Python Worker
    U->>G: HTTP 请求,无 traceparent
    Note over G: 新建 trace_id=4bf9...
    G->>J: HTTP 携带 traceparent 头
    Note over J: 创建子 span,继承 trace_id
    J->>P: Kafka 消息 + traceparent
    Note over P: 创建子 span,继承 trace_id
    P-->>J: 处理完成
    J-->>G: 响应
    G-->>U: 响应

跨进程传播媒介不同:HTTP 用 header,gRPC 用 metadata,Kafka/RabbitMQ 用消息 header(需要显式注入与提取,很多团队漏掉这一步,导致链路在消息队列处断开)。

4.2 OpenTelemetry 架构

graph LR
    A1["Java SDK/Agent"] --> COL["OTel Collector"]
    A2["Go SDK"] --> COL
    A3["Python SDK"] --> COL
    COL -->|"traces"| TEMPO["Tempo / Jaeger"]
    COL -->|"metrics"| PROM["Prometheus"]
    COL -->|"logs"| LOKI["Loki"]
    TEMPO --> GF["Grafana"]
    PROM --> GF
    LOKI --> GF
组件作用说明
API/SDK各语言生成遥测数据统一语义约定(Semantic Conventions)
Collector接收、处理、导出采样、脱敏、批处理、多目的地
OTLP传输协议gRPC/HTTP,厂商中立
Backend存储与查询Tempo/Jaeger(追踪)、Prometheus(指标)、Loki(日志)

各语言 SDK 成熟度:

语言追踪指标日志备注
Java稳定(支持自动埋点 Agent)稳定稳定零代码改动可接入
Python稳定稳定开发中自动埋点覆盖常用框架
Go稳定稳定稳定需显式埋点较多
Node/TS稳定稳定稳定自动埋点覆盖主流框架
Rust稳定稳定开发中生态相对年轻
Dart实验性实验性实验性需自行封装 HTTP 拦截器
C++稳定稳定开发中手动埋点为主

4.3 后端选型

方案适用不适用
Grafana Tempo与 Loki/Prometheus/Grafana 同栈、成本敏感需要复杂查询语言(TraceQL 较新)
Jaeger独立部署、UI 成熟、生态广与 Grafana 栈整合不如 Tempo 自然
Zipkin轻量、老系统集成大规模与高吞吐场景
商业 APM需要开箱即用与专家支持数据主权、成本敏感

5 跨服务关联:trace_id 贯穿一切

三支柱的黏合剂是 trace_id。落地方式:

  1. 入口服务(网关)从 traceparent 提取或生成 trace_id
  2. 应用日志框架自动把 trace_id 写入每条日志(Java 用 MDC + OTel Agent,Python 用 contextvars,Go 用 context)
  3. 调用下游时自动注入 traceparent
  4. 在 Grafana 中从指标 exemplar 或日志跳到追踪详情
# 排障示例:从 Loki 找到一条错误日志,拿到 trace_id,再查完整链路
# 1. Loki 查询
{namespace="polyglot", app="order-api"} |= "ERROR" | json | line_format "{{.trace_id}} {{.message}}"
 
# 2. Tempo/Jaeger 用 trace_id 查询
# 3. 在追踪中发现耗时集中在 MySQL 调用,结合 MySQL 慢查询日志定位

没有统一 trace_id 的多语言系统,排障只能靠猜;有了它,跨语言的调用链第一次成为一份可读的证据。


6 Grafana、告警与 SLO

6.1 仪表盘分层

层级受众内容
业务大盘产品/管理层订单量、成功率、核心漏斗
服务大盘开发团队每服务的 RED 指标、依赖健康
资源大盘运维节点 USE、中间件状态、容量趋势
排障视图on-call日志流、追踪搜索、错误 Top N

Grafana 的原则:每个面板都要能回答一个具体问题。堆 50 个面板的大盘等于没有大盘。告警直接链接到对应面板与 runbook。

6.2 SLI、SLO 与错误预算

概念定义示例
SLI可测量的服务质量指标成功请求占比、P99 延迟
SLOSLI 的目标值99.9% 请求成功(30 天窗口)
SLA对外的合同承诺低于 SLO,含赔偿条款
错误预算1 - SLO 允许的失败量0.1% × 总请求数

错误预算驱动决策:预算充足时可以加快发布;预算耗尽则冻结变更、优先修复稳定性。基于错误预算的告警(burn rate)比单点阈值告警更符合业务:它直接回答「按当前错误速度,多久会耗尽预算」。


7 OpenTelemetry Collector 部署模式

模式部署方式优点缺点适用
Agent(DaemonSet)每节点一个 Collector本地聚合、减少应用直连后端节点资源开销大多数集群的默认
Gateway(Deployment)中心化 Collector 集群统一采样与脱敏、后端解耦单点需高可用多团队、统一策略
Sidecar每 Pod 一个隔离性最好资源开销大、维护复杂特殊合规场景
应用直连后端无 Collector简单无法统一处理、暴露后端地址本地开发

推荐组合:DaemonSet agent + Deployment gateway。应用发 OTLP 到本节点 agent,agent 批处理后转发到 gateway,gateway 统一采样、脱敏并写入各后端。这样应用只依赖一个本机地址,后端更换不影响应用。


8 成本控制

手段收益代价
头部采样(head sampling)采集端就减少数据量,成本最低可能丢掉出错的请求
尾部采样(tail sampling)保留错误与慢请求的全链路Collector 需要缓存整个 trace,内存开销大
指标标签控制避免基数爆炸丢失部分维度
日志级别动态调整平时少记,排障时全记需要动态配置能力
保留期分层热数据 7 天、冷数据 30-90 天查询冷数据变慢
按服务差异化核心服务全量、边缘服务采样需要治理策略

尾部采样是多语言系统里性价比最高的手段:在 Collector 中配置「错误 trace 100% 保留、慢请求 100% 保留、正常请求 1% 保留」,既保住排障能力,又把成本压到可控。


9 常见坑

症状规避
高基数标签Prometheus 内存暴涨、查询变慢禁止把 user_idorder_idtrace_id 放进指标标签
日志打爆磁盘节点磁盘压力、Pod 被 Evicted控制 INFO 量、采样、设置日志轮转与采集限流
时间戳格式不统一跨服务日志无法按时间对齐全部 UTC + RFC3339,禁止本地时间
多行堆栈被拆成多行错误日志难读用结构化字段 error.stack 或采集器多行解析
trace 在消息队列断开链路只到生产者显式在消息 header 注入/提取 traceparent
指标命名不一致无法写统一查询遵循 Prometheus 命名规范与 OTel Semantic Conventions
采样率一刀切要么丢错误要么成本爆炸尾部采样 + 错误优先保留
只有 CPU 监控服务错误率无人知晓强制每个服务提供 RED 指标
告警过多on-call 麻木、真告警被忽略只对可行动事件告警,定期清理
采集器成为瓶颈遥测数据丢失Collector 设置资源 limits 与队列重试,横向扩容

10 本章小结

  1. 可观测性三支柱各司其职:日志查根因、指标看趋势、追踪定瓶颈,用 trace_id 关联
  2. 全语言统一数据约定:JSON 日志到 stdout、UTC 时间、统一字段名与指标命名
  3. 指标遵循 RED/USE 方法,Histogram 用于延迟,PromQL 是查询与告警的基础
  4. 追踪依赖 W3C TraceContext 传播,HTTP/gRPC/MQ 都要显式传递,否则链路断裂
  5. OpenTelemetry 提供厂商中立的 SDK 与 Collector,推荐 agent + gateway 部署模式
  6. SLO 与错误预算是把监控升级为可靠性治理的关键一步;成本控制靠采样与保留期策略

动手实践

  1. 统一日志格式:用两种语言各写一个 HTTP 服务,按本章约定输出 JSON 日志(含 trace_id),用 Docker Compose 启动后把日志同时输出到控制台与 Loki(可用 Grafana Loki 单机版)。
    • 验收标准:两个服务的日志字段完全一致;在 Grafana 中能用同一查询(如 | json | level="ERROR")同时检索两个服务。
  2. 指标与告警:给两个服务接入 Prometheus 客户端,暴露请求数与延迟 Histogram;编写 PromQL 计算错误率与 P99,并配置一条错误率告警规则。
    • 验收标准:Prometheus 中能看到两个服务的指标;人为制造错误后告警在设定时间内触发;能解释 ratehistogram_quantile 的含义。
  3. 跨语言链路追踪:用 OpenTelemetry 为 Go 网关与 Python 服务接入追踪,确保 traceparent 正确传播,并在 Tempo/Jaeger 中看到完整链路。
    • 验收标准:一条请求的 trace 包含两个以上服务的 span;故意让下游变慢后能定位到具体 span;日志中的 trace_id 与追踪中的一致。
  4. 成本实验:在 OTel Collector 中配置尾部采样(错误全留、正常 1%),对比采样前后的数据量。
    • 验收标准:正常请求的 trace 数量显著下降,但人为制造的 500 请求 100% 保留;能说明头部与尾部采样的取舍。