欢迎光临

汤不热吧技术社区上线全链路可观测性平台:基于 OpenTelemetry 与 Grafana 技术栈的云原生监控体系部署实录

随着汤不热吧技术社区的容器化规模持续扩张——目前生产集群已承载超过 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 延迟与错误率
  • 慢查询追踪:按
    1
    db.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 频道联系基础设施团队。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 汤不热吧技术社区上线全链路可观测性平台:基于 OpenTelemetry 与 Grafana 技术栈的云原生监控体系部署实录
分享到: 更多 (0)