OpenTelemetry SDK、Collector 与 OTLP 工具手册
一次结算请求在入口日志里出现了 trace ID,库存服务却查不到对应 span。更麻烦的是,Collector 的健康检查仍然成功,后端也能查询其他调用链。这个现场不能靠“重新接一次 Jaeger”解决:可能是应用没有创建 span,可能是跨线程或 HTTP 传播丢了 Context,可能是 Collector 拒绝、采样或丢弃了数据,也可能是出口队列正在重试。
OpenTelemetry 把这些责任拆成稳定的层次。应用用 API 表达 span、metric 与 log;SDK 决定采样、处理和导出;自动插桩拦截框架与常用库;Context 让同一次调用保持父子关系;Resource 标识数据由哪个服务实例产生;OTLP 负责传输;Collector 再通过 receiver、processor、exporter 和 connector 组成管道。它不是追踪查询 UI,也不承诺存储数据,但让应用不必直接绑定每一种后端。
下面会启动一个 Python 应用、一个边缘 Collector 和一个模拟下游的 Collector,并故意发送成功、错误、敏感属性、错误协议和下游中断。请在隔离开发机上准备 Python 3.11 或更高版本、Docker Compose、至少 3 GiB 可用内存和 5 GiB 空闲磁盘;确认 4317、4318、8888、13133 和 8000 未被占用。示例中的 token、邮箱和 endpoint 都是测试值,不能替换成真实客户数据后再把日志发到共享平台。
先让一条 Trace 穿过两个 Collector
新建 otel-lab,准备 compose.yaml、edge.yaml、sink.yaml 和 app.py。Collector core 与 contrib 发行版包含的组件不同,而且每个组件有自己的成熟度;选择镜像前应在 Collector distributions 和 组件清单 确认配置需要的 tail_sampling、file_storage、spanmetrics 与 health_check 都存在。这里使用 contrib 发行版并固定版本:
# compose.yaml
services:
edge:
image: otel/opentelemetry-collector-contrib:0.156.0
command: ["--config=/etc/otelcol-contrib/config.yaml"]
ports:
- "127.0.0.1:4317:4317"
- "127.0.0.1:4318:4318"
- "127.0.0.1:8888:8888"
- "127.0.0.1:13133:13133"
volumes:
- ./edge.yaml:/etc/otelcol-contrib/config.yaml:ro
- edge-queue:/var/lib/otelcol
depends_on: [sink]
sink:
image: otel/opentelemetry-collector-contrib:0.156.0
command: ["--config=/etc/otelcol-contrib/config.yaml"]
volumes:
- ./sink.yaml:/etc/otelcol-contrib/config.yaml:ro
volumes:
edge-queue: {}边缘 Collector 接收 OTLP,先做内存保护,再删除或哈希敏感属性,然后基于完整 trace 决定是否保留,最后批量发往 sink。spanmetrics connector 同时作为 traces pipeline 的 exporter 和 metrics pipeline 的 receiver,把已保留 span 转成请求数与延迟指标。宿主机发布端口全部绑定 127.0.0.1;Collector 配置中的 0.0.0.0 只在容器网络命名空间内监听,使 edge 与 sink 能互通,并不把端口直接发布到外部网卡。
# edge.yaml
extensions:
health_check:
endpoint: 0.0.0.0:13133
file_storage:
directory: /var/lib/otelcol
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: 384
spike_limit_mib: 96
resource/defaults:
attributes:
- key: deployment.environment.name
value: local
action: insert
attributes/privacy:
actions:
- key: http.request.header.authorization
action: delete
- key: user.email
action: hash
tail_sampling:
decision_wait: 5s
num_traces: 50000
expected_new_traces_per_sec: 100
policies:
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
batch:
timeout: 1s
send_batch_size: 512
send_batch_max_size: 1024
connectors:
spanmetrics:
histogram:
explicit:
buckets: [5ms, 10ms, 50ms, 100ms, 500ms, 1s]
dimensions:
- name: http.request.method
exporters:
otlp/sink:
endpoint: sink:4317
tls:
insecure: true
sending_queue:
storage: file_storage
queue_size: 5000
retry_on_failure:
enabled: true
initial_interval: 1s
max_interval: 10s
max_elapsed_time: 10m
debug/metrics:
verbosity: normal
service:
extensions: [health_check, file_storage]
telemetry:
metrics:
readers:
- pull:
exporter:
prometheus:
host: 0.0.0.0
port: 8888
pipelines:
traces:
receivers: [otlp]
processors:
[memory_limiter, resource/defaults, attributes/privacy, tail_sampling, batch]
exporters: [otlp/sink, spanmetrics]
metrics/span-derived:
receivers: [spanmetrics]
processors: [memory_limiter, batch]
exporters: [debug/metrics]组件只在 service.extensions 或 service.pipelines 中被引用后才运行。处理顺序也是行为:memory limiter 尽早形成背压,隐私处理发生在数据离开本机信任边界前,tail sampling 必须看到用于决策的属性,batch 放在网络出口前。Connector 的本质是“一端像 exporter,另一端像 receiver”,适合跨 pipeline 生成或路由信号;它不是把不同信号塞进同一 pipeline。官方 Collector 配置 与 connector 说明 给出了这套装配模型。
sink 只接收并打印脱敏后的 trace:
# sink.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch: {}
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]先让 Collector 自己拒绝错误配置,再启动服务。以下 Bash 轮询会等待健康端点真正可用,而不是把容器进入 running 当成就绪:
docker compose run --rm edge validate --config=/etc/otelcol-contrib/config.yaml
docker compose run --rm sink validate --config=/etc/otelcol-contrib/config.yaml
docker compose up -d
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:13133/ >/dev/null && break
sleep 2
done
curl -fsS http://127.0.0.1:13133/PowerShell 对应命令如下:
docker compose run --rm edge validate --config=/etc/otelcol-contrib/config.yaml
docker compose run --rm sink validate --config=/etc/otelcol-contrib/config.yaml
docker compose up -d
$ready = $false
1..60 | ForEach-Object {
try {
Invoke-WebRequest http://127.0.0.1:13133/ -UseBasicParsing | Out-Null
$ready = $true
return
} catch { Start-Sleep 2 }
}
if (-not $ready) { throw 'edge Collector 未在 120 秒内就绪' }validate 应无错误退出,健康端点应返回成功。若出现 unknown type 或 unknown component,通常是发行版不含该组件或组件 ID 拼错;若 file_storage 报权限错误,检查数据卷挂载和容器用户。健康成功只证明 Collector 进程可服务,不能证明它正在接收或成功导出遥测。
API、SDK 与自动插桩各做什么
共享库应依赖 OpenTelemetry API,只创建与业务语义有关的 span 或 metric,不替宿主应用选择 exporter。应用启动层安装 SDK、Sampler、SpanProcessor、Exporter 与 Propagator。这样库被没有启用 OTel 的应用加载时,API 可以保持 no-op;应用也能在不改业务库的情况下切换 OTLP endpoint。
自动插桩通过 agent、字节码修改、monkey patch、运行时 hook 或 eBPF 等方式覆盖 HTTP、数据库和消息库。它能快速建立技术调用图,却不知道“库存预留”“风险复核”这些领域阶段,因此通常要与少量手工 span 配合。可用语言与机制持续变化,应从 zero-code instrumentation 进入对应语言文档,不要假设所有语言拥有同样能力。
创建 Python 虚拟环境并安装 SDK、OTLP/HTTP exporter 与自动插桩。实验把 Python 包锁到一组可重复安装的版本,避免 opentelemetry-bootstrap -a install 随时间解析出不同的 instrumentation;团队项目仍应把解析结果写入自己的 lockfile。Bash 使用:
python -m venv .venv
source .venv/bin/activate
python -m pip install \
Flask==3.1.2 \
requests==2.32.4 \
opentelemetry-distro==0.56b0 \
opentelemetry-exporter-otlp-proto-http==1.35.0 \
opentelemetry-instrumentation-flask==0.56b0 \
opentelemetry-instrumentation-requests==0.56b0PowerShell 不使用 Bash 的 source 与反斜杠续行:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install Flask==3.1.2 requests==2.32.4 opentelemetry-distro==0.56b0 opentelemetry-exporter-otlp-proto-http==1.35.0 opentelemetry-instrumentation-flask==0.56b0 opentelemetry-instrumentation-requests==0.56b0应用用自动插桩生成 Flask server span 和 Requests client span,用 API 增加领域 span。它故意写入假的 Authorization 与邮箱,以验证 Collector 是否在出口前处理它们。
# app.py
from flask import Flask, jsonify, request
import requests
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
app = Flask(__name__)
tracer = trace.get_tracer("checkout-domain")
@app.get("/inventory")
def inventory():
if request.args.get("fail") == "1":
return jsonify(error="inventory unavailable"), 503
return jsonify(available=True)
@app.get("/checkout")
def checkout():
fail = request.args.get("fail", "0")
with tracer.start_as_current_span("reserve-stock") as span:
span.set_attribute("order.channel", "lab")
span.set_attribute("user.email", "demo@example.invalid")
span.set_attribute(
"http.request.header.authorization", "Bearer lab-secret"
)
response = requests.get(
f"http://127.0.0.1:8000/inventory?fail={fail}", timeout=2
)
if response.status_code >= 500:
span.set_status(Status(StatusCode.ERROR, "inventory failed"))
return jsonify(error="checkout failed"), 502
return jsonify(ok=True)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000, threaded=True)Resource 回答“谁产生了数据”。service.name 是最重要的标识;环境、版本和实例 ID 应有稳定语义,但 pod UID 或主机名一类实例属性不应被误写成服务名。Resource 是不可变属性集合,SDK 会合并显式配置、环境变量和检测器结果,冲突优先级应以对应 SDK 与 Resource 规范 为准。
在终端 A 启动应用。Bash 使用:
export OTEL_SERVICE_NAME=checkout-api
export OTEL_RESOURCE_ATTRIBUTES='service.version=1.0.0,deployment.environment.name=local'
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
export OTEL_PROPAGATORS=tracecontext,baggage
export OTEL_TRACES_SAMPLER=always_on
opentelemetry-instrument python app.pyPowerShell 使用:
$env:OTEL_SERVICE_NAME = 'checkout-api'
$env:OTEL_RESOURCE_ATTRIBUTES = 'service.version=1.0.0,deployment.environment.name=local'
$env:OTEL_EXPORTER_OTLP_PROTOCOL = 'http/protobuf'
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'http://127.0.0.1:4318'
$env:OTEL_TRACES_EXPORTER = 'otlp'
$env:OTEL_METRICS_EXPORTER = 'none'
$env:OTEL_LOGS_EXPORTER = 'none'
$env:OTEL_PROPAGATORS = 'tracecontext,baggage'
$env:OTEL_TRACES_SAMPLER = 'always_on'
opentelemetry-instrument python app.py应用端使用 always_on,因为 tail sampling 需要先收到完整 trace;边缘 Collector 再决定保留哪些 trace。在终端 B 先执行就绪轮询,再触发一条成功请求和一条错误请求。以下是 Bash:
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8000/inventory >/dev/null && break
sleep 1
done
curl -fsS http://127.0.0.1:8000/inventory >/dev/null || { echo 'Python 应用未在 60 秒内就绪' >&2; exit 1; }
curl -i 'http://127.0.0.1:8000/checkout?fail=0'
curl -i 'http://127.0.0.1:8000/checkout?fail=1'
sleep 7
docker compose logs --since=2m sinkPowerShell 的就绪与请求命令为:
$ready = $false
1..60 | ForEach-Object {
try {
Invoke-RestMethod http://127.0.0.1:8000/inventory | Out-Null
$ready = $true
return
} catch { Start-Sleep 1 }
}
if (-not $ready) { throw 'Python 应用未在 60 秒内就绪' }
Invoke-WebRequest 'http://127.0.0.1:8000/checkout?fail=0' -UseBasicParsing
try { Invoke-WebRequest 'http://127.0.0.1:8000/checkout?fail=1' -UseBasicParsing } catch { $_.Exception.Response.StatusCode.value__ }
Start-Sleep 7
docker compose logs --since=2m sink成功请求返回 200,错误请求返回 502。当前采样策略只保留 ERROR,因此 sink 应打印错误 trace,成功 trace 应被丢弃。错误 trace 中 Flask server span、reserve-stock 和 Requests client span 应共享同一个 trace ID,并形成父子关系;user.email 不再是原文,Authorization 属性应完全消失。这个正反实验同时证明了自动插桩、手工 API、Context、脱敏和 tail sampling,不应只用“后端出现了一条 trace”作为判断依据。
Context 为什么会断,Baggage 为什么不能放秘密
Context 保存当前 span 与传播所需状态。HTTP 客户端注入 traceparent,服务端提取后创建子 span;消息系统则需要在 message properties 中注入与提取。线程池、异步 callback、定时任务和批量消费者经常跨越语言运行时的默认上下文边界,必须使用对应 SDK 的 context-aware executor、wrapper 或显式 attach/detach。错误地创建新 root span 会让后端看到两个都“正常”的孤立 trace。
排查断链时从上游 span 记录 trace ID 与 span ID,检查出站载体是否有合法 traceparent,再检查下游提取后的 parent ID。不要把两个独立 root span 仅凭时间接近强行拼接。传播器也必须一致:一端只发 W3C Trace Context、另一端只读 B3 时,网络成功但链路必断。
Baggage 是随请求传播的键值,不会因为叫 baggage 就自动加密。它可能被多个服务、代理和日志看到,体积还会放大每次请求的 header。租户分区、实验组等少量非敏感路由信息可以经过评审后使用;密码、token、邮箱、身份证号和支付信息不能进入 baggage。Baggage 也不会自动变成 span attribute,需要显式复制,而复制动作本身要经过数据治理。
OTLP 端口、路径和错误语义
OTLP/gRPC 通常使用 4317,OTLP/HTTP 通常使用 4318。HTTP exporter 在统一 endpoint 后追加 /v1/traces、/v1/metrics 或 /v1/logs;如果配置使用 signal-specific endpoint,则路径语义可能不同。把 gRPC 发到 HTTP 端口、在 endpoint 中重复写 /v1/traces、把明文发到 TLS 端口、代理不支持 HTTP/2,都会表现为连接或协议错误。协议要求与重试分类应查 OTLP specification。
做一个端口反实验时,先在终端 A 按 Ctrl+C 停止正常应用,确认 127.0.0.1:8000 已释放,再用错误 OTLP 端口启动同一个进程。这样 exporter 的协议错误不会被第二个 Flask 进程的“address already in use”掩盖。Bash 使用:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
opentelemetry-instrument python app.pyPowerShell 使用:
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'http://127.0.0.1:4317'
$env:OTEL_EXPORTER_OTLP_PROTOCOL = 'http/protobuf'
opentelemetry-instrument python app.py在终端 B 等应用重新就绪并触发请求后,应用 exporter 应报告连接重置、404 或协议解析错误,edge 的 otelcol_receiver_accepted_spans 不增长。再次按 Ctrl+C 停止错误配置进程,恢复 http://127.0.0.1:4318,重新启动并通过 /inventory 就绪轮询后再验证。HTTP 400 一类永久坏数据不应无限重试;429、502、503、504 等临时失败通常可以退避重试。OTLP partial success 可能仍返回成功状态,因此 Collector 与后端必须同时观察 rejected 计数,不能只看 HTTP status。
Receiver、Processor、Exporter 与 Connector 的失败边界
Receiver 只证明数据进入 Collector。Processor 可以修改、聚合、拒绝或丢弃;Exporter 可能排队、重试并最终放弃;Connector 可能生成另一种信号。判断“trace 去哪了”时应按数据流比较:
SDK exported
-> receiver accepted / refused
-> processor dropped / sampled / limited
-> exporter queue size / send failed
-> backend accepted and queryableCollector 的 internal telemetry 给出了关键指标。查看边缘实例:
curl -fsS http://127.0.0.1:8888/metrics | grep -E \
'otelcol_receiver_(accepted|refused)_spans|otelcol_exporter_(queue|send_failed|sent)_spans'accepted 增长而 sent 不增长,问题在 processor、queue 或 exporter;refused 持续增长说明 receiver 向上游施加了错误或背压,SDK 是否重试决定是否丢数据;send_failed 增长不一定已经丢失,因为数据可能仍在队列等待重试。日志中的 sending_queue is full 才是明确的丢弃证据之一。
Batch、memory limiter 与重试队列不是同一种缓冲
SDK 的 BatchSpanProcessor 在应用进程内短暂聚合 span,减少请求开销;进程崩溃时尚未导出的 span 会丢。Collector batch processor 再把接收到的数据按大小或时间聚合,降低出口请求数。两处 batch 都不是持久消息队列,配置过大还会增加延迟与内存峰值。
Memory limiter 周期检查内存,在达到软限制时拒绝新数据并触发 Go GC,以避免 Collector 被 OOM killer 直接杀死。它依赖上游正确重试;如果 SDK 或前一跳不重试,拒绝就是丢失。limit_mib 与 spike_limit_mib 要根据容器硬限制、流量峰值、batch、tail sampling 和 queue 一起压测,不能照抄固定比例。
Exporter sending queue 吸收短时下游故障,retry_on_failure 用指数退避和抖动重试。当前配置把队列放进 file_storage,Collector 重启后仍可恢复未发送数据;磁盘满、文件损坏、队列满或超过 max_elapsed_time 仍会丢失。官方 Collector resiliency 将这些失败条件与持久队列边界分开说明。
停止 sink,连续制造错误 trace。由于 tail_sampling.decision_wait 是 5 秒,刚结束请求时这些 trace 还可能只在采样处理器内存中,不能立刻重启 edge。下面的 Bash 命令先等待决策窗口,再轮询到持久 exporter queue 的 size 确实大于零,只有拿到“已入队”证据后才进入重启步骤:
docker compose stop sink
for i in $(seq 1 100); do
curl -s 'http://127.0.0.1:8000/checkout?fail=1' >/dev/null
done
sleep 7
for i in $(seq 1 30); do
curl -fsS http://127.0.0.1:8888/metrics | awk '
/otelcol_exporter_queue_size/ && $NF + 0 > 0 { found=1 }
END { exit !found }
' && break
sleep 1
done
curl -fsS http://127.0.0.1:8888/metrics | awk '
/otelcol_exporter_queue_size/ && $NF + 0 > 0 { found=1 }
END { exit !found }
' || { echo 'tail sampling 决策后仍未观察到持久队列入队' >&2; exit 1; }
curl -fsS http://127.0.0.1:8888/metrics | grep -E \
'otelcol_exporter_queue_(size|capacity)|otelcol_exporter_send_failed_spans'
docker compose logs --since=2m edgePowerShell 对应为:
docker compose stop sink
1..100 | ForEach-Object {
try { Invoke-WebRequest 'http://127.0.0.1:8000/checkout?fail=1' -UseBasicParsing | Out-Null } catch {}
}
Start-Sleep 7
$queued = $false
1..30 | ForEach-Object {
$metrics = (Invoke-WebRequest http://127.0.0.1:8888/metrics -UseBasicParsing).Content
$queueSizes = [regex]::Matches($metrics, '(?m)^otelcol_exporter_queue_size\{[^}]*\}\s+([0-9.eE+-]+)$')
if ($queueSizes | Where-Object { [double]$_.Groups[1].Value -gt 0 }) {
$queued = $true
return
}
Start-Sleep 1
}
if (-not $queued) { throw 'tail sampling 决策后仍未观察到持久队列入队' }
docker compose logs --since=2m edgequeue size 与 send failures 应增长。若轮询超时,应先查 tail sampling 是否保留了错误 trace、指标名是否与当前 Collector 版本一致以及 file storage 是否可写,不能继续重启来假设数据已经持久化。确认入队后再重启 edge,确认持久卷仍在,再恢复 sink:
docker compose restart edge
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:13133/ >/dev/null && break
sleep 2
done
curl -fsS http://127.0.0.1:13133/ >/dev/null || { echo 'edge Collector 重启后未就绪' >&2; exit 1; }
docker compose start sink
for i in $(seq 1 60); do
curl -fsS http://127.0.0.1:8888/metrics | awk '
/otelcol_exporter_queue_size/ { seen=1; if ($NF + 0 != 0) pending=1 }
END { exit !(seen && !pending) }
' && break
sleep 2
done
curl -fsS http://127.0.0.1:8888/metrics | awk '
/otelcol_exporter_queue_size/ { seen=1; if ($NF + 0 != 0) pending=1 }
END { exit !(seen && !pending) }
' || { echo '持久队列未在 120 秒内排空' >&2; exit 1; }
curl -fsS http://127.0.0.1:8888/metrics | grep -E \
'otelcol_exporter_queue_(size|capacity)|otelcol_exporter_sent_spans'
docker compose logs --since=2m sinkPowerShell 在重启后执行同样的就绪与排空判断:
docker compose restart edge
$edgeReady = $false
1..60 | ForEach-Object {
try {
Invoke-WebRequest http://127.0.0.1:13133/ -UseBasicParsing | Out-Null
$edgeReady = $true
return
} catch { Start-Sleep 2 }
}
if (-not $edgeReady) { throw 'edge Collector 重启后未就绪' }
docker compose start sink
$drained = $false
1..60 | ForEach-Object {
$metrics = (Invoke-WebRequest http://127.0.0.1:8888/metrics -UseBasicParsing).Content
$queueSizes = [regex]::Matches($metrics, '(?m)^otelcol_exporter_queue_size\{[^}]*\}\s+([0-9.eE+-]+)$')
if ($queueSizes.Count -gt 0 -and -not ($queueSizes | Where-Object { [double]$_.Groups[1].Value -ne 0 })) {
$drained = $true
return
}
Start-Sleep 2
}
if (-not $drained) { throw '持久队列未在 120 秒内排空' }
docker compose logs --since=2m sink正向结果是重启后队列继续发送并最终回落;反向结果是 queue capacity 被打满、达到 10 分钟重试上限或 file storage 无法写入,日志会给出丢弃证据。容量估算至少需要:
所需缓冲条目 ≈ 峰值出口批次/秒 x 可容忍下游中断秒数
所需磁盘 ≈ 平均批次字节 x 缓冲条目 x 安全系数这只是起点。trace 大小由 span 数、事件数和属性长度决定,tail sampling 的等待缓存还要额外容纳 并发新 trace/秒 x decision_wait。增加队列会延长恢复时间并放大磁盘写入,增加消费者会提高吞吐也可能压垮后端。
受控压力实验可以逐步提高并发,直到 otelcol_receiver_refused_spans 出现,再降低流量观察恢复。不要把 limit_mib 故意改得极低后在共享 Collector 上实验;memory limiter 的正确证据是进程保持存活、拒绝指标可见、上游重试受控,而不是“完全不丢数据”。
Head sampling 与 tail sampling 的成本完全不同
Head sampling 在 trace 开始时决定,成本低、延迟小、容易水平扩展,但它还不知道请求最终是否错误或缓慢。一致概率采样能让跨服务保持同一决定,并允许后端按采样概率进行统计校正。Tail sampling 等 trace 的 span 到齐后按错误、延迟或属性决策,能保留稀有故障,却必须缓存状态、等待迟到 span 并承担更高内存和路由复杂度。
当前实验让 SDK 全采,Collector 只保留错误。生产若直接照搬,高流量会先把所有 span 都传到 gateway,再在昂贵的位置丢弃。常见折中是入口做一致概率 head sampling 限流,再在保留下来的集合上 tail sample;但 head 阶段丢掉的错误无法恢复。
多个 tail-sampling gateway 之间不能随机分发同一 trace 的 span。官方 agent-to-gateway pattern 要求按 trace ID 做粘性路由,使一个 gateway 看到完整 trace。扩缩容改变 hash ring 时会重新分片,滚动升级也会丢失尚未完成决策的内存状态,因此需要预热、排空、容量冗余与明确的采样准确度目标。
敏感数据要在源头和第一道信任边界处理
Collector 删除两个已知属性只是最低限度。敏感信息还可能藏在 URL query、数据库语句、日志 body、exception message、stack trace、消息 payload、Resource、Baggage 与自定义 event 中。应用最清楚字段语义,应在 SDK/埋点源头使用 allowlist;边缘 Collector 在离开主机或集群前做第二次 filter、transform、redaction 或 attributes 处理;后端再用租户权限、保留和审计兜底。
Hash 不是匿名化保证。低基数字段可被枚举,固定 hash 仍可跨事件关联;加密字段又带来密钥轮换和查询权限。真正不需要的字段应删除,需要关联的字段应使用经过审批的不可逆 token,并限制可见范围。处理器本身的 alpha/beta/stable 状态也决定它能否成为唯一合规控制,相关转换能力和性能警告可查 transforming telemetry。
做隐私回归时,不只搜索输出中是否存在明文,还要固定一组带敏感字段的 trace/log fixtures,验证删除、哈希、采样和路由后的结构。失败时保存的是字段名、计数与测试值,不保存真实数据样本。
从本地单实例走向 agent 与 gateway
SDK 直连后端最简单,但每个应用都要持有后端凭证并承担重试;本机 sidecar/agent 能快速接收、初步脱敏和缓冲;gateway 集中完成认证、路由、tail sampling、跨信号 connector 与后端出口。Agent + gateway 增加一跳和一套容量治理,却把后端切换、凭证和策略从应用发布周期中解耦。
生产拓扑通常让 agent 靠近工作负载,并把 gateway 部署在独立故障域。Receiver 只监听受控网络,OTLP 使用 TLS 或 mTLS;exporter token 通过 Secret 注入并只允许写指定租户。health_check、pprof、zPages 和内部 metrics 放在管理网络,不能因为“只是诊断端口”就公开。企业代理还要验证 gRPC 的 HTTP/2、CONNECT 和证书链,无法保证时可选择 OTLP/HTTP。
Collector 本身必须被观测,但不要让它只通过自身故障域上报自身故障。独立抓取或旁路告警至少覆盖接收拒绝、处理器丢弃、queue utilization、send failures、CPU、内存、GC、磁盘、配置 reload 与进程重启。health=OK 且业务 trace 消失,正是缺少数据流指标时最危险的假健康。
成本来自应用 CPU、SDK 内存、网络字节、Collector 计算与磁盘、tail sampling 状态、后端摄入和保留。治理对象应包括每服务 span 率、平均 span 大小、属性数量与长度、事件数、日志 body、metric cardinality、采样概率和租户预算。Connector 从 trace 生成 metric 时也要控制 dimensions,否则会把 trace 的高基数属性复制成指标成本。
迁移、升级与回滚要保住数据契约
从专有 agent 迁入 OTel 时,先保持旧链路,给少量实例启用 OTel 并双发到旧、新后端,比较 trace 数、错误率、根 span、服务名、传播、时钟、属性和采样。确认语义后再扩大流量,最后撤销旧 agent 的注入、端口、凭证和 daemon。双发会增加 CPU、网络与账单,必须有结束时间。
从一个后端迁到另一个后端时,可以在 Collector 添加第二个 exporter。先验证新端接收和查询,再切换团队链接与告警,保留旧端覆盖所需查询窗口,最后停止旧出口。回滚只需恢复旧 exporter 和查询入口,前提是没有先删除旧凭证与数据。OTLP 降低传输锁定,不会自动消除各后端查询语言、告警、索引和保留策略的差异。
升级 SDK 或 Collector 前锁定组件清单,执行 validate,用固定 trace fixtures 比较前后输出,再压测 memory limiter、batch、queue 和 tail sampling。Semantic Conventions 的属性名与成熟度会演进,改名应采用一段兼容窗口或双写映射,避免看板、告警和采样策略在升级后静默失效。自定义 Collector 发行版可以减少攻击面与镜像体积,但团队要承担构建、签名、SBOM、漏洞修复和组件升级责任。
团队职责也要落到对象上:应用 owner 维护业务埋点与传播,平台 owner 维护发行版、Collector 与出口,安全 owner 维护属性 allowlist、信任边界和凭证,成本 owner 维护采样与配额。每次事故都应能回答某一段的 accepted、dropped、queued、retried、sent 与 queryable 数量,而不是用“OTel 看起来正常”结束排查。
实验结束后停止 Python 应用,再清理容器:
docker compose down
# 确认不再需要验证持久队列后:
docker compose down -v保留 volume 可以重放下游中断恢复;删除 volume 会同时删除尚未发送的持久队列。一个可退出的 OTel 方案应保留开放 API、OTLP、语义约定、管道配置、属性治理规则和可重放测试,而不是把可移植性寄托在某个 exporter 名称上。
