1. 引言:为什么 Mesh 层是可观测性的最佳切入点
微服务架构把单体拆成了几十上百个服务,随之而来的是调用链路变长、故障定位变难。传统做法是在每个业务服务里埋点、打日志、上报指标,但这种方式有三个绕不开的问题:
- 侵入性强:每个服务都要引入 SDK,语言不同、框架不同,埋点方式五花八门;
- 覆盖不全:老服务、第三方服务、异构语言服务往往没有埋点,链路在中间就断了;
- 升级困难:埋点 SDK 升级需要业务方配合发版,周期长、阻力大。
Service Mesh 把服务间通信从业务进程中剥离出来,交给 Sidecar 代理(如 Envoy)统一处理。这意味着所有进出服务的流量都会经过代理,代理天然具备采集可观测性数据的位置优势。本文围绕分布式追踪、指标、日志三条主线,讲清楚如何在 Mesh 层构建一套统一的可观测性方案。
下面这张图概括了本文的三条主线如何汇聚成一套统一的可观测性方案:
2. Mesh 层可观测性的独特价值:服务间通信的全景视图
2.1 从"应用视角"到"网络视角"
应用层埋点看到的是"我的服务处理了什么请求",而 Mesh 层看到的是"服务 A 到服务 B 之间发生了什么"。后者能回答很多前者回答不了的问题:
- 请求在网络上耗时多少?是服务处理慢还是网络慢?
- 连接是否被重置?是否触发了重试?重试了几次?
- 上游返回的 5xx 是业务错误还是代理层错误(如上游连接超时)?
- 双向 TLS 握手是否成功?mTLS 证书是否即将过期?
2.2 零侵入与语言无关
Sidecar 代理以独立进程运行,与业务语言完全解耦。Java、Go、Python、Node.js 服务只要接入 Mesh,就自动获得统一的指标、追踪和访问日志能力,无需改动一行业务代码。
2.3 统一的数据口径
所有服务的流量都经过同一种代理,意味着指标名称、标签维度、日志格式天然统一。这为后续的聚合分析、跨服务对比、SLO 计算打下了基础------不用再为"每个团队指标命名不一样"而头疼。
2.4 全景视图的边界
需要客观说明的是,Mesh 层看不到服务内部的逻辑。一个请求在服务内部调用了数据库、缓存、下游 HTTP 接口,这些细节只有应用层埋点才能覆盖。因此最佳实践是 Mesh 层 + 应用层结合:Mesh 层负责服务间通信的全景,应用层负责服务内部的细节,两者通过统一的 Trace Context 串联起来。
下图对比了应用层与 Mesh 层各自覆盖的观测范围,以及两者通过 Trace Context 衔接的方式:
3. OpenTelemetry 与 Mesh 的集成:Trace Context 跨代理传播
3.1 分布式追踪的核心:上下文传播
分布式追踪的本质是把一次请求经过的所有环节串成一棵树。串联的纽带就是 Trace Context------一组随请求头传递的标识:
traceparent:W3C 标准头,格式为00-<trace-id>-<parent-id>-<flags>;tracestate:可选的厂商扩展信息;- 兼容格式:B3(Zipkin)、Jaeger 等。
只要链路中每个环节都能正确读取并透传这些头,追踪就能连起来。
Trace Context 在一次跨服务调用中的传播过程可以这样理解:
3.2 Envoy 的追踪能力
Envoy 内置了追踪支持,通过 tracing 配置指定上报的集群和采样策略:
yaml
tracing:
http:
name: envoy.tracers.opentelemetry
typed_config:
"@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig
grpc_service:
envoy_grpc:
cluster_name: otel_collector
service_name: mesh-proxy
关键点在于:Envoy 会在入站请求 时读取上游传来的 traceparent,生成自己的 span,并在出站请求时把新的上下文注入到请求头中。这样即使业务代码完全没有埋点,服务间的调用关系也能被完整记录下来。
3.3 跨代理传播的注意事项
实际落地时有几个坑需要留意:
- 头透传白名单 :部分 Mesh 实现(如 Istio)默认会清理未识别的请求头,需要确认
traceparent、tracestate在透传白名单内; - 采样一致性 :如果每个代理独立采样,可能出现上游采样了下游没采样、链路断裂的情况。建议在入口处统一决策采样,并通过
traceparent的 flags 位向下传递; - 应用层与代理层的衔接:如果业务服务也用了 OpenTelemetry SDK,需要保证 SDK 与 Envoy 使用同一套传播格式,否则会出现两条互不相连的链路;
- 异步消息场景:Kafka、RabbitMQ 等消息中间件的上下文传播需要额外配置,不能只依赖 HTTP 头。
3.4 一个典型的链路形态
在这条链路中,每个 Envoy 都会产生入站和出站两个 span,业务服务如果也有埋点则再补充内部 span。最终在 Tempo/Jaeger 中看到的是一棵完整的调用树,能清晰区分"网络耗时"和"服务处理耗时"。
4. 指标采集:从 Envoy 指标到 RED 方法论
4.1 Envoy 暴露了哪些指标
Envoy 的指标体系分为三层:
- Downstream :入站方向,描述代理接收到的请求,如
envoy_http_downstream_rq_total、envoy_http_downstream_rq_time; - Upstream :出站方向,描述代理发往上游的请求,如
envoy_cluster_upstream_rq_total、envoy_cluster_upstream_rq_time; - Server:代理自身状态,如内存、连接数、线程数。
这些指标默认以 Prometheus 格式暴露在管理端口(通常是 :15090/stats/prometheus),可以直接被 Prometheus 抓取。
4.2 RED 方法论
RED 是面向服务的指标方法论,三个字母分别代表:
- Rate:请求速率(QPS);
- Errors:错误率;
- Duration:响应时长分布。
RED 三个维度与 Envoy 指标、观测目标的对应关系如下:
相比 USE(面向资源)方法论,RED 更贴合服务间通信的观测需求。基于 Envoy 指标可以这样构造:
promql
# Rate:每秒请求数
sum(rate(envoy_http_downstream_rq_total[1m])) by (destination_service)
# Errors:5xx 错误率
sum(rate(envoy_http_downstream_rq_xx{response_code_class="5"}[1m])) by (destination_service)
/
sum(rate(envoy_http_downstream_rq_total[1m])) by (destination_service)
# Duration:P99 响应时间
histogram_quantile(0.99,
sum(rate(envoy_http_downstream_rq_time_bucket[1m])) by (le, destination_service)
)
4.3 指标标签的规划
指标的价值很大程度上取决于标签维度。建议至少保留以下标签:
| 标签 | 说明 | 用途 |
|---|---|---|
source_workload |
调用方工作负载 | 定位问题来源 |
destination_workload |
被调方工作负载 | 定位问题目标 |
destination_service |
目标服务名 | 服务级聚合 |
response_code |
HTTP 状态码 | 错误分析 |
response_flags |
Envoy 响应标志 | 区分超时/重置/无健康上游 |
其中 response_flags 尤其有用:UF 表示上游连接失败、UO 表示上游溢出、NR 表示无路由,这些标志能快速把"5xx"细分成具体原因。
4.4 指标采集的架构
对于大规模集群,建议使用 Prometheus 的联邦或 Thanos/VictoriaMetrics 等方案做长期存储与跨集群聚合,避免单实例压力过大。
5. 日志关联:Trace ID 与访问日志的联动分析
5.1 访问日志的价值
指标告诉你"出问题了",追踪告诉你"问题在哪条链路",而日志告诉你"具体发生了什么"。Envoy 的访问日志可以记录每个请求的详细信息,包括响应码、耗时、上游地址、响应标志等。
5.2 在访问日志中注入 Trace ID
这是日志与追踪联动的关键一步。Envoy 支持在访问日志格式中使用 %REQ(X-REQUEST-ID)% 或通过 %DYNAMIC_METADATA% 提取 trace id:
yaml
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
log_format:
json_format:
timestamp: "%START_TIME%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
protocol: "%PROTOCOL%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
duration_ms: "%DURATION%"
upstream_host: "%UPSTREAM_HOST%"
trace_id: "%REQ(X-B3-TRACEID)%"
span_id: "%REQ(X-B3-SPANID)%"
注意:不同追踪格式对应的头名不同。使用 W3C 标准时,traceparent 是一个复合头,需要额外解析;使用 B3 格式时可以直接取 X-B3-TraceId。
5.3 日志采集与关联查询
日志通过 Fluent Bit / Vector 等采集器收集后写入 Loki 或 Elasticsearch。关联分析的关键是用同一个 Trace ID 作为查询条件:
- 在 Grafana 看板上发现某个服务的 P99 突增;
- 点击图表下钻,跳转到 Tempo 查看该时段的慢链路;
- 选中一条慢链路,拿到
trace_id; - 在 Loki 中用
{app="order"} |= "<trace_id>"查询该请求的所有相关日志。
这条路径把"指标发现问题 → 追踪定位链路 → 日志还原细节"串成了一个闭环。
整个关联排查的闭环流程如下:
5.4 日志字段规范建议
为了让关联查询高效,建议统一日志字段:
trace_id、span_id:必填,用于关联;service_name、pod_name:用于过滤;response_flags:用于快速筛选异常请求;duration_ms:便于排序找慢请求。
同时注意日志量控制:全量记录访问日志在高 QPS 场景下成本很高,建议对错误请求和慢请求做全量记录,正常请求按比例采样。
6. 实战:基于 Prometheus + Grafana + Tempo 的 Mesh 可观测性栈
6.1 整体架构
6.2 部署 OpenTelemetry Collector
Collector 承担追踪数据的接收、批处理和转发:
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
6.3 配置 Prometheus 抓取 Envoy 指标
yaml
scrape_configs:
- job_name: 'envoy-sidecars'
metrics_path: /stats/prometheus
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_container_name]
action: keep
regex: istio-proxy
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: destination_workload
- source_labels: [__address__]
regex: '(.+):\d+'
target_label: __address__
replacement: '${1}:15090'
6.4 Grafana 看板设计
一个实用的 Mesh 可观测性看板通常包含四块:
- 全局概览:整体 QPS、错误率、P99 延迟,按服务分组;
- 服务详情:选中某个服务后,展示其上下游调用关系、各状态码分布;
- 异常下钻 :按
response_flags分类的错误趋势,点击可跳转 Tempo; - 日志面板:嵌入 Loki 查询,展示当前时间窗口内的错误日志。
Grafana 的 Exemplar 功能可以把 trace_id 直接挂在指标图表上,点击数据点即可跳转到对应的追踪,这是打通指标与追踪最优雅的方式。
6.5 告警规则示例
yaml
groups:
- name: mesh-slo
rules:
- alert: HighErrorRate
expr: |
sum(rate(envoy_http_downstream_rq_xx{response_code_class="5"}[5m])) by (destination_service)
/
sum(rate(envoy_http_downstream_rq_total[5m])) by (destination_service)
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.destination_service }} 5xx 错误率超过 5%"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(envoy_http_downstream_rq_time_bucket[5m])) by (le, destination_service)
) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.destination_service }} P99 延迟超过 1s"
6.6 落地过程中的经验
- 采样策略要分层:入口全采样、内部按比例采样,既保证关键链路完整,又控制成本;
- 指标基数要控制 :避免把
path、user_id等高基数字段作为标签,否则 Prometheus 内存会迅速膨胀; - 日志与追踪的存储要分开:Tempo 存追踪、Loki 存日志,通过 trace_id 关联,不要试图用一套存储解决所有问题;
- 先建看板再谈告警:没有稳定的看板基线,告警阈值很难定准,容易产生大量误报。
7. 总结
Service Mesh 为可观测性提供了一个统一、零侵入的数据采集层。通过 OpenTelemetry 的 Trace Context 实现跨代理传播,通过 Envoy 指标落地 RED 方法论,通过 Trace ID 把访问日志与追踪关联起来,最终在 Prometheus + Grafana + Tempo 的技术栈上形成"指标发现问题、追踪定位链路、日志还原细节"的完整闭环。
需要强调的是,Mesh 层可观测性不是应用层可观测性的替代品,而是补充。两者结合、口径统一,才能真正做到对分布式系统的全面掌控。