背景#

在 Homelab 的可观测性建设中,我经历了几个阶段:

  1. 最初:kube-prometheus-stack 提供 Metrics + Grafana,Promtail 采集日志到 Loki
  2. OTel 日志迁移:用 OTel Collector DaemonSet 替换 Promtail,统一到 OTLP 协议
  3. 多集群扩展:oracle-k3s 集群通过 OTel Collector 将 logs + metrics 推送到 homelab 的 Loki + Prometheus
  4. 本次改进:打通 Traces 管道,实现完整的 LGTM(Loki/Grafana/Tempo/Mimir)三信号可观测性

本文重点记录第 4 步——如何在双集群场景下实现 OpenTelemetry 全链路追踪。

部署环境#

集群 位置 节点 IP Tailscale IP
k3s-homelab Proxmox VM 10.10.10.10 100.107.254.112
oracle-k3s Oracle Cloud ARM 10.0.0.26 100.107.166.37

两个集群通过 Tailscale 互联,RTT ~80ms。所有可观测性后端(Loki、Prometheus、Tempo、Grafana)运行在 homelab 集群的 monitoring 命名空间。

改进前的状态#

在动手之前,先梳理了现有的 OTel 体系:

组件 homelab oracle-k3s
OTel Collector 版本 v0.146.1 (Helm chart) v0.120.0 (手动 DaemonSet)
日志采集 ✅ filelog → OTLP HTTP → Loki ✅ filelog → OTLP HTTP → Loki
指标采集 ✅ Prometheus 本地 scrape ✅ prometheusremotewrite → Prometheus
追踪采集 ❌ 无 OTLP receiver ❌ 无 OTLP receiver
内存保护 ❌ 无 memory_limiter ❌ 无 memory_limiter
健康检查 ❌ 无 health_check ❌ 无 health_check
Tempo 已部署但无数据流入 N/A

几个关键问题:

  • 没有 OTLP receiver:OTel Collector 只有 filelog receiver,应用无法推送 traces
  • 没有 memory_limiter:高负载下 Collector 可能 OOM
  • Grafana Tempo 数据源 URL 错误:配置了 :3100(Loki 端口),Tempo 实际使用 :3200
  • 没有 Tempo NodePort:oracle-k3s 的 traces 无法发送到 homelab 的 Tempo

架构设计#

改进后的追踪架构:

┌──────── homelab Cluster ─────────┐    ┌────── oracle-k3s Cluster ──────┐
│                                   │    │                                │
│  App Pod                          │    │  App Pod                       │
│  ┌─────────────┐                  │    │  ┌─────────────┐               │
│  │ OTel SDK    │                  │    │  │ OTel SDK    │               │
│  │ (Go/Java/   │                  │    │  │ (any lang)  │               │
│  │  Node/Rust) │                  │    │  └──────┬──────┘               │
│  └──────┬──────┘                  │    │         │ OTLP gRPC :4317      │
│         │ OTLP gRPC :4317         │    │         ▼                      │
│         ▼                         │    │  ┌─────────────────────┐       │
│  ┌─────────────────────┐          │    │  │ OTel Collector      │       │
│  │ OTel Collector      │          │    │  │ (DaemonSet)         │       │
│  │ (DaemonSet)         │          │    │  │                     │       │
│  │                     │          │    │  │ resource attrs:     │       │
│  │ resource attrs:     │          │    │  │   cluster=oracle-k3s│       │
│  │   cluster=homelab   │          │    │  └──────┬──────────────┘       │
│  └──────┬──────────────┘          │    │         │                      │
│         │ OTLP gRPC               │    └─────────┼──────────────────────┘
│         ▼                         │              │ OTLP gRPC
│  ┌─────────────┐                  │              │ via Tailscale
│  │ Tempo 2.8.2 │ ◄───────────────┼──────────────┘ :31317 NodePort
│  │ (5Gi NFS)   │                  │
│  └──────┬──────┘                  │
│         │ TraceQL                 │
│         ▼                         │
│  ┌─────────────┐                  │
│  │ Grafana     │                  │
│  │ 12.3.3      │                  │
│  └─────────────┘                  │
└───────────────────────────────────┘

核心设计决策

  1. 统一走 OTel Collector 中转:应用不直接连 Tempo,而是发送到本集群的 OTel Collector ClusterIP Service,由 Collector 添加 cluster 标签后转发。这样可以统一做采样、限流和资源标注。
  2. gRPC 传输:Traces 数据量大,gRPC 的双向流和压缩优于 HTTP。
  3. Tailscale NodePort 跨集群:oracle-k3s 的 Collector 通过 Tailscale IP 100.107.254.112:31317 发送到 homelab Tempo,复用已有的跨集群网络。
  4. Head-based 采样(暂缓):当前 traces 量很小,全量采集;后续自研服务上量后再在 Collector 层加 probabilistic_sampler

实施步骤#

1. Homelab OTel Collector — 添加 Traces 管道#

Homelab 集群使用 Helm chart 部署 OTel Collector,修改 values/opentelemetry-collector.yaml

config:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

  processors:
    memory_limiter:
      limit_mib: 200
      spike_limit_mib: 50
      check_interval: 5s

  exporters:
    otlp/tempo:
      endpoint: tempo.monitoring.svc.cluster.local:4317
      tls:
        insecure: true

  extensions:
    health_check:
      endpoint: 0.0.0.0:13133

  service:
    extensions: [health_check]
    pipelines:
      traces:
        receivers: [otlp]
        processors: [memory_limiter, k8sattributes, resource, batch]
        exporters: [otlp/tempo]
      logs:
        receivers: [filelog, otlp]  # 同时接受 SDK 推送的日志
        processors: [memory_limiter, k8sattributes, resource, batch]
        exporters: [otlphttp]

关键配置说明:

  • otlp receiver:开启 gRPC (4317) 和 HTTP (4318) 两个端口,接收应用推送的 traces 和 logs
  • memory_limiter:200MiB 硬限制,50MiB spike buffer,5 秒检查一次。必须放在 pipeline 的第一个 processor
  • otlp/tempo exporter:gRPC 连接到集群内的 Tempo Service
  • health_check:暴露 :13133 端点,供 K8s liveness/readiness 探针使用
  • resource processor:注入 cluster=homelab 属性,标识数据来源

同时需要开启 Helm chart 的 ClusterIP Service:

service:
  enabled: true  # DaemonSet 模式默认不创建 Service

部署命令:

cd k8s/helm && just deploy-otel-collector

2. Oracle-k3s OTel Collector — 添加 Traces 管道#

Oracle 集群使用手动 DaemonSet manifest,需要在 ConfigMap 中添加 traces 管道配置:

exporters:
  otlp/tempo:
    endpoint: "100.107.254.112:31317"  # Tempo NodePort via Tailscale
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [otlp/tempo]

同时添加 ClusterIP Service,让 oracle-k3s 上的应用也能通过 otel-collector.monitoring.svc:4317 推送 traces:

apiVersion: v1
kind: Service
metadata:
  name: otel-collector
  namespace: monitoring
spec:
  selector:
    app: otel-collector
  ports:
    - name: otlp-grpc
      port: 4317
      targetPort: 4317
    - name: otlp-http
      port: 4318
      targetPort: 4318

部署命令:

kubectl --context oracle-k3s apply -f cloud/oracle/manifests/monitoring/otel-collector.yaml
kubectl --context oracle-k3s rollout restart daemonset/otel-collector -n monitoring

3. Tempo NodePort — 跨集群入口#

为了让 oracle-k3s 的 traces 能发送到 homelab 的 Tempo,需要创建一个 NodePort Service:

apiVersion: v1
kind: Service
metadata:
  name: tempo-otlp-external
  namespace: monitoring
spec:
  type: NodePort
  selector:
    app.kubernetes.io/name: tempo
  ports:
    - name: otlp-grpc
      port: 4317
      targetPort: 4317
      nodePort: 31317

这样 oracle-k3s 的 OTel Collector 通过 100.107.254.112:31317(Tailscale IP + NodePort)即可发送 gRPC traces 到 Tempo。

这与 Kopia 使用 NodePort :31515 的模式一致——对于需要跨集群直连的 gRPC 服务,Cloudflare Tunnel 不适合(双向流 + 524 超时),NodePort + Tailscale 是更可靠的方案。

4. Tempo 存储扩容#

将 Tempo 的 PVC 从 2Gi 扩容到 5Gi,为 traces 数据增长预留空间:

# values/tempo.yaml
persistence:
  enabled: true
  storageClassName: nfs-client
  size: 5Gi

5. Grafana 数据源联动#

这是让追踪体验真正好用的关键——配置 Tempo 数据源与 Loki、Prometheus 的联动:

# kube-prometheus-stack additionalDataSources
- name: Tempo
  type: tempo
  url: http://tempo.monitoring.svc.cluster.local:3200
  jsonData:
    tracesToLogsV2:
      datasourceUid: loki
      filterByTraceID: true
      filterBySpanID: true
      tags:
        - key: k8s.namespace.name
          value: k8s_namespace_name
        - key: k8s.pod.name
          value: k8s_pod_name
        - key: service.name
          value: service_name
    tracesToMetrics:
      datasourceUid: prometheus
    nodeGraph:
      enabled: true
    serviceMap:
      datasourceUid: prometheus

配置效果:

  • tracesToLogs:在 Tempo 查看 trace 时,可以一键跳转到 Loki 查看该请求的关联日志,自动根据 traceID 和 spanID 过滤
  • tracesToMetrics:从 trace 跳转到 Prometheus 查看对应服务的 RED 指标
  • nodeGraph:可视化展示服务间的调用拓扑
  • serviceMap:基于 Prometheus 的 traces_spanmetrics_* 指标绘制服务地图

踩坑:之前 Tempo 数据源 URL 配置的是 :3100,这其实是 Loki 的端口。Tempo 的查询端口是 :3200。花了一些时间才发现这个小错误。

验证#

部署完成后,直接用 curl 发送一个测试 trace 来验证:

# 通过 port-forward 访问 OTel Collector
kubectl port-forward svc/opentelemetry-collector-agent -n monitoring 4318:4318

# 发送测试 trace
curl -X POST http://localhost:4318/v1/traces \
  -H "Content-Type: application/json" \
  -d '{
    "resourceSpans": [{
      "resource": {
        "attributes": [
          {"key": "service.name", "value": {"stringValue": "demo-app"}},
          {"key": "cluster", "value": {"stringValue": "homelab"}}
        ]
      },
      "scopeSpans": [{
        "spans": [{
          "traceId": "5b8aa5a2d2c872e8321cf37308d69df2",
          "spanId": "051581bf3cb55c13",
          "name": "GET /api/test",
          "kind": 2,
          "startTimeUnixNano": "1740000000000000000",
          "endTimeUnixNano": "1740000000500000000",
          "status": {"code": 1}
        }]
      }]
    }]
  }'

收到 HTTP 200 响应后,通过 Tempo API 验证 trace 已持久化:

kubectl port-forward svc/tempo -n monitoring 3200:3200
curl -s "http://localhost:3200/api/traces/5b8aa5a2d2c872e8321cf37308d69df2" | jq '.batches[0].resource'

返回了完整的 trace 数据,包含 service.name=demo-appcluster=homelab 属性。

应用接入指南#

基础设施已就绪,接下来就是在自研服务中接入 OTel SDK。对于所有语言,K8s Deployment 中只需要添加以下环境变量:

env:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: "http://otel-collector.monitoring.svc:4317"
  - name: OTEL_SERVICE_NAME
    value: "my-service"
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: "cluster=homelab,k8s.namespace.name=my-namespace"

Go#

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
    exp, err := otlptracegrpc.New(ctx) // 自动读取 OTEL_EXPORTER_OTLP_ENDPOINT
    if err != nil {
        return nil, err
    }
    tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exp))
    otel.SetTracerProvider(tp)
    return tp, nil
}

HTTP 中间件使用 otelhttp

import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

handler := otelhttp.NewHandler(mux, "server")

Java + Spring Boot(零代码修改)#

Spring Boot 可以通过 Java Agent 实现零代码自动埋点:

# 下载 OTel Java Agent
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /otel/

ENV JAVA_TOOL_OPTIONS="-javaagent:/otel/opentelemetry-javaagent.jar"

Agent 自动 instrument Spring MVC、JDBC、Redis、gRPC 等常用库,无需修改任何业务代码。

Node.js#

import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter(), // 读取 OTEL_EXPORTER_OTLP_ENDPOINT
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

auto-instrumentations-node 包含 Express、Koa、HTTP、MySQL、Redis 等 30+ 库的自动埋点。

Rust#

use opentelemetry::global;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::TracerProvider;

fn init_tracer() -> TracerProvider {
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .build()
        .expect("failed to create exporter");

    let provider = TracerProvider::builder()
        .with_batch_exporter(exporter)
        .build();

    global::set_tracer_provider(provider.clone());
    provider
}

配合 tracing-opentelemetry crate 可以将 Rust 的 tracing 生态与 OTel 打通。

跨集群 NodePort 汇总#

完成本次改进后,homelab 目前使用的 NodePort 端口:

NodePort 服务 用途
31317 tempo-otlp-external oracle-k3s traces → Tempo gRPC
31515 kopia-server-external Kopia gRPC 备份客户端
31100 loki-external oracle-k3s logs → Loki HTTP
31090 prometheus-external oracle-k3s metrics → Prometheus remote write

这些端口都仅通过 Tailscale 内网暴露,不经过 Cloudflare Tunnel。

总结#

经过这次改进,Homelab 的可观测性体系从"两个信号"(Logs + Metrics)升级为完整的"三个信号"(Logs + Metrics + Traces):

信号 采集 传输 存储 查询
Logs OTel filelog receiver OTLP HTTP Loki 3.x LogQL
Metrics Prometheus scrape / OTel prometheusremotewrite Remote Write Prometheus PromQL
Traces OTel OTLP receiver OTLP gRPC Tempo 2.8.2 TraceQL

所有数据在 Grafana 中实现了联动:Tempo trace → 关联 Loki 日志 → 关联 Prometheus 指标。这正是 Grafana Labs 推崇的 LGTM 栈最佳实践。

后续计划#

  • 在第一个自研 Go 服务中实际接入 OTel SDK
  • 添加 probabilistic_sampler processor,按比例采样(当 trace 量增大时)
  • 部署 Grafana Trace Dashboard(服务拓扑图、延迟分布)
  • 升级 oracle-k3s 的 OTel Collector 从 v0.120.0 到最新版本

相关文章