在 Kubernetes 中构建统一的 OpenTelemetry 流水线
去年发生一起生产环境故障时,我打开了所有标准工具来调试支付失败问题。Grafana 显示 CPU 峰值,CloudWatch 中的日志分散在三个服务中,Jaeger 展示了 50 条外观相似的追踪数据。二十分钟过去了,我依然无法回答一个基本问题:“哪条追踪数据对应实际失败的请求?” 告警提示我们支付功能已中断,日志显示错误信息,追踪数据也存在,但这些数据之间毫无关联。最后,我只能在日志组中逐个搜索请求 ID,才找到问题根源。
问题不在于工具或数据——我们两者都不缺,而在于“关联”的缺失。在之前关于 kube-prometheus-stack 的文章中,我解释过为何监控仪表盘不等于可观测性;本文将展示如何借助 OpenTelemetry 真正实现可观测性:你将搭建一条统一的流水线,让指标、日志和追踪数据在共享上下文的环境中流转,只需几秒(而非几小时)就能从告警定位到具体失败的追踪数据。
OpenTelemetry 解决了厂商锁定(及其他一系列问题)
OpenTelemetry 是 CNCF(云原生计算基金会)的毕业项目,提供与厂商无关的 instrumentation 库和 Collector(收集器),能够大规模接收、处理和导出遥测数据。你无需将应用代码与特定厂商绑定,只需通过 OTel SDK 完成一次埋点,即可将遥测数据路由到任意所需的后端。
在一个自由职业项目中,我曾将系统从 kube-prometheus-stack 迁移到 OTel。当时我们需要自定义指标、日志和追踪数据,而“厂商锁定”是核心顾虑:kube-prometheus-stack 虽能处理基础的 Prometheus 指标,但要添加分布式追踪,就必须额外集成独立系统;且厂商服务的成本会迅速攀升。
借助 OTel,我只需对应用进行一次埋点,就能灵活评估不同后端——无需修改代码。我们最初使用自托管的 Grafana,两周后测试某商业厂商服务时,仅修改了 Collector 的导出器配置,未对应用做任何调整。这种灵活性正是 OTel 的核心优势。
但“厂商灵活性”并非最主要的价值,OTel 真正的价值在于集中式增强与关联:所有经过 Collector 的信号(指标、日志、追踪数据)都会附加相同的 Kubernetes 元数据(Pod、命名空间、团队注解)、遵循相同的采样规则、共享相同的追踪上下文。这意味着日志中的 service.name 和 trace_id 与追踪数据一致,而追踪数据的属性又与指标属性匹配。
当所有数据共享上下文时,故障排查过程中就能在不同信号间自由切换,无需手动关联时间戳或靠猜测定位问题。
Collector 的三种部署方式
大多数团队在部署 Collector 时存在误区:要么给每个 Pod 都部署 Sidecar(边车),导致 YAML 配置爆炸;要么用 DaemonSet 部署到所有节点,却疑惑为何节点会内存耗尽。
OTel Collector 可通过 Sidecar、DaemonSet 或集中式网关(Gateway)三种模式部署,每种模式各有取舍:
| 部署模式 | 描述 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| Sidecar(边车) | 每个 Pod 部署一个 Collector 容器 | 隔离性强、支持按服务控制、延迟最低 | 每个工作负载需额外 YAML 配置、配置缩放难度高 | 高安全性工作负载或对延迟敏感的应用 |
| DaemonSet(代理) | 每个节点部署一个 Collector | 运维简单、可收集主机+Pod 遥测数据、配置清单少 | 处理能力受节点 CPU/内存限制、不适合重处理 | 集群级广泛覆盖、轻量数据转换场景 |
| Gateway(部署) | 集中式 Collector 服务 | 配置集中、支持重处理、易于扇出(fan-out) | 多一次网络跳转、存在潜在瓶颈 | 集中式策略管控、采样、多后端路由 |
我采用的方案是:在每个节点用 DaemonSet 部署代理(Agent)负责数据收集,同时部署一个 Gateway 负责数据处理。Agent 将原始信号转发到 Gateway,由 Gateway 执行数据增强、采样和路由。这种架构能避免节点资源过载(许多团队因在 DaemonSet 代理中执行重处理导致节点内存耗尽,务必避免此做法),同时将复杂配置集中在一处管理。
在 Kubernetes 中安装 OpenTelemetry Operator
你可以通过原始配置清单部署 Collector,但 OpenTelemetry Operator 提供了 OpenTelemetryCollector CRD(自定义资源定义),能自动处理服务发现和 RBAC(基于角色的访问控制)。该 Operator 需要 cert-manager 来支持准入 Webhook,部署步骤如下:
-
先安装 cert-manager
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml等待约一分钟,确保 cert-manager 就绪。
-
安装 OpenTelemetry Operator
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts helm repo update helm install opentelemetry-operator open-telemetry/opentelemetry-operator \ --namespace opentelemetry-operator \ --create-namespace \ --set manager.replicas=2 -
验证安装结果
kubectl -n opentelemetry-operator get pods其中
manager.replicas=2是为了确保高可用。安装完成后,你只需定义 Collector 自定义资源,Operator 会自动创建所需的其他资源。
备选方案:通过 opentelemetry-kube-stack Helm 图表安装
若你倾向于用单个 Helm 图表集成常用组件,可选择 opentelemetry-kube-stack 图表。它能快速搭建集群级遥测基础,适合作为初始方案;后续若需更精细的控制,再将配置拆分到由 Operator 管理的独立 Collector 即可。
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm install otel-kube-stack open-telemetry/opentelemetry-kube-stack \
--namespace observability \
--create-namespace
查看图表的配置选项和结构:opentelemetry-kube-stack
OpenTelemetry Gateway Collector 配置
Gateway 接收基于 OTLP(OpenTelemetry 协议)的信号——OTLP 是标准的传输协议,支持通过 gRPC(4317 端口)或 HTTP(4318 端口)承载指标、日志和追踪数据。Gateway 的核心功能是为信号附加 Kubernetes 元数据、执行智能采样,并将数据导出到后端。其中关键组件是 k8sattributes 处理器,配置如下:
processors:
k8sattributes:
auth_type: serviceAccount
extract:
metadata: [k8s.namespace.name, k8s.pod.name, k8s.deployment.name]
annotations:
- tag_name: team
key: team
from: pod
- tag_name: runbook.url
key: runbook-url
from: pod
该配置会自动为所有信号附加 Kubernetes 元数据,并从 Pod 注解中提取自定义字段(如 team 和 runbook-url),使每条追踪数据、指标和日志都包含“服务归属”信息和故障修复文档链接。在故障排查时,这能帮你省去在 Wiki 或 Slack 中查找“服务负责人”的时间。
采样配置:基于尾部的智能采样
我采用尾部采样(tail-based sampling) 策略,保留 100% 的错误请求和慢请求数据。若你的应用每天处理 1000 万次请求,若存储所有追踪数据,会迅速耗尽存储资源并影响查询性能;采样则能在保留关键数据的同时减少无效存储。
基础的概率采样(如随机保留 10% 数据)存在缺陷:它对所有追踪数据一视同仁,可能会遗漏关键的错误追踪。而尾部采样更智能:它会等待追踪数据完整生成后,再根据规则决定是否保留,配置如下:
tail_sampling:
decision_wait: 10s # 等待 10 秒确保追踪数据完整
policies:
- name: errors-first
type: status_code
status_code: {status_codes: [ERROR]} # 保留 100% 错误请求
- name: slow-requests
type: latency
latency: {threshold_ms: 2000} # 保留 100% 耗时超 2 秒的请求
- name: probabilistic-sample
type: probabilistic
probabilistic: {sampling_percentage: 10} # 其他请求保留 10%
这种配置能确保关键问题(错误、慢请求)的可见性,同时减少 80%-90% 的存储消耗。建议初始采样率设为 10%,后续根据查询模式为高价值流程适当提高采样率。
内存限制配置
memory_limiter 处理器能在内存使用率接近阈值时,通过“背压”(back-pressure)机制限制接收器(receiver)的数据接收,避免 Collector 因内存溢出(OOM)被杀死:
memory_limiter:
check_interval: 1s # 每秒检查一次内存使用
limit_mib: 3072 # 内存上限 3GB
spike_limit_mib: 800 # 内存峰值上限 800MB
部署 Gateway
完整的 Gateway 配置(包含所有接收器、导出器和资源限制)可在 gateway.yaml 中查看,部署命令如下:
kubectl create namespace observability # 创建命名空间
kubectl apply -f gateway.yaml # 部署 Gateway
kubectl -n observability get pods -l app.kubernetes.io/name=otel-gateway # 验证部署
若需通过 prometheusremotewrite 导出器将指标导出到 Prometheus,需确保 Prometheus 启动时添加参数 --web.enable-remote-write-receiver。
备选方案:直接对接原生支持远程写入的后端(如 Grafana Mimir、Cortex、Thanos),或使用 Collector 的 prometheus 导出器,再配置 Prometheus 抓取 Collector 的数据。
DaemonSet 代理(Agent)配置
Agent 的配置需保持精简,仅负责“接收-批量处理-转发”流程,避免占用过多节点资源:
spec:
mode: daemonset # 以 DaemonSet 模式部署
config: |
receivers:
# 接收 OTLP 信号(HTTP 和 gRPC 协议)
otlp:
protocols: {http: {endpoint: 0.0.0.0:4318}, grpc: {endpoint: 0.0.0.0:4317}}
# 收集主机指标(CPU、内存、磁盘、网络)
hostmetrics:
scrapers: [cpu, memory, disk, network]
processors:
# 批量处理数据,减少网络请求次数
batch: {timeout: 5s}
# 内存限制,避免节点资源过载
memory_limiter:
check_interval: 1s
limit_mib: 400
spike_limit_mib: 100
exporters:
# 将数据转发到 Gateway
otlp:
endpoint: otel-gateway.observability.svc.cluster.local:4317
service:
pipelines:
# 追踪数据流水线:接收 → 内存限制 → 批量处理 → 转发到 Gateway
traces: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp]}
# 指标流水线:接收(OTLP + 主机指标)→ 内存限制 → 批量处理 → 转发
metrics: {receivers: [otlp, hostmetrics], processors: [memory_limiter, batch], exporters: [otlp]}
# 日志流水线:接收 → 内存限制 → 批量处理 → 转发
logs: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp]}
Agent 会收集主机指标和 Pod 发送的 OTLP 信号,批量处理后转发到 Gateway。完整配置可参考 agent.yaml,部署命令如下:
kubectl apply -f agent.yaml # 部署 Agent
# 验证部署(-o wide 可查看 Agent 所在节点)
kubectl -n observability get pods -l app.kubernetes.io/name=otel-agent -o wide
应用埋点(Instrumentation)
要让 Collector 正常工作,应用需主动输出遥测信号。以下是一个基于 Python Flask 框架的应用示例,已集成 OpenTelemetry 追踪功能:
from flask import Flask, request
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.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
# 定义资源属性(所有遥测信号共享的上下文)
resource = Resource.create({
"service.name": "checkout-service", # 服务名(关键关联字段)
"service.version": "v1.0.0", # 服务版本
"deployment.environment": "production", # 部署环境
"team": "payments", # 负责团队
})
# 初始化追踪提供者
trace_provider = TracerProvider(resource=resource)
# 配置 OTLP 导出器,将追踪数据发送到 Agent
otlp_exporter = OTLPSpanExporter(
endpoint="http://otel-agent.observability.svc.cluster.local:4318/v1/traces"
)
# 添加批量 span 处理器(减少网络开销)
trace_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
# 设置全局追踪提供者
trace.set_tracer_provider(trace_provider)
# 初始化 Flask 应用并自动埋点
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app) # 自动捕获 Flask 请求的追踪数据
# checkout 接口示例
@app.route("/checkout", methods=["POST"])
def checkout():
# 创建自定义 span(追踪 checkout 业务逻辑)
with trace.get_tracer(__name__).start_as_current_span("checkout") as span:
# 添加业务属性(如用户 ID),便于故障排查
span.set_attribute("user.id", request.json.get("user_id"))
# 业务逻辑(此处省略)
return {"status": "success"}, 200
部署应用时添加注解
部署应用的 Pod 时,需添加注解以便 Collector 自动发现和关联元数据:
metadata:
annotations:
team: "payments" # 与应用埋点中的 team 属性一致
runbook-url: "https://runbooks.internal/payments/checkout" # 故障修复文档链接
此时,每条追踪数据都会包含 service.name、team 和 runbook.url。故障排查时,你可在 Grafana 中按“团队”筛选数据,并直接访问修复文档。
关联是核心:打通指标、日志与追踪数据
统一流水线的价值,仅在故障排查时能“跨信号导航”才得以体现:当你看到“错误率过高”的告警,需要查看对应服务的日志,再定位到具体失败的追踪数据。若缺乏关联,你只能在三个工具中手动匹配时间戳,靠猜测寻找正确请求;而有了完善的关联,只需点击几次就能从告警跳转到日志再到追踪数据。
要实现这一点,需满足三个条件:
- 所有信号(指标、日志、追踪)共享一致的资源属性(如
service.name、team); - 每条日志都注入追踪上下文(
trace_id和span_id); - 在 Grafana 中配置数据源关联,支持跨信号跳转。
为日志注入追踪上下文
通过 OTel SDK 可将 trace_id 和 span_id 注入日志,示例代码如下(基于上述 Flask 应用扩展):
import logging
from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
# 复用之前定义的资源(确保与追踪数据的上下文一致)
# resource = Resource.create({"service.name": "checkout-service", ...})
# 初始化日志导出器(发送日志到 Agent)
log_exporter = OTLPLogExporter(
endpoint="http://otel-agent.observability.svc.cluster.local:4318/v1/logs"
)
# 初始化日志提供者
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
# 配置 Python 标准日志模块,注入 OTel 上下文
handler = LoggingHandler(logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)
# 复用之前的追踪器
tracer = trace.get_tracer(__name__)
@app.route("/checkout", methods=["POST"])
def checkout():
with tracer.start_as_current_span("checkout") as span:
# 获取当前 span 的上下文(包含 trace_id 和 span_id)
span_context = trace.get_current_span().get_span_context()
# 记录日志时注入 trace_id 和 span_id
logging.info(
"Processing checkout", # 日志消息
extra={
"trace_id": format(span_context.trace_id, "032x"), # 格式化 trace_id(32 位十六进制)
"span_id": format(span_context.span_id, "016x"), # 格式化 span_id(16 位十六进制)
"user_id": request.json.get("user_id") # 业务属性(用户 ID)
}
)
# 业务逻辑(此处省略)
return {"status": "success"}, 200
配置完成后,你可在 Grafana 中搭建这样的仪表盘:展示 Prometheus 错误率告警、按 service.name 和 trace_id 筛选的 Loki 日志,以及展示完整请求流程的 Tempo 追踪数据。点击告警 → 查看日志 → 跳转追踪数据,故障排查时间可从几小时缩短到几分钟。
生产环境前的验证
部署到生产环境前,需通过冒烟测试验证流水线是否正常工作:
-
端口转发 Gateway 的 4318 端口(用于发送测试追踪数据):
kubectl -n observability port-forward svc/otel-gateway 4318:4318 -
发送测试追踪数据(模拟应用输出的 OTLP 信号):
curl -X POST http://localhost:4318/v1/traces \ -H "Content-Type: application/json" \ -d '{ "resourceSpans": [{ "resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "test-service"}}]}, "scopeSpans": [{ "spans": [{ "traceId": "5b8aa5a2d2c872e8321cf37308d69df2", "spanId": "051581bf3cb55c13", "name": "test-span", "kind": 1, "startTimeUnixNano": "1609459200000000000", "endTimeUnixNano": "1609459200500000000" }] }] }] }' -
验证追踪数据是否到达后端(以 Tempo 为例,端口转发后查询):
kubectl -n monitoring port-forward svc/tempo 3100:3100 curl http://localhost:3100/api/search?q=test-service # 搜索 test-service 的追踪数据
若指标、日志和追踪数据均能到达对应后端,说明流水线正常。包含完整指标、日志和追踪数据验证的脚本可参考 test-pipeline.sh。
运维与扩展
需将 OTel Collector 配置视为应用代码进行管理:将配置清单存储在 Git 中,配置变更需通过 PR 审批,按“开发环境→预发环境→生产环境”的顺序部署,并对 Collector 的健康状态(队列大小、数据丢弃率、CPU/内存使用率)设置告警。
为 Gateway 配置水平自动扩缩容(HPA)
基于 CPU 使用率为 Gateway 配置 HPA,确保高负载时能自动扩容:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: otel-gateway
namespace: observability
spec:
scaleTargetRef: # 目标扩容对象(Gateway Deployment)
apiVersion: apps/v1
kind: Deployment
name: otel-gateway
minReplicas: 2 # 最小副本数(确保高可用)
maxReplicas: 10 # 最大副本数
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 使用率阈值 70%
监控 Collector 关键指标
Collector 会在 :8888/metrics 端口暴露自身指标,需重点监控以下指标并设置告警:
otelcol_receiver_accepted_spans:接收器已接收的 span 数量otelcol_receiver_refused_spans:接收器拒绝的 span 数量(需告警)otelcol_exporter_sent_spans:导出器已发送的 span 数量otelcol_exporter_send_failed_spans:导出器发送失败的 span 数量(需告警)otelcol_processor_batch_batch_send_size:批量处理器的发送大小(监控批量效率)
若“拒绝数”或“发送失败数”突增,需及时排查 Collector 资源限制、后端连接或配置问题。
Grafana 跨信号导航配置
要实现“指标→日志→追踪数据”的点击跳转,需在 Grafana 中配置数据源关联。以下是关键配置步骤:
1. Tempo(追踪)→ Loki(日志)关联
在 Tempo 数据源中添加“追踪到日志”的链接,点击追踪数据时可直接查看对应日志:
{
"datasourceUid": "loki-uid", // Loki 数据源的 UID
"tags": [{"key": "service.name", "value": "service_name"}], // 共享 service.name 属性
"query": "{service_name=\"${__field.labels.service_name}\"} |~ \"${__span.traceId}\"" // Loki 查询语句(按服务名和 trace_id 筛选)
}
2. Loki(日志)→ Tempo(追踪)关联
在 Loki 数据源中添加“日志到追踪”的链接,点击日志中的 trace_id 可直接跳转追踪数据:
{
"datasourceUid": "tempo-uid", // Tempo 数据源的 UID
"field": "trace_id", // 日志中存储 trace_id 的字段
"url": "/explore?left={\"datasource\":\"tempo-uid\",\"queries\":[{\"query\":\"${__value.raw}\"}]}" // 跳转 URL
}
配置完成后,在 Tempo 中查看追踪数据时,可点击“Logs for this trace”查看所有相关日志;在 Loki 中查看日志时,点击 trace_id 字段可直接打开对应的 Tempo 追踪数据。
迁移后的实际收益
去年我主导了一个生产环境迁移项目,将三个独立的代理(Prometheus 导出器、Fluentd、Jaeger 代理)整合为单一的 OTel 流水线。三个月后,我们观察到以下显著收益:
- 故障排查时间:中位数从 90 分钟缩短至 25 分钟,工程师无需在四个工具间切换,一个带跨链接的 Grafana 仪表盘即可满足需求。
- 存储成本:追踪采样使存储消耗减少 85%,且未损失调试能力。
- 运维效率:每条信号都包含
team和runbook.url属性,彻底消除了“谁负责这个服务?”的疑问。
最大的收益并非技术层面,而是运维流程的优化:值班工程师无需猜测,只需遵循固定路径排查问题——告警 → 仪表盘 → 追踪数据 → 修复文档。当你能从 Prometheus 告警一键跳转到 Loki 日志,再定位到 Tempo 中具体失败的追踪数据时,可观测性就从“理论概念”变成了“实用工具”。
下一步建议
- 先部署 Gateway 和 Agent,完成基础流水线搭建;
- 选择一个核心服务进行埋点,验证遥测数据的收集与关联;
- 在 Grafana 中配置跨信号导航,测试“告警→日志→追踪”的跳转流程;
- 逐步将所有服务迁移到 OTel 流水线,同时优化采样策略和配置。
理解 Kubernetes 部署模式(如 DaemonSet、Deployment)有助于确保监控基础设施的可靠性,尤其是在集群升级时需保证 Collector 持续运行。
扩展:将安全信号纳入统一流水线
当你已搭建好指标、日志、追踪数据的统一流水线后,可进一步将安全信号整合进来。我的另一篇文章《Security Observability in Kubernetes Goes Beyond Logs》(Kubernetes 安全可观测性:不止于日志)详细介绍了如何将 Kubernetes 审计日志、Falco 运行时检测和网络流量可见性接入 OpenTelemetry 流水线,实现“安全事件→请求追踪”的关联,帮助你定位触发安全事件的具体请求。
原文链接:https://fatihkoc.net/posts/opentelemetry-kubernetes-pipeline/