随着汤不热吧技术社区的容器化规模持续扩张——目前生产集群已承载超过 120 个微服务、日均处理请求量峰值突破 800 万——传统的 Prometheus + 单点 Grafana 监控方案逐渐暴露出三大痛点:指标、日志、链路数据分散存储导致排障时需要在多个系统间来回跳转;跨服务调用的分布式追踪链路不完整,P99 延迟归因常常需要人工逐跳拼接;以及随着链路数据量增长,Jaeger 单机存储成为瓶颈。为彻底解决这些问题,社区基础设施团队历时六周,完成了基于 OpenTelemetry 统一采集、Grafana Tempo 存储链路、Grafana Loki 存储日志、Grafana Mimir 存储指标的全链路可观测性平台搭建。本文记录整体架构设计、关键配置细节与踩坑经验,供同行参考。
一、为什么选择 OpenTelemetry 而非独立探针方案
在立项之初,团队对比了三种主流方案:继续沿用 Prometheus + Jaeger + ELK 的组合并做横向扩展;采用 Datadog/Splunk 等商业 APM;以及全面拥抱 CNCF 推动的 OpenTelemetry(OTel)标准。最终选择 OTel 的核心理由有三点:
- 避免厂商锁定:OTel 是 CNCF 的第二代可观测性标准,将数据采集与后端存储彻底解耦。后端可在 Tempo/Loki/Mimir 之间自由切换,甚至未来引入商业 APM 也无需改造业务侧埋点。
- 三支柱统一采集:Metrics、Logs、Traces 通过同一套 SDK 和 Collector 采集,共享资源标签与采样策略,从根源上解决了”三方数据对不齐”的问题。
- 生态成熟度:截至 2026 年中,OTel 已支持 11 种语言的稳定版 SDK,社区贡献的 instrumentation 库覆盖了 Spring Boot、FastAPI、Gin、Express、gRPC、Kafka 等主流框架,业务侧改造成本可控。
下表对比了三种方案的关键维度:
| 维度 | Prometheus+Jaeger+ELK | 商业 APM | OpenTelemetry + Grafana 栈 |
|---|---|---|---|
| 厂商锁定 | 低 | 高 | 极低 |
| 三支柱统一 | 否 | 是 | 是 |
| 数据所有权 | 自有 | 供应商托管 | 自有 |
| 月度成本(社区规模) | 约 1200 元 | 约 8000 元 | 约 1500 元 |
| 运维复杂度 | 高(三套系统) | 低 | 中(一套 Collector) |
二、整体架构设计
平台的整体数据流如下:业务服务通过 OTel SDK 将三支柱数据上报到 OpenTelemetry Collector(部署为 Kubernetes DaemonSet,每个节点一个实例),Collector 完成批量处理、资源标签注入、尾部采样(tail-based sampling)后,按数据类型分别路由到对应后端:
- Metrics → Grafana Mimir(兼容 Prometheus 查询协议,支持长期存储与多租户)
- Traces → Grafana Tempo(基于对象存储的分布式追踪后端,支持 TraceQL 查询)
- Logs → Grafana Loki(类 Prometheus 标签索引的日志聚合系统)
所有后端共享同一个对象存储(MinIO 集群,三副本),Grafana 作为统一查询与可视化前端,通过 “Exemplars” 功能将指标、链路、日志三者关联起来——在指标图表上点击异常点即可下钻到对应的 Trace,再从 Trace 中的 Span 跳转到该时间窗口的日志,实现真正的”三支柱联动”。
2.1 Collector 部署拓扑
Collector 采用 DaemonSet 模式部署,而非 Deployment + HPA 模式。原因有二:一是每个节点本地采集可减少跨节点网络流量;二是当节点故障时,仅丢失该节点上正在缓冲的数据,爆炸半径小。关键配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-collector
namespace: observability
spec:
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: collector
image: otel/opentelemetry-collector-contrib:0.102.0
args: ["--config=/etc/otelcol/config.yaml"]
ports:
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
- containerPort: 8888 # 自身指标
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1
memory: 1Gi
volumeMounts:
- name: config
mountPath: /etc/otelcol
volumes:
- name: config
configMap:
name: otel-collector-config
三、Collector 关键配置详解
Collector 配置是整个平台的核心。我们将配置拆分为接收(receivers)、处理(processors)、导出(exporters)三段,并通过 pipelines 组装。下面给出生产环境实际使用的精简版本。
3.1 接收器:统一 OTLP 协议
业务侧统一使用 OTLP 协议上报,Collector 同时开放 gRPC(4317)与 HTTP(4318)端口。gRPC 用于同集群内高性能场景,HTTP 用于跨网络或调试场景:
1
2
3
4
5
6
7
8 receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
max_recv_msg_size_mib: 16
http:
endpoint: 0.0.0.0:4318
3.2 处理器:尾部采样与资源标签规范化
尾部采样(tail-based sampling)是 OTel 相较传统头部采样的核心优势。头部采样在请求入口即决定是否记录,无法根据”是否出错”做差异化保留;尾部采样在 Collector 端缓冲一段时间内的完整 Trace,再根据策略决定保留或丢弃。社区采用的策略是:100% 保留错误请求与慢请求,正常请求按 10% 采样,在保证排障数据完整性的同时将存储量压缩到原来的约 15%。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 processors:
# 资源标签规范化:补全 k8s 元信息
resourcedetection:
detectors: [env, k8s]
timeout: 2s
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.deployment.name
- k8s.node.name
# 尾部采样:缓冲 30 秒,按策略保留
tail_sampling:
decision_wait: 30s
num_traces: 50000
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: slow
type: latency
latency:
threshold_ms: 800
- name: baseline
type: probabilistic
probabilistic:
sampling_percentage: 10
# 批量处理,降低后端写入压力
batch:
timeout: 5s
send_batch_size: 1024
send_batch_max_size: 4096
需要特别注意
1 | decision_wait |
与
1 | num_traces |
两个参数的权衡。
1 | decision_wait |
越长,越能保证跨服务异步调用链的完整性,但内存占用越高;
1 | num_traces |
决定缓冲区上限,超限后会触发 LRU 淘汰,导致部分 Trace 被提前丢弃。社区在 64GB 内存节点上设置
1 | num_traces: 50000 |
,实测内存占用约 1.8GB,留有充足余量。
3.3 导出器:三支柱分流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 exporters:
prometheusremotewrite:
endpoint: http://mimir-distributor.observability:8080/api/v1/push
resource_to_telemetry_conversion:
enabled: true # 将资源标签转为指标标签
otlp/tempo:
endpoint: tempo-distributor.observability:4317
tls:
insecure: true
loki:
endpoint: http://loki-distributor.observability:3100/loki/api/v1/push
default_labels_enabled:
exporter: false
job: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [resourcedetection, k8sattributes, batch]
exporters: [prometheusremotewrite]
traces:
receivers: [otlp]
processors: [resourcedetection, k8sattributes, tail_sampling, batch]
exporters: [otlp/tempo]
logs:
receivers: [otlp]
processors: [resourcedetection, k8sattributes, batch]
exporters: [loki]
四、业务侧接入实践
平台搭建完成后,业务侧的接入工作量是决定推广成败的关键。社区以 Python FastAPI 服务为例,演示最小化接入方式。核心思路是:不侵入业务代码,通过中间件自动埋点。
4.1 自动埋点:FastAPI 示例
1
2
3
4
5
6
7
8
9
10 # requirements.txt
opentelemetry-distro==0.45b0
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-requests
opentelemetry-instrumentation-sqlalchemy
opentelemetry-exporter-otlp
# 启动命令(无需改动业务代码)
# 通过 opentelemetry-instrument 自动注入
opentelemetry-instrument --service_name article-service --resource_attributes="deployment.environment=prod,team=content" --exporter_otlp_endpoint=http://otel-collector.observability:4317 gunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker
上述命令通过
1 | opentelemetry-instrument |
启动器自动注入 FastAPI、requests、SQLAlchemy 的 instrumentation,业务代码零改动即可产出 HTTP 路由、下游调用、数据库查询三类 Span。对于需要附加业务语义的场景(如记录文章 ID、用户 ID),可在关键路径手动添加 Span 属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@app.post("/articles/{slug}/publish")
async def publish(slug: str, user: User = Depends(get_current_user)):
with tracer.start_as_current_span("publish_article") as span:
span.set_attribute("article.slug", slug)
span.set_attribute("user.id", user.id)
span.set_attribute("publish.scheduled", False)
# 业务逻辑
article = await article_service.publish(slug, user)
span.set_attribute("article.word_count", article.word_count)
return article
4.2 日志关联 TraceID
要让日志能在 Grafana 中与 Trace 关联,必须在日志输出中注入
1 | trace_id |
与
1 | span_id |
。OTel Python SDK 提供
1 | InstrumentedLoggingHandler |
,但对已采用 structlog 的服务,更优雅的方式是自定义 processor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import structlog
from opentelemetry import trace
def add_trace_context(_, __, event_dict):
span = trace.get_current_span()
ctx = span.get_span_context()
if ctx.is_valid:
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
structlog.configure(
processors=[
add_trace_context,
structlog.processors.JSONRenderer(),
],
)
Loki 收到日志后,
1 | trace_id |
作为字段存储,Grafana 在 Trace 详情页通过
1 | {trace_id="xxx"} |
查询自动关联出该 Trace 期间的全部日志,实现从指标→链路→日志的一键下钻。
五、Grafana 统一可视化与告警
三个后端(Mimir、Tempo、Loki)都原生支持作为 Grafana 数据源。配置完成后,在任意 Grafana 面板中都可以通过统一标签(如
1 | service_name |
、
1 | k8s_namespace_name |
)跨数据源查询。社区构建了一套标准看板,包含以下核心视图:
- 服务总览:按 namespace 聚合的 RED 指标(Rate、Errors、Duration),异常服务高亮
- 调用拓扑:基于 Trace 数据自动生成的服务依赖图,标注 P99 延迟与错误率
- 慢查询追踪:按
1db.statement
聚合的数据库 Span,定位 N+1 查询
- SLO 燃尽图:基于 Mimir 多查询窗口计算错误预算消耗速率
告警采用 Grafana Alloy + Alertmanager 方案。一个典型的高延迟告警规则如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 # alerting/rules.yaml
groups:
- name: service-latency
rules:
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_request_duration_seconds_bucket[5m]))
by (le, service_name)
) > 0.8
for: 10m
labels:
severity: warning
team: "{{ $labels.team }}"
annotations:
summary: "{{ $labels.service_name }} P99 延迟超过 800ms"
runbook: "https://wiki.tbr8.org/runbooks/high-latency"
六、踩坑记录与性能调优
6.1 Collector OOM:尾部采样缓冲过大
上线第一周,高峰期 Collector 频繁 OOM 重启。排查发现
1 | tail_sampling |
的
1 | num_traces |
默认值 50000 在请求高峰期不足,触发 LRU 淘汰后导致大量 Trace 被截断。解决方法是结合实际 QPS 估算:
1 | num_traces ≈ QPS × decision_wait × 安全系数 |
。社区峰值 QPS 约 90,
1 | decision_wait |
30 秒,安全系数取 2,最终设为 60000,同时将 Pod 内存上限提升至 2GB。
6.2 Loki 高基数标签导致查询超时
初期将
1 | trace_id |
作为 Loki 标签(label)而非字段(field)存储,导致标签基数爆炸——每个 Trace 产生一个独立标签值,Loki 索引膨胀至 200GB,查询超时。修复方式是将
1 | trace_id |
改为结构化字段,仅在日志行内 JSON 中保留,Grafana 通过 JSON Path 解析查询。这一改动让索引体积回落到 12GB。
6.3 Tempo 查询慢:未启用 Prefilter
Tempo 2.0 引入了
1 | prefilter |
功能,可在查询时先通过 Block 元数据过滤掉不相关的 Block,再进行精确查找。社区初期未启用,TraceQL 查询 24 小时范围耗时约 40 秒;启用后降至 3 秒。配置如下:
1
2
3
4
5 querier:
max_concurrent: 16
query_range:
align_queries_with_step: true
split_queries_by_interval: 15m
七、上线效果与后续规划
平台上线运行两个月后,关键指标改善如下:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| P99 延迟归因平均耗时 | 35 分钟 | 4 分钟 |
| 日志+链路存储月成本 | 1200 元 | 680 元 |
| 告警误报率 | 18% | 5% |
| 业务侧接入平均工时 | 2 人日 | 0.3 人日 |
后续规划主要围绕三个方向:一是引入 Profiling 支持——OTel 已在 2026 年 Q2 将 Profiling 纳入稳定版,配合 Pyroscope 可实现”四支柱”联动;二是基于 Trace 数据构建 服务级 SLO 自动化,将错误预算消耗接入发布门禁;三是探索 eBPF-based auto-instrumentation,对于无法改造的遗留服务(如部分 PHP 与 Java 存量应用),通过内核态采集网络调用链,实现零代码改动的可观测性覆盖。
汤不热吧技术社区将持续在云原生基础设施与 AI 辅助运维方向投入,相关技术细节会陆续在本站发布。如对本文涉及方案有疑问或希望交流实践经验,欢迎在评论区留言或通过社区 Discord 频道联系基础设施团队。
汤不热吧