Kubernetes 基础体系 · 第 68/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 分布式追踪:OpenTelemetry、Context、采样和基础设施关联
分布式追踪(Distributed Tracing)用于回答一个具体问题:
一次用户请求经过哪些服务、每个服务耗时多少、在哪一步失败,以及这次请求运行在哪些 Kubernetes 资源上?
指标通常告诉我们“错误率升高了”,日志通常告诉我们“某个请求报错了”,而追踪把一次请求在多个进程、线程、Pod 和节点之间的传播过程串成一条因果链。
在 Kubernetes 中,追踪系统通常由以下部分组成:
flowchart LR
C[客户端] --> A[API Gateway / Ingress]
A --> S1[订单服务]
S1 --> S2[库存服务]
S1 --> DB[(数据库)]
S2 --> MQ[(消息队列)]
A -. trace context .-> S1
S1 -. trace context .-> S2
A -->|OTLP| O[OpenTelemetry Collector]
S1 -->|OTLP| O
S2 -->|OTLP| O
O --> B[(Trace Backend)]
S1 --> L[日志]
S1 --> M[指标]
L -. trace_id / span_id .-> B
M -. exemplars .-> B
OpenTelemetry 负责生成、传播和导出遥测数据;Context 负责在一次调用链中携带当前 Span;采样决定哪些 Trace 被保留;Kubernetes 关联负责把 Trace 与 Namespace、Pod、容器、Node、Workload 等基础设施信息连接起来。
这几个概念必须放在一起理解。只有创建 Span 而不传播 Context,Trace 会断裂;只有 Trace 而没有采样策略,生产环境可能产生无法承受的数据量;只有业务字段而没有 Kubernetes 资源属性,出现问题时又难以定位到具体 Pod。
一、先建立追踪模型:Trace、Span 和因果关系
1.1 Trace 是一次完整请求的因果图
一个 Trace 表示一次逻辑操作的完整过程,例如:
用户请求
└── API Gateway
└── order-service
├── inventory-service
└── PostgreSQL 查询
Trace 通常包含:
- 一个
trace_id:标识整条调用链; - 多个 Span:表示调用链中的局部操作;
- Span 之间的父子关系或链接关系;
- 每个 Span 的开始时间、结束时间、状态、属性和事件。
Span 是一个有开始和结束时间的操作区间:
如果父 Span 包含子 Span,则可以近似表示:
但“包含”不意味着父 Span 的耗时等于所有子 Span 耗时之和。子 Span 可能并发执行,也可能存在没有被观测的本地计算。
例如:
order-service 总耗时:120 ms
├── inventory-service:80 ms
└── payment-service:70 ms
两个下游调用并发执行时,父 Span 仍然可能只耗时约 120 ms,而不是 80 + 70 = 150 ms。
1.2 Span 的常见字段
一个 Span 通常包含:
| 字段 | 含义 |
|---|---|
trace_id |
整条 Trace 的标识 |
span_id |
当前 Span 的标识 |
parent_span_id |
父 Span 标识 |
start_time、end_time |
操作时间范围 |
name |
操作名称 |
kind |
SERVER、CLIENT、PRODUCER、CONSUMER、INTERNAL 等 |
status |
UNSET、OK、ERROR |
| attributes | HTTP、RPC、数据库、消息队列等属性 |
| events | 时间点事件,例如异常 |
| links | 与其他 Trace 或 Span 的关联 |
Span.kind 不是装饰字段。它帮助后端判断一个 Span 在调用关系中的角色。例如:
- HTTP 服务端收到请求:
SERVER; - HTTP 客户端调用下游:
CLIENT; - 消息生产者发送消息:
PRODUCER; - 消费者异步处理消息:
CONSUMER。
1.3 追踪不是调用树的简单日志拼接
追踪需要表达因果关系,而不是单纯按时间排序。
同步 HTTP 调用通常是父子关系:
server span: order-service 接收请求
└── client span: order-service 调用 inventory-service
└── server span: inventory-service 接收请求
异步消息通常需要区分两个时间点:
Trace A
└── PRODUCER: 订单服务发送消息
Trace B
└── CONSUMER: 库存服务消费消息
└── LINK -> Trace A 的 PRODUCER Span
消费者处理可能在很久之后发生,也可能被多个消费者并行处理。此时强行把消费者 Span 作为生产者 Span 的子 Span,往往会制造不准确的树结构;使用 Span Link 更能表达“这个处理由那条消息触发”。
二、OpenTelemetry 负责什么,Kubernetes 不负责什么
2.1 OpenTelemetry 的边界
OpenTelemetry(OTel)是开放的可观测性框架,主要提供:
- API:应用如何创建 Span、设置属性和传播 Context;
- SDK:采样、处理、批量导出和资源识别;
- 自动插桩:为 HTTP、数据库、消息队列等库自动创建 Span;
- OTLP 协议:统一向 Collector 或后端发送数据;
- Collector:接收、处理、批量发送和路由遥测数据。
OpenTelemetry 不等于某一个 Trace 后端。Jaeger、Tempo、Zipkin、商业 APM 后端可以作为不同的存储和查询系统。
2.2 Kubernetes 的边界
Kubernetes 负责调度和管理容器,但不会自动把任意应用调用串成 Trace。应用必须:
- 使用 OTel SDK 或自动插桩;
- 正确传播 Context;
- 配置 OTLP 导出;
- 为运行环境提供资源属性;
- 处理采样和故障。
Kubernetes 自身的一些组件或发行版可能提供追踪能力,但这属于具体组件和版本的实现能力,不能推导为“所有 Kubernetes 请求都有 Trace”。
例如,下面这些对象本身不会自动生成业务 Span:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
Deployment 只描述期望状态。真正需要创建 Span 的是运行在 Pod 中的应用、代理或网关。
三、Context:Span 为什么能跨进程传播
3.1 Context 的定义
OpenTelemetry 中的 Context 是与当前执行流程关联的上下文容器。它通常包含:
- 当前 Span;
- 当前 Trace 的传播信息;
- Baggage;
- 取消、超时或框架上下文中的其他状态。
需要区分两件事:
- 进程内 Context:在同一个进程的函数调用、线程、协程或异步任务之间传递;
- 跨进程传播格式:把 Context 编码到 HTTP Header、RPC Metadata 或消息属性中。
Context 不会凭空穿过网络。必须执行:
进程 A 的 Context
-- Inject -->
HTTP Header / RPC Metadata / Message Header
-- 网络传输 -->
进程 B
-- Extract -->
进程 B 的 Context
3.2 W3C Trace Context
最常见的 HTTP 传播格式是 W3C Trace Context,主要使用:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
格式可抽象为:
version-trace-id-parent-id-trace-flags
其中:
version:传播格式版本;trace-id:32 个十六进制字符,标识整条 Trace;parent-id:16 个十六进制字符,标识当前上游 Span;trace-flags:目前主要包含 sampled 标志。
traceparent 中的 parent-id 不是下游 Span 的 ID,而是“传播者当前 Span 的 ID”。下游提取后会创建新的 Span,并把这个 parent-id 作为新 Span 的父 Span。
还可以使用:
tracestate: vendor1=value1,vendor2=value2
tracestate 用于携带供应商或实现相关的信息。应用不应随意修改未知字段。
3.3 Baggage 与 Trace Context 不同
Baggage 是任意键值对,例如:
baggage: tenant.id=tenant-42,region=cn-east
它可以跨服务传递业务上下文,但有重要风险:
- 会增加每次请求的 Header 大小;
- 可能把敏感数据传播到不应访问的服务;
- 下游服务可能信任不可信的外部值;
- 高频高基数字段会增加日志、指标或 Trace 成本。
因此,不应把密码、Token、身份证号等敏感信息放入 Baggage,也不应把所有用户字段都放入 Span 属性。
3.4 进程内异步代码中的 Context
下面是一个 Python 示例,展示显式创建 Span、读取当前 Context,并在异步任务中保留当前上下文:
import asyncio
from opentelemetry import trace
tracer = trace.get_tracer("demo")
async def query_inventory():
current = trace.get_current_span()
print("inventory parent span:", current.get_span_context().span_id)
await asyncio.sleep(0.01)
async def handle_order():
with tracer.start_as_current_span("handle_order") as span:
span.set_attribute("order.id", "order-1001")
await query_inventory()
asyncio.run(handle_order())
在支持的 Python 异步模型中,OpenTelemetry 使用运行时上下文机制保存当前 Span。常见错误是把 Span 放到一个全局变量:
# 错误示例
CURRENT_SPAN = span
并发请求会覆盖这个全局变量,导致请求 A 的 Span 被请求 B 使用。正确做法是使用框架支持的 Context 机制,或者使用 OTel API 提供的当前 Span 访问方式。
不同语言的运行时行为不同:
- Java 通常依赖线程上下文和异步框架集成;
- Go 通常显式传递
context.Context; - Python 依赖
contextvars和异步框架适配; - Node.js 通常依赖
AsyncLocalStorage。
因此,“已经创建 Span”不等于“Context 会自动在所有并发边界中正确传播”。线程池、协程、消息回调、定时任务和自定义执行器都需要验证。
四、HTTP 和消息队列中的传播过程
4.1 HTTP 请求的完整时序
一次 HTTP 调用可以拆成以下步骤:
sequenceDiagram
participant U as Client
participant A as order-service
participant B as inventory-service
participant C as Collector
U->>A: HTTP request + traceparent
A->>A: Extract parent Context
A->>A: Create SERVER Span
A->>A: Create CLIENT Span
A->>B: HTTP request + new traceparent
B->>B: Extract Context
B->>B: Create SERVER Span
B-->>A: HTTP response
A-->>U: HTTP response
A->>C: OTLP spans
B->>C: OTLP spans
关键点是:A 不应把收到的 traceparent 原样转发给 B。A 应先创建自己的客户端 Span,再把该客户端 Span 作为当前 Span 注入下游。
如果 A 直接原样转发:
U -> A -> B
那么 B 可能错误地把 U 的原始 Span 当成自己的父 Span,丢失 A 作为中间调用方的节点。
4.2 手动传播的伪代码
不同语言 API 名称略有不同,但生命周期应当一致:
ctx = propagator.extract(incoming_headers)
with tracer.start_span(
name="HTTP GET /orders/{id}",
context=ctx,
kind=SERVER
) as server_span:
with tracer.start_span(
name="HTTP GET inventory",
context=current_context(),
kind=CLIENT
) as client_span:
outgoing_headers = {}
propagator.inject(current_context(), outgoing_headers)
response = http_client.get(url, headers=outgoing_headers)
错误处理也属于 Span 生命周期的一部分:
try:
response = call_downstream()
if response.status >= 500:
span.set_status(ERROR)
except Exception as exc:
span.record_exception(exc)
span.set_status(ERROR)
raise
通常不应把完整请求体、响应体或异常堆栈全部塞入属性。异常事件可以包含堆栈,但必须考虑敏感信息和数据量。
4.3 消息传播
消息系统没有统一的 HTTP Header,但通常提供消息属性或 Header:
message.headers["traceparent"] = inject(current_context())
producer.send(message)
消费者收到消息后:
parent_context = extract(message.headers)
with start_consumer_span(
"consume order.created",
context=parent_context,
kind=CONSUMER
):
process(message)
实际生产中还需要考虑:
- 消息重试是否创建新的 Consumer Span;
- 死信队列是否保留原始传播字段;
- 同一消息多次消费是否产生多个 Span;
- 批量消费时一个 Span 是否覆盖多个消息;
- 消费处理是否跨越较长时间,导致父子关系不适合表达。
五、一个可运行的最小 OpenTelemetry 应用
下面用 Python Flask 展示最小的 HTTP 服务端和客户端插桩。示例假设本机有一个 OTLP gRPC 接收端监听 localhost:4317,可以是本地 Collector 或测试后端。
安装依赖:
python -m venv .venv
. .venv/bin/activate
pip install \
flask \
requests \
opentelemetry-api \
opentelemetry-sdk \
opentelemetry-exporter-otlp-proto-grpc \
opentelemetry-instrumentation-flask \
opentelemetry-instrumentation-requests
创建 app.py:
import os
import requests
from flask import Flask, jsonify
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
resource = Resource.create({
"service.name": os.getenv("OTEL_SERVICE_NAME", "order-service"),
"service.version": "1.0.0",
"deployment.environment": os.getenv("OTEL_ENVIRONMENT", "dev"),
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317"),
insecure=True,
)
)
)
trace.set_tracer_provider(provider)
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
@app.get("/orders/<order_id>")
def get_order(order_id):
response = requests.get(
os.getenv("INVENTORY_URL", "http://localhost:8081/inventory/") + order_id,
timeout=2,
)
return jsonify({
"order_id": order_id,
"inventory_status": response.status_code,
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
启动:
export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
export OTEL_SERVICE_NAME=order-service
python app.py
调用:
curl -i http://localhost:8080/orders/order-1001
预期结果是 Flask 自动创建一个服务端 Span,Requests 自动创建一个客户端 Span。若 inventory-service 也正确提取 traceparent,它会创建属于同一 trace_id 的服务端 Span。
这里有几个生命周期细节:
TracerProvider应在应用启动阶段初始化;BatchSpanProcessor会异步发送 Span,减少请求线程的导出开销;- 进程退出时应调用 SDK 的 shutdown 或 flush,避免最后一批 Span 丢失;
- exporter 不可用时,业务请求不应无限等待;
- OTLP endpoint、协议和 TLS 配置必须与 Collector 接收器一致。
自动插桩能覆盖常见库,但不能保证覆盖所有自定义线程池、RPC 框架、消息客户端或数据库封装。需要通过实际 Trace 验证父子关系是否正确,而不能只根据“安装成功”判断。
六、OpenTelemetry Collector:接收、处理和导出
6.1 Collector 的数据流
Collector 通常包含四类组件:
Receiver -> Processor -> Exporter
|
Pipeline
- Receiver:接收 OTLP、Jaeger、Zipkin 等协议;
- Processor:批量、限流、补充属性、采样、过滤;
- Exporter:发送到 Trace 后端;
- Extension:健康检查、认证、负载均衡等辅助能力。
一个最小 OTLP Collector 配置:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
batch:
timeout: 5s
send_batch_size: 512
exporters:
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
4317 是常见的 OTLP gRPC 端口,4318 是常见的 OTLP HTTP 端口。端口本身不是 Kubernetes 规范要求,应用和 Collector 必须使用相同协议和地址。
启动方式取决于发行版和安装包。例如使用 Collector 二进制时:
otelcol-contrib --config collector.yaml
不同 Collector 构建版本包含的组件可能不同。otelcol 与 otelcol-contrib 的组件集合不完全相同,配置中的 receiver、processor 或 exporter 必须存在于实际构建中。
debug exporter 适合确认数据是否进入 Collector,不适合生产持久化。生产环境通常将其替换为后端对应的 OTLP exporter,例如:
exporters:
otlp:
endpoint: tempo.monitoring.svc.cluster.local:4317
tls:
insecure: true
是否允许明文、是否需要认证、后端是否要求特定协议,取决于具体产品和部署方式。生产环境应优先使用 TLS 和认证,而不是照搬 insecure: true。
6.2 Collector 在 Kubernetes 中的部署模式
常见部署模式有两种。
DaemonSet Agent:
每个 Node 一个 Collector
Pod -> 本机 Agent -> Gateway / Backend
优点是网络路径短,适合收集本节点 Pod 的遥测;缺点是每个节点都需要资源,配置和升级数量较多。
Deployment Gateway:
多个 Pod -> Collector Gateway -> Trace Backend
优点是集中处理、统一出口和统一采样;缺点是需要处理负载均衡、故障转移和跨节点网络。
常见组合是:
应用或 Sidecar -> DaemonSet Agent -> Gateway -> Backend
如果使用尾部采样(Tail Sampling),同一条 Trace 的 Span 必须尽量被送到同一个做决策的 Collector 实例,否则一个实例只看到 Trace 的一部分,可能错误地把失败 Trace 当成成功 Trace。
七、把 Collector 部署到 Kubernetes
下面的配置用于演示 OTLP 接收和 Kubernetes 元数据关联。它不是完整生产清单,但可以说明关键权限和数据流。
7.1 ServiceAccount、RBAC 和配置
apiVersion: v1
kind: ServiceAccount
metadata:
name: otel-collector
namespace: observability
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-collector
rules:
- apiGroups: [""]
resources: ["pods", "namespaces", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets", "deployments"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: otel-collector
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: otel-collector
subjects:
- kind: ServiceAccount
name: otel-collector
namespace: observability
---
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector
namespace: observability
data:
collector.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
k8sattributes:
auth_type: serviceAccount
passthrough: false
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.pod.uid
- k8s.node.name
- k8s.deployment.name
- k8s.replicaset.name
- k8s.container.name
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.uid
- sources:
- from: resource_attribute
name: k8s.pod.ip
- sources:
- from: connection
batch:
timeout: 5s
send_batch_size: 512
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
exporters:
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [debug]
这里的关键点不是 YAML 的数量,而是以下因果关系:
k8sattributes需要访问 Kubernetes API;- ServiceAccount 本身不自动拥有权限;
- ClusterRole 和 ClusterRoleBinding 让 Collector 能读取 Pod、Namespace、Node 等对象;
- Collector 根据资源属性、Pod IP 或连接信息关联 Pod;
- 关联成功后,Processor 把 Kubernetes 属性写入 Resource;
- 导出到后端后,可以按 Namespace、Pod 或 Deployment 查询。
k8sattributes 的关联成功率取决于 Collector 是否能看到正确的资源信息。某些网络拓扑、代理转发或自定义 exporter 会丢失 Pod IP 和连接信息,因此不能假定仅部署 Processor 就一定能关联成功。
7.2 Collector Deployment 和 Service
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: observability
spec:
replicas: 2
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
serviceAccountName: otel-collector
containers:
- name: collector
image: otel/opentelemetry-collector-contrib:0.123.0
args:
- "--config=/etc/otelcol/collector.yaml"
ports:
- name: otlp-grpc
containerPort: 4317
- name: otlp-http
containerPort: 4318
volumeMounts:
- name: config
mountPath: /etc/otelcol
readinessProbe:
httpGet:
path: /
port: 13133
volumes:
- name: config
configMap:
name: otel-collector
---
apiVersion: v1
kind: Service
metadata:
name: otel-collector
namespace: observability
spec:
selector:
app: otel-collector
ports:
- name: otlp-grpc
port: 4317
targetPort: 4317
- name: otlp-http
port: 4318
targetPort: 4318
上面的 Deployment 使用了示例版本标签。部署前应确认该镜像标签在目标仓库存在,并根据组织的镜像镜像审计、漏洞扫描和升级策略固定版本。不要在生产环境无审计地使用 latest。
配置中的 readiness 探针端口 13133 还需要 Collector 启用 health check extension;如果没有启用,该探针不会正常工作。一个可用配置应补充:
extensions:
health_check:
endpoint: 0.0.0.0:13133
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, resource, batch]
exporters: [debug]
部署和验证:
kubectl create namespace observability
kubectl apply -f collector.yaml
kubectl -n observability get pods
kubectl -n observability get svc otel-collector
kubectl -n observability logs deploy/otel-collector
如果应用配置:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.observability.svc.cluster.local:4317
则它会通过 Kubernetes Service 访问 Collector。若使用 OTLP gRPC,应用端应配置 gRPC exporter;若使用 OTLP HTTP,则通常使用 http://...:4318,具体是否需要 /v1/traces 由 SDK exporter 实现决定。
八、Resource 属性和 Kubernetes 基础设施关联
8.1 Resource 与 Span Attribute 的区别
Resource 描述“产生遥测数据的实体”,例如:
service.name=order-service
service.version=1.4.2
k8s.namespace.name=commerce
k8s.pod.name=order-service-7d8b9c6f5d-x7abc
k8s.node.name=node-03
Span Attribute 描述“某一次操作”,例如:
http.request.method=GET
http.route=/orders/{id}
server.address=inventory
db.operation.name=SELECT
判断原则是:
- 对同一进程或同一 Pod 中大多数 Span 都相同的字段,优先放 Resource;
- 每次请求可能不同的字段,放 Span Attribute;
- 不能把
user.id、order.id等高基数字段随意变成指标标签。
8.2 Kubernetes 关联的实际数据流
典型流程如下:
应用 SDK
└── service.name、service.version、Pod 相关环境变量
↓
OTLP
↓
Collector k8sattributes processor
├── 根据 k8s.pod.uid 关联
├── 或根据 k8s.pod.ip 关联
├── 或根据连接地址关联
└── 查询 Kubernetes API
↓
补充 Namespace、Pod、Node、Deployment 等 Resource
↓
Trace Backend
Kubernetes Downward API 可以把部分信息注入环境变量,例如:
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
但应用自行设置:
k8s.pod.name=$(POD_NAME)
并不等于该值已经被 Kubernetes 认证。外部请求可以伪造 Header,应用也可能配置错误。生产环境通常让 Collector 通过 Kubernetes API 进行关联,并限制可访问权限。
8.3 Pod 会变化,Workload 身份更稳定
Pod 名称和 UID 在重建后会变化:
order-service-7d8b9c6f5d-x7abc
order-service-7d8b9c6f5d-k9pqr
但 Deployment 名称和 service.name 通常更适合长期聚合。查询时可以按层次使用:
service.name=order-service
k8s.namespace.name=commerce
k8s.deployment.name=order-service
k8s.pod.name=具体实例
不要只按 Pod 名称做长期告警维度,否则滚动发布会产生大量新时间序列或 Trace 查询条件失效。
九、采样:为什么不能保存所有 Trace
9.1 采样的基本定义
采样是决定一个请求的 Span 是否进入后续处理和存储的过程。
设请求到达率为 ,每条 Trace 平均 Span 数为 ,每个 Span 平均大小为 ,采样率为 ,则粗略数据量为:
例如:
- 每秒 1000 条请求;
- 每条 Trace 平均 8 个 Span;
- 每个 Span 平均 1 KB;
- 采样率 10%。
则:
这只是原始 Span 数据,不包含索引、压缩、网络协议和存储副本开销。
9.2 Head Sampling
Head Sampling 在 Trace 开始时就做决定,常见实现包括:
- 固定比例采样;
- Parent-based sampling;
- 按服务或环境配置不同采样率。
优点:
- 决策简单;
- 不需要等待整条 Trace;
- 资源占用较小;
- 适合在应用 SDK 或入口 Collector 提前削减流量。
缺点是决定时还不知道请求最终是否失败。例如一条请求刚开始时看起来正常,后面却发生数据库超时;如果最初没有采样,这条错误 Trace 可能完全不存在。
9.3 Parent-based Sampling
Parent-based Sampling 使用上游采样决定:
上游 sampled=true -> 下游通常继续采样
上游 sampled=false -> 下游通常不采样
这能保证同一条 Trace 大体一致。否则可能出现:
Gateway 保留了 Trace
order-service 没保留
inventory-service 又保留了一个孤立 Span
但 Parent-based Sampling 不是安全边界。外部客户端可以构造 traceparent,所以不能无条件信任外部传入的 sampled 标志。入口网关通常需要重新建立信任边界,并根据自身策略决定是否接受上游采样决策。
9.4 Tail Sampling
Tail Sampling 等整条 Trace 的一部分或全部 Span 到达后再决策:
1. 收集 Trace 的 Span
2. 按 trace_id 暂存
3. 等待 decision_wait
4. 查看错误、延迟、属性等
5. 决定保留或丢弃
它可以实现:
错误 Trace:100% 保留
延迟超过 1 秒:100% 保留
正常 Trace:随机保留 5%
概念配置如下:
processors:
tail_sampling:
decision_wait: 10s
num_traces: 10000
expected_new_traces_per_sec: 100
policies:
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
- name: keep-slow
type: latency
latency:
threshold_ms: 1000
- name: sample-normal
type: probabilistic
probabilistic:
sampling_percentage: 5
这里的 decision_wait 是等待时间,不是无限等待。若某些 Span 到达过晚,Collector 可能已经作出决定,导致 Trace 不完整。
Tail Sampling 的容量至少受到以下因素影响:
其中:
- :同时处于等待状态的 Trace 数;
- :每条 Trace 的 Span 数;
- :每个 Span 占用的内存;
- 还应加上 Collector 内部索引、队列和运行时开销。
在多个 Collector 副本之间,必须考虑 Trace 路由一致性。普通 Kubernetes Service 的随机负载均衡不能天然保证同一个 trace_id 总是到达同一个 Tail Sampling 实例。
9.5 采样不是只保留“成功”和“失败”
如果只按错误采样,可能丢失:
- 具有特定租户或区域的异常;
- 只在某个 Pod 上发生的延迟;
- 业务成功但性能严重下降的请求;
- 采样决策自身造成的空洞。
合理策略通常需要组合:
错误 Trace 100%
慢 Trace 100%
关键业务操作较高比例
正常流量低比例
开发环境按需提高
但“错误 100% 保留”也不代表绝对不会丢失。Collector 崩溃、网络断开、后端限流、内存保护和应用进程退出,都可能导致数据丢失。
十、采样与 Context 的关系
采样决定不仅影响后端是否收到 Span,还会影响传播标志。
一个常见流程是:
入口 Span 创建
↓
Sampler 作出记录决策
↓
trace_flags.sampled 设置
↓
向下游注入 traceparent
↓
下游依据 Parent-based 策略继续或停止记录
因此,采样决策应在调用下游之前确定。如果应用在创建下游客户端 Span 后才改变采样状态,下游可能已经收到不一致的传播信息。
还要区分:
- recording:SDK 是否记录 Span 内容;
- sampled:是否建议将 Trace 发送到后端;
- exported:是否实际成功发送到后端。
一个 Span 可能被创建但没有导出,也可能因为导出失败而丢失。不能仅凭应用日志中出现“Span created”判断后端一定能查询到 Trace。
十一、Trace、日志、指标和 SLO 如何关联
11.1 日志关联
日志中通常加入:
trace_id=4bf92f3577b34da6a3ce929d0e0e4736
span_id=00f067aa0ba902b7
这样可以从一条错误日志跳转到 Trace,也可以从 Trace 中定位同一时间段的日志。
但日志中的 trace_id 必须来自当前 Context,而不是应用启动时生成的固定值。以下做法是错误的:
应用启动时生成一个 trace_id,所有日志都使用它
这种日志看似具有关联字段,实际会把所有请求错误地归为同一条 Trace。
Kubernetes 容器日志通常由容器运行时写入标准输出,再由 Fluent Bit、Vector、Filebeat 或其他 Agent 收集。日志 Agent 不会自动知道某条日志属于哪个 Trace;应用需要将字段写入日志,或者使用能够读取 OTel Context 的日志集成。
11.2 指标关联和 Exemplars
指标适合聚合,例如:
http_server_request_duration_seconds
http_server_requests_total
Trace 适合分析单次请求。Prometheus 指标不应把 trace_id 作为普通 Label,因为 Trace ID 几乎每次都不同,会制造极高基数。
Exemplar 是指标样本与某条 Trace 的关联,例如:
latency histogram bucket
exemplar:
trace_id=4bf92f3577b34da6a3ce929d0e0e4736
这样可以从延迟直方图中的异常点跳转到具体 Trace。Exemplar 需要:
- SDK 或指标库支持;
- 指标导出链路保留 exemplar;
- Prometheus 及查询界面支持;
- Trace 后端能够按 Trace ID 查询。
因此,“用了 Prometheus 就能从指标跳 Trace”并不是 Kubernetes 或 Prometheus 单独保证的能力。
11.3 Metrics、Trace 和 SLO 的分工
以延迟 SLO 为例:
99% 的订单请求延迟小于 500 ms
Prometheus 适合计算:
满足延迟目标的请求比例
错误预算消耗速度
按服务、区域、版本聚合的延迟
Trace 适合分析一次不满足目标的请求:
Gateway:20 ms
order-service:35 ms
inventory-service:410 ms
数据库查询:380 ms
SLO 不应直接依赖所有 Trace。Trace 可能被采样,不能代替完整的请求计数和延迟分布指标。
十二、与 Service Mesh 的关系:Sidecar、mTLS 和遥测
Service Mesh 通常通过 Sidecar、节点代理或环境内代理处理:
- 服务间流量转发;
- mTLS;
- 重试、超时和流量治理;
- 部分网络指标和访问日志;
- 可选的代理级 Span。
12.1 mTLS 不等于 Trace 传播
mTLS 解决的是:
通信双方身份认证 + 传输加密
Trace Context 解决的是:
请求因果关系传播
连接使用 mTLS,并不意味着代理会自动转发 traceparent。反过来,HTTP Header 中有 traceparent 也不代表连接经过加密。
12.2 Sidecar 可能产生重复 Span
如果应用和 Sidecar 都生成 HTTP Span,一次调用可能出现:
应用 SERVER Span
应用 CLIENT Span
Sidecar 出站 Span
Sidecar 入站 Span
下游 Sidecar 出站 Span
下游应用 SERVER Span
这不是必然错误,但必须理解每个 Span 的边界。否则一次 10 ms 的调用可能被误解为多个串行调用。
应明确:
- 应用负责业务语义和数据库、缓存、内部函数等 Span;
- 代理负责网络层、路由、重试和连接级遥测;
- 哪一方负责传播 Header;
- 是否需要在后端隐藏或折叠某些代理 Span;
- 重试是否被显示为多个尝试。
12.3 重试会改变因果图
假设客户端第一次调用超时,Sidecar 自动重试一次:
业务请求
├── 第一次网络尝试:失败
└── 第二次网络尝试:成功
如果只看业务服务端 Span,可能只看到成功的一次;如果只看客户端 Span,可能看到两次尝试。诊断网络问题时需要代理遥测,但计算业务请求耗时时,应避免把重试次数误当成业务请求数。
12.4 Service Mesh 与 Collector 的连接方式
常见连接方式包括:
应用 SDK -> Collector
Sidecar -> Collector
Sidecar 产生的 Span -> Collector
如果应用和 Sidecar 都向 Collector 发送 Trace,Collector 可以统一批处理和导出;但采样策略必须协同,否则可能出现:
- 应用采样后没有业务 Span,Sidecar 仍产生孤立 Span;
- Sidecar 在入口丢弃了请求,应用无法保留错误 Trace;
- 多级代理重复注入或覆盖传播 Header。
对于跨信任边界的流量,入口代理应验证和清理外部传播字段,而不是无条件相信客户端传入的 traceparent 和 Baggage。
十三、Kubernetes 中最容易被忽略的后台任务
分布式追踪常被设计成“HTTP 请求进来、HTTP 请求出去”,但 Kubernetes 应用还有大量非请求触发的工作:
- CronJob 定时任务;
- Deployment 启动和优雅退出;
- 消费消息;
- 控制器 Reconcile;
- 异步任务队列;
- Leader Election 后执行的任务;
- 预热缓存和数据库迁移。
这些任务没有自然的上游 HTTP Context。可以创建新的根 Trace:
CronJob execution
└── reconcile inventory
├── database query
└── publish message
对于 Kubernetes Controller,还要避免把一次长期运行的 Reconcile 生命周期错误地绑定到某个已经结束的请求 Context。Context 的取消和超时应符合控制器的实际工作边界。
Pod 终止时,SDK 和 Collector 需要有时间发送缓冲数据。Kubernetes 的 terminationGracePeriodSeconds 只是给进程退出的宽限时间;应用必须在收到终止信号后停止接收新请求、flush exporter,并在超时后退出。
十四、故障路径:Trace 丢失时如何定位
14.1 应用没有 Trace
检查顺序:
kubectl -n commerce exec deploy/order-service -- printenv \
| grep '^OTEL_'
确认:
OTEL_SERVICE_NAME是否存在;- OTLP endpoint 是否指向正确的 Service;
- gRPC 和 HTTP 协议是否匹配;
- TLS 配置是否匹配;
- 自动插桩是否真的加载;
- 应用是否创建了
TracerProvider; - exporter 是否在进程退出前 flush。
然后检查 DNS 和端口:
kubectl -n commerce exec deploy/order-service -- \
getent hosts otel-collector.observability.svc.cluster.local
若容器内没有 getent,可使用临时诊断 Pod。不要因为业务容器缺少诊断工具就修改生产镜像。
14.2 Collector 收到数据但后端没有
检查:
kubectl -n observability logs deploy/otel-collector
kubectl -n observability describe pod -l app=otel-collector
重点观察:
- exporter 连接错误;
- TLS 或认证错误;
- 后端限流;
memory_limiter丢弃;- exporter queue 满;
- Collector 重启;
- 配置加载失败。
可以暂时使用 debug exporter 验证 Collector 是否收到 Trace。如果 Debug exporter 有输出,而正式 exporter 没有,问题在后端连接、认证、协议或导出队列。
14.3 Trace 断裂
Trace 断裂通常不是 Kubernetes Service 的问题,而是传播链问题:
A 有 trace_id
B 没有 trace_id
检查:
- A 是否注入
traceparent; - Service Mesh、Ingress 或 API Gateway 是否删除或覆盖 Header;
- B 是否提取正确的 Header;
- B 是否创建了新的根 Span;
- HTTP 客户端是否被自动插桩;
- 异步任务是否在 Context 之外执行;
- 消息队列是否保留消息属性。
可以在测试环境记录非敏感的传播字段进行比对:
A outgoing traceparent
B incoming traceparent
B created span trace_id
不要在生产日志中无控制地打印完整 Baggage 或请求 Header。
14.4 Kubernetes 属性缺失
如果 Trace 有 service.name,但没有 k8s.pod.name,检查:
- Collector 是否启用了
k8sattributes; - ServiceAccount 是否有读取权限;
- ClusterRole 是否包含需要的资源;
- 应用是否发送了可关联的资源属性;
- Collector 是否在正确的网络和集群中;
- Pod IP 是否因代理或网络转换而不可见。
查看权限:
kubectl auth can-i \
--as=system:serviceaccount:observability:otel-collector \
get pods --all-namespaces
kubectl auth can-i \
--as=system:serviceaccount:observability:otel-collector \
list nodes
输出 yes 只能证明 RBAC 允许访问,不代表属性关联一定成功;还要在最终 Trace 中验证属性是否出现。
十五、常见误解与反例
15.1 “有 trace_id 就代表 Trace 完整”
反例:
Gateway: trace_id=T, span_id=A
order-service: trace_id=T, span_id=B
inventory-service: trace_id=T, span_id=C
虽然三者的 trace_id 相同,但如果父子关系错误、Span 时间不合理或中间节点缺失,依然不能还原真实调用链。
15.2 “采样率 10% 就一定保留 10% 的错误”
错误。随机 Head Sampling 下,错误 Trace 也只有约 10% 被保留。若需要保留错误 Trace,应使用入口分类、SDK 策略或 Tail Sampling。
15.3 “把 trace_id 放进 Prometheus Label 就能关联”
这会导致近似每个请求产生一个新的 Label 值:
http_requests_total{trace_id="..."}
时间序列数量会迅速膨胀。应使用 Exemplars 或从日志、Trace 后端跳转。
15.4 “Service Mesh 已经生成 Trace,应用不需要插桩”
代理能观察网络调用,但通常不知道:
- 业务订单号;
- 数据库查询;
- 业务阶段;
- 下游调用的业务语义;
- 应用内部的异步工作。
代理遥测和应用遥测是互补关系,不是简单替代关系。
15.5 “Collector 是可靠队列”
Collector 通常是内存中的处理和转发组件。进程崩溃、节点故障、网络中断或队列耗尽都可能丢失数据。磁盘队列、持久化缓冲、后端重试和多副本部署能降低风险,但也会增加磁盘、延迟和运维复杂度。
15.6 “所有 Kubernetes 属性都应该放到 Span 上”
将 k8s.pod.name 写到每个 Span 上并不一定错误,但会重复数据。Resource 属性更适合描述遥测来源;Span 属性用于描述具体操作。后端如何索引和展示 Resource 也存在实现差异,因此要用目标后端验证查询语法。
十六、生产取舍:可靠性、成本和隐私
16.1 不要让追踪阻塞业务请求
导出失败时,应用不应因为 Trace 后端不可用而无限等待。常见控制手段包括:
- BatchSpanProcessor;
- exporter 超时;
- 有界队列;
- 内存限制;
- 丢弃低优先级 Trace;
- Collector 与业务进程解耦;
- 后端限流和重试。
观测系统的故障应尽量降级为“看不到数据”,而不是变成“业务请求全部失败”。
16.2 属性设计必须控制基数
适合作为 Span Attribute 的字段:
http.route=/orders/{id}
rpc.method=GetInventory
db.system=postgresql
k8s.namespace.name=commerce
需要谨慎的字段:
user.id
order.id
session.id
raw.url
exception.stacktrace
高基数属性可以帮助单次诊断,但可能提高索引和存储成本;同一个字段是否可搜索、是否参与聚合,取决于后端实现。
16.3 脱敏和信任边界
Trace、日志和 Baggage 都可能携带敏感信息。应明确:
- 哪些 Header 允许传播;
- 哪些外部 Header 必须清理;
- 哪些属性需要哈希或截断;
- 是否记录 SQL 参数;
- 是否记录请求体;
- 后端访问权限和保留期限。
尤其不能把认证 Token、Cookie、密码或完整 Authorization Header 当作普通 Span 属性。
十七、一个可验证的端到端检查标准
部署后,至少应验证以下链路,而不是只检查 Pod 是否为 Running:
kubectl -n observability get pods
kubectl -n observability logs deploy/otel-collector
kubectl -n commerce get pods -o wide
然后发起一次请求:
curl -v http://order-service.example/orders/order-1001
验证结果:
- 入口服务生成一个
SERVERSpan; - 下游调用生成一个
CLIENTSpan; - 下游服务生成一个
SERVERSpan; - 这些 Span 具有相同的
trace_id; - 父子关系符合真实调用方向;
- Collector 能接收 OTLP;
- Trace 后端能查询到该 Trace;
- Trace 包含
service.name; - Trace 能关联到 Namespace、Deployment 或 Pod;
- 错误请求的 Span 状态和异常事件符合预期;
- 应用关闭或 Pod 终止时,最后一批 Span 不会大量丢失;
- 指标、日志和 Trace 的跳转关系经过实际验证。
可以构造一个确定失败的测试请求,例如让下游服务返回 HTTP 500,然后检查:
order-service CLIENT Span: status=ERROR 或记录错误事件
inventory-service SERVER Span: status=ERROR
Trace Backend: 能按 trace_id 查询完整链路
日志: 包含相同 trace_id
需要注意,HTTP 500 是否自动设置 Span 状态为 ERROR,取决于具体插桩库和版本;不能仅凭 HTTP 状态码假设所有 SDK 行为完全一致,应在目标语言和版本中验证。
结语:把追踪看成一条有信任边界的因果数据流
Kubernetes 中的分布式追踪不是“安装一个 Collector”这么简单,它至少包含四条必须闭合的链路:
Span 创建
-> Context 在进程内正确传播
-> traceparent 跨进程正确传播
-> Collector 接收、采样和导出
-> Trace 与日志、指标、Kubernetes 资源关联
其中:
- OpenTelemetry 定义和实现遥测 API、SDK、插桩与 OTLP;
- Context 决定当前 Span 能否跨函数、线程、协程和进程传播;
- 采样决定可观测性成本与异常保留能力之间的取舍;
- Kubernetes 关联把抽象的服务调用映射到具体的 Namespace、Workload、Pod 和 Node;
- Service Mesh 可以补充网络层遥测,但 mTLS、流量治理和 Trace Context 传播解决的是不同问题。
真正可靠的追踪系统,不是拥有最多 Span,而是在发生故障时,能够以可接受的成本恢复正确的因果关系,并进一步回答:哪次请求、经过哪个服务、在哪个 Pod、由哪个版本、在什么基础设施条件下失败。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes 日志架构:stdout、Node Agent、Sidecar、采集和留存
- 下一篇:Kubernetes 容量规划:Request、利用率、碎片、故障余量和压测
- 延伸:Kubernetes 指标与监控:Metrics Server、Prometheus、告警和 SLO
- 延伸:Kubernetes Service Mesh:Sidecar、mTLS、流量治理、遥测和边界
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论