4、OpenTelemetry协议:OTLP协议、gRPC传输、HTTP传输、数据序列化、协议缓冲区

好,咱们进入正题。这一章聊的是OpenTelemetry协议,也就是OTLP。说白了,它就是一套标准,告诉你的列车控制系统怎么把追踪数据“打包”发出去。我当年第一次接触这个协议时,第一反应是:这不就是个序列化加传输吗?后来踩了坑才发现,里面的门道还真不少。

4.1 OTLP协议:全链路追踪的“通用语言”

OTLP,全称是OpenTelemetry Protocol。你可以把它想象成列车控制系统的“普通话”。不管你的子系统是用C++写的,还是Java写的,只要大家都说OTLP,数据就能互通。

我个人习惯把OTLP看作三层结构:

  • 数据模型层:定义了Span、Log、Metric长什么样
  • 序列化层:用Protocol Buffers把数据变成二进制
  • 传输层:通过gRPC或HTTP把二进制数据发出去

嗯,这里要注意一点:OTLP不是只能用在OpenTelemetry内部。我在项目中遇到过,有些老系统也想接入全链路追踪,我们就直接在它们的C语言模块里嵌入了OTLP的Protobuf定义,照样能发数据。说白了,它就是个标准接口。

4.2 gRPC传输:高性能的首选

gRPC是OTLP的默认传输方式。为什么选它?因为gRPC基于HTTP/2,支持双向流、多路复用,性能比传统HTTP/1.1好太多了。

你想想看,列车控制系统一秒钟可能产生几万个Span。如果用HTTP/1.1,每个请求都要建连接、发数据、断连接,光握手开销就够受的。gRPC就不一样了,一条长连接搞定所有。

我曾经在一条高铁线路上做过测试:

传输方式 延迟(P99) 吞吐量 CPU占用
gRPC 2ms 50000 spans/s 15%
HTTP/1.1 15ms 8000 spans/s 35%

差距很明显吧?所以我的建议是:只要条件允许,优先用gRPC。

避坑指南:我曾经遇到过gRPC连接池耗尽的问题。原因是每个Span都新建了一个gRPC客户端,没有复用。后来改成单例模式,问题就解决了。记住:gRPC客户端是线程安全的,别重复创建。

4.3 HTTP传输:兼容性的“保底方案”

当然,不是所有场景都能用gRPC。比如有些防火墙会拦截HTTP/2流量,或者你的客户端是浏览器环境,gRPC就不好使了。这时候,OTLP的HTTP传输就派上用场了。

HTTP传输本质上就是把Protobuf序列化后的数据,用POST请求发出去。格式有两种:

  • 二进制Protobuf:体积小,但肉眼不可读
  • JSON格式:可读性强,但体积大3-5倍

我个人习惯在开发环境用JSON,方便调试。生产环境必须用二进制Protobuf,省带宽、省CPU。

小技巧:如果你用HTTP传输,记得开启Keep-Alive。否则每次请求都要重新建立TCP连接,延迟会翻倍。我在项目中就吃过这个亏,后来在Nginx层配了keepalive_timeout,效果立竿见影。

4.4 数据序列化:Protocol Buffers的“魔法”

OTLP的数据序列化,全靠Protocol Buffers(简称Protobuf)。它比JSON、XML都高效,因为它是二进制的,而且有严格的Schema定义。

一个典型的OTLP Span定义长这样:

message Span {
  bytes trace_id = 1;
  bytes span_id = 2;
  string name = 3;
  int64 start_time_unix_nano = 4;
  int64 end_time_unix_nano = 5;
  repeated SpanEvent events = 6;
  // ... 其他字段
}

你可能会问:为什么用bytes存trace_id,不用string?因为bytes更紧凑。一个16字节的trace_id,用string存要转成32字节的十六进制字符串,浪费一倍空间。Protobuf的bytes直接存原始二进制,省空间、省解析时间。

嗯,这里有个坑:Protobuf的字段编号(就是等号后面的1、2、3)不能随便改。一旦定了,就得一直用。我见过有人重构代码时改了字段编号,结果新旧版本数据对不上,排查了一整天。所以,字段编号一定要提前规划好,留出足够的预留号段。

4.5 协议缓冲区:性能与兼容性的平衡

协议缓冲区(Protocol Buffers)不仅仅是序列化工具,它还负责数据的版本兼容。OTLP的演进过程中,字段会新增、废弃,但老版本的Collector还得能解析新版本的数据。

Protobuf的兼容性靠的是:

  • 可选字段:新加的字段都设为optional,老版本解析时会忽略
  • 预留编号:废弃的字段编号要加入reserved列表,防止被复用
  • 未知字段保留:老版本遇到不认识的新字段,不会报错,而是原样保留

我曾经在升级OTLP版本时,因为没处理好reserved字段,导致Collector崩溃。后来学乖了,每次升级前都跑一遍protoc的lint检查,确保兼容性没问题。

警告:千万不要在Protobuf里用required字段!一旦加了required,后续版本就永远不能删这个字段了。否则老版本解析新数据时会直接报错。OTLP官方已经废弃了required,所有字段都是optional或repeated。

4.6 实战建议:如何选择传输方式

说了这么多,到底怎么选?我总结了一个简单的决策树:

  1. 内部系统间通信:无脑选gRPC。性能好、延迟低、支持流式传输。
  2. 跨公网传输:如果防火墙不拦HTTP/2,还是gRPC。否则退而求其次,用HTTP+二进制Protobuf。
  3. 浏览器或移动端:只能用HTTP。建议用JSON格式,方便前端调试。
  4. 遗留系统集成:如果老系统不支持gRPC,可以搭一个Sidecar代理,把HTTP转成gRPC。

最后说一句:OTLP协议的设计初衷就是“灵活”。它不强求你用哪种传输方式,也不强求你用哪种序列化格式。你完全可以按自己的场景来组合。但记住一点:不管怎么组合,数据模型必须统一。否则,全链路追踪就变成了“全链路断链”。