一套运行近十年的指标管道,很少只是一个可以原地替换的进程。来源摘要中的平台长期使用自行维护的开源 StatsD 实现 gostatsd,并将其作为每台主机上的 sidecar。把这样的系统迁移到 OpenTelemetry,真正需要迁移的是指标语义、可靠性边界、运维工具和团队习惯,而不只是监听端口。
不要把迁移单位定义成“一个 Agent”
StatsD 的接口看起来很薄:应用向 UDP 端口发送计数器、Gauge 或 Timer,Agent 聚合后再转发。但平台规模扩大后,应用往往已经依赖一组没有写进协议的契约:
- 指标名如何清洗,点号、横线和非法字符如何转换;
- Counter 在每个聚合窗口内归零,还是作为累计值继续增长;
- Timer 最终变成平均值、分位数,还是可继续聚合的直方图;
- 标签来自报文、主机元数据,还是 sidecar 自动补充;
- UDP 丢包是否可接受,流量高峰时是丢弃、阻塞还是落盘;
- 后端如何识别主机、服务、环境和集群。
OpenTelemetry 提供统一的数据模型和传输方式,但不会自动替你决定这些语义。迁移前应先建立指标目录,至少记录以下字段:
| 字段 | 需要回答的问题 |
|---|---|
| 名称 | 新旧管道输出的名称是否一致? |
| 类型 | Counter、Gauge、Histogram 如何映射? |
| 单位 | ms 是否转换成 s?单位是否写入元数据? |
| 单调性 | Counter 是否允许下降?进程重启如何识别? |
| Temporality | 使用 delta 还是 cumulative?后端支持哪一种? |
| 属性 | 哪些标签是业务维度,哪些应成为 Resource 属性? |
| 基数预算 | 用户 ID、请求 ID等高基数字段是否会进入指标? |
尤其要警惕 Timer。旧系统预先算出的 p95,不能像直方图桶一样跨主机正确聚合。如果迁移时只比较某个分位数是否“看起来接近”,全局仪表盘可能仍然产生错误结果。
用兼容桥拆开应用迁移和平台迁移
在大规模环境中,要求所有应用同时从 StatsD 改成 OTLP 通常不可行。更稳妥的做法是让 OpenTelemetry Collector 暂时同时接收两类流量:
- 老应用继续发送 StatsD;
- Collector 的 StatsD Receiver 将其转换为 OpenTelemetry Metrics;
- 已完成改造的新应用直接发送 OTLP;
- 两类指标经过统一处理后进入目标后端。
这个兼容层的价值在于把一次“大爆炸切换”拆成两条独立路径:平台团队先验证 Collector 和后端,应用团队再按服务逐步迁移 SDK。
但兼容桥不是永久架构。它会保留 StatsD 的部分限制,例如 UDP 缺少确认机制、原始报文表达能力有限,以及旧 Timer 语义可能无法无损转换。迁移计划应给兼容入口设置退出日期和剩余调用方清单。
可以这样搭建一个本地迁移实验
下面是一个可复制的最小实验:Collector 同时开放 StatsD 和 OTLP 接口,将结果暴露为 Prometheus 指标,并通过日志打印转换结果。它是迁移验证模板,不代表来源系统的最终生产配置。
创建 otelcol.yaml:
receivers:
statsd:
endpoint: 0.0.0.0:8125
aggregation_interval: 10s
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
exporters:
prometheus:
endpoint: 0.0.0.0:9464
enable_open_metrics: true
debug:
verbosity: basic
service:
pipelines:
metrics:
receivers:
- statsd
- otlp
processors:
- batch
exporters:
- prometheus
- debug
再创建 compose.yaml:
services:
collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otelcol.yaml:/etc/otelcol-contrib/config.yaml:ro
ports:
- 8125:8125/udp
- 4317:4317
- 4318:4318
- 9464:9464
启动 Collector:
docker compose up -d
docker compose logs -f collector
另开一个终端,用 Python 标准库发送两条 StatsD 指标:
python3 - <<'PY'
import socket
payload = b'checkout.requests:1|c\ncheckout.latency:42|ms\n'
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.sendto(payload, ('127.0.0.1', 8125))
print('sent StatsD metrics')
PY
sleep 12
curl -s http://localhost:9464/metrics | grep -E 'checkout(_|\.)'
aggregation_interval 设置为 10 秒,所以发送后需要等待一个聚合窗口。若没有结果,应先查看 Collector 日志,确认 Receiver 是否启动、UDP 端口是否映射,以及当前 Collector 版本是否调整了组件配置。
生产环境不要直接使用 latest。应把镜像替换成经过验证的固定版本,并把 Collector 配置、镜像版本和回滚配置放进同一个发布单元。
双跑期间比较什么
迁移验收不能只看“新后端里出现了数据”。更有效的方法是选择一组黄金指标,让旧管道和新管道同时运行,并按固定窗口比较。
比较数据语义
- Counter 的增量和长期增长率是否接近;
- Gauge 在相同时间点的值是否一致;
- Histogram 的 count、sum 和桶边界是否符合预期;
- 标签集合、默认资源属性和单位是否改变;
- 进程重启、主机替换和网络中断后是否出现异常尖峰。
不要要求每个采样点完全相等。聚合窗口、刷新时刻和传输延迟不同,都会产生短期偏差。可以比较 5 分钟或 15 分钟窗口内的总量、增长率和缺失比例,并为每类指标设置单独阈值。
比较平台行为
Collector 自身也必须被监控,重点包括:
- 接收、拒绝和丢弃的数据点数量;
- Exporter 发送失败、重试和队列使用情况;
- CPU、内存以及每秒处理的数据点;
- 配置加载失败和 Collector 重启次数;
- 新旧管道端到端延迟。
具体内部指标名称可能随 Collector 版本变化,因此应基于所选版本的自监控端点建立仪表盘,而不是把未经验证的指标名硬编码进发布条件。
避免双写造成双重计数
如果同一业务指标通过旧、新管道同时写进同一个生产数据集,查询可能把两份数据相加。双跑阶段应使用隔离租户、独立命名空间或明确的迁移来源属性。验证完成后,再让查询和告警切换到新数据源。
按故障域推进,而不是按百分比推进
“已经迁移 30% 主机”并不能说明风险是否下降。更有意义的批次划分方式是故障域:
- 先选内部环境和低风险服务;
- 再覆盖一种操作系统、一类运行时或一个集群;
- 单独验证高吞吐、短生命周期任务和高基数业务;
- 最后处理承担关键告警的指标。
每个批次都应定义自动停止条件,例如新管道缺失率超过阈值、Collector 内存持续增长、Exporter 队列接近上限,或关键告警与旧管道产生明显分歧。回滚应只需要恢复流量路由或上一版配置,而不是临时修改应用代码。
上线前的检查清单
- 已建立核心指标的名称、类型、单位和标签映射表;
- 已明确 delta 与 cumulative 的转换位置;
- 已对 Timer、Summary 和 Histogram 做聚合正确性测试;
- 已设置标签基数预算并阻止请求 ID 等无界属性;
- 已监控 Collector 的拒绝、丢弃、重试、队列和资源消耗;
- 双跑数据彼此隔离,不会让生产查询重复计数;
- 每个迁移批次都有停止条件和可演练的回滚路径;
- Collector 使用固定镜像版本,配置经过自动校验;
- StatsD 兼容入口有明确的下线日期和责任人。
从 gostatsd 走向 OpenTelemetry 的关键,不是尽快关掉旧 sidecar,而是把原来隐藏在实现中的指标契约显式化。先用兼容桥降低切换风险,再用语义对比、管道自监控和分批回滚验证每一步,才有机会让一场大规模迁移变成一系列可控制的小变更。