- 云原生
- 容器运行时
【免费下载链接】cri-o
Open Container Initiative-based implementation of Kubernetes Container Runtime Interface
导读
本文围绕 containerd 官方子项目otelttrpc展开,讲解如何为 ttRPC(containerd 的高性能轻量 RPC 框架)接入 OpenTelemetry 可观测性插桩:通过两个拦截器在客户端与服务端自动生成 trace span、传播上下文并记录 RPC 指标。读完本文,你将掌握otelttrpc.UnaryClientInterceptor/otelttrpc.UnaryServerInterceptor的接入方法、全部配置选项、底层实现原理,以及 CRI-O 如何在 NRI(Node Resource Interface)链路中实际复用这套插桩。本文以仓库内vendor/github.com/containerd/otelttrpc/README.md为骨架,并结合该目录下的 Go 源码进行源码级印证。
一、这是什么:ttRPC 的 OpenTelemetry 插桩包
otelttrpc是一个 Go 语言包,实现了对ttRPC的 OpenTelemetry 插桩支持。它的核心价值在于:无需在每个 RPC 方法体里手写埋点,只要在创建 ttRPC 客户端和服务端时挂上拦截器,就能自动为所有被调用(client 侧)与被服务(server 侧)的 unary RPC 方法生成 OpenTelemetry trace span。
从源码结构看,该包由如下几个关键文件构成(位于 vendor/github.com/containerd/otelttrpc):
| 文件 | 职责 |
|---|---|
| interceptor.go | 核心拦截器实现(UnaryClientInterceptor与UnaryServerInterceptor) |
| config.go | 插桩配置结构与全部Option(Propagators / TracerProvider / MeterProvider / MessageEvents) |
| metadata_supplier.go | 基于 ttRPC metadata 的上下文注入与提取(inject/extract) |
| semconv.go | ttRPC 相关的 OpenTelemetry 语义约定属性键 |
| internal/parse.go | 将 ttRPCFullMethod(形如/package.service/method)解析为 span 名与属性 |
| doc.go | 包级文档注释 |
| version.go | 插桩版本号(当前仓库内为0.0.0占位值) |
插桩能力通过两个拦截器对外提供:
UnaryClientInterceptor:用于 unary客户端,挂载在ttrpc.NewClient的ttrpc.UnaryClientInterceptor(...)选项上;UnaryServerInterceptor:用于 unary服务端,挂载在ttrpc.NewServer的ttrpc.WithUnaryServerInterceptor(...)选项上。
二、快速上手:把拦截器挂进 ttRPC 客户端与服务端
README 给出了最直接的使用方式——把两个拦截器分别作为ttrpc.ClientOpts与ttrpc.ServerOpt传入。完整示例如下:
import ( "github.com/containerd/ttrpc" "github.com/containerd/otelttrpc" ) // 客户端侧:在创建 ttrpc.Client 时挂上客户端拦截器 ... client := ttrpc.NewClient( conn, ttrpc.UnaryClientInterceptor( otelttrpc.UnaryClientInterceptor(), ), ) // 服务端侧:在创建 ttrpc.Server 时挂上服务端拦截器 ... server, err := ttrpc.NewServer( ttrpc.WithUnaryServerInterceptor( otelttrpc.UnaryServerInterceptor(), ), )两个拦截器函数本身都接受可选的...Option参数,例如可以这样显式传入自定义的TracerProvider:
client := ttrpc.NewClient( conn, ttrpc.UnaryClientInterceptor( otelttrpc.UnaryClientInterceptor( otelttrpc.WithTracerProvider(myTracerProvider), ), ), )一旦启用,拦截器会对所有被调用和被服务的unary方法调用生成 trace span。只要代码其余部分已正确配置 OpenTelemetry 的收集与导出链路,这些 span 就会出现在采集到的 trace 中。
说明:上游 README 还提到了 sample client 与 sample server 两个完整示例;当前 CRI-O 仓库中的 vendored 副本未包含
example/目录,如需参考完整示例请以 containerd/otelttrpc 上游仓库为准。
三、拦截器干了什么:从 span 创建到状态码映射
拦截器并非简单地包一层defer span.End(),其内部实现(见 interceptor.go)包含一条完整的可观测性流水线,值得逐段拆解。
3.1 客户端拦截器执行流程
UnaryClientInterceptor的执行逻辑(interceptor.go#L75-L120)大致为:
- 构建 Tracer:
newConfig(opts)合并所有选项,然后以插桩名github.com/containerd/otelttrpc创建 Tracer,并携带trace.WithInstrumentationVersion(Version())。 - 计算 span 名与属性:调用
spanInfo(info.FullMethod, peerFromCtx(ctx)),将 ttRPC 完整方法名解析为 span 名,并附加rpc.system=ttrpc、rpc.service、rpc.method以及对端地址等属性。 - 启动客户端 span:
tracer.Start(ctx, name, trace.WithSpanKind(trace.SpanKindClient), ...),随后defer span.End()。 - 注入上下文:
inject(ctx, cfg.Propagators, req)把当前 trace 上下文写入请求 metadata,使其跨进程传递到服务端。 - 记录消息事件(可选):若配置了
SentEvent,在调用前记录messageSent事件;若配置了ReceivedEvent,在调用返回后记录messageReceived事件。 - 执行真实 RPC:调用
invoker(ctx, req, reply)。 - 记录结果状态:若出错,从
status.FromError(err)解析出 gRPC 状态码,将 span 标记为codes.Error并附加rpc.ttrpc.status_code属性;成功则附加grpc_codes.OK。
3.2 服务端拦截器执行流程
UnaryServerInterceptor(interceptor.go#L124-L178)与之对称:
- 提取上下文:
extract(ctx, cfg.Propagators)从请求 metadata 中还原客户端注入的 trace 上下文,从而把服务端 span 正确地挂在客户端 span 之下(形成跨进程父子关系)。 - 启动服务端 span:使用
trace.SpanKindServer启动 span。 - 记录指标:通过
defer计算 RPC 耗时(毫秒),记录到名为rpc.server.duration的Int64Histogram指标中(单位ms),并附带全部 RPC 属性。 - 执行方法:调用
method(ctx, unmarshal)。 - 状态映射:出错时通过
serverStatus(s)做映射——只有Unknown、DeadlineExceeded、Unimplemented、Internal、Unavailable、DataLoss这几类错误会把 span 标记为Error,其余错误 span 状态保持Unset;成功则附加grpc_codes.OK属性。
这段状态映射逻辑(interceptor.go#L243-L255)遵循 OpenTelemetry 对 RPC span 的推荐做法:并非所有错误都值得把 span 标红,只有真正表示服务端内部故障/不可达/未实现等严重错误才提升 span 状态为 Error。
3.3 span 命名与语义属性
span 的名字和属性来自spanInfo与internal.ParseFullMethod(internal/parse.go):
- 入参
FullMethod形如/ttrpc.v1.WhateverService/MethodName; ParseFullMethod去掉开头的/后按/切分,得到 service 与 method;- span 名采用切分后的完整
service/method字符串; - 附加属性包括
rpc.service=<service>与rpc.method=<method>(分别对应semconv.RPCService/semconv.RPCMethod)。
在 semconv.go 中定义了三组语义约定:
- 系统标识:
RPCSystemTTRPC = rpc.system: "ttrpc",标明遥测来源系统是 ttRPC; - 消息相关键:
name、message.type、message.id,配合RPCMessageTypeSent = "SENT"与RPCMessageTypeReceived = "RECEIVED"用于记录消息收发事件; - 状态码键:
rpc.ttrpc.status_code(定义在 config.go),用于在 span 上记录数字形式的 RPC 状态码。
对端地址属性由peerAttr(interceptor.go#L191-L219)生成:解析host:port后,若 host 是 IP 则使用net.sock.peer.addr/net.sock.peer.port,否则使用net.peer.name/net.peer.port;解析失败则返回空属性集。注意当前实现里peerFromCtx恒返回空字符串(源码中留有 TODO 注释),因此对端属性在实际运行时通常不会填充——这是该插桩当前的一个已知实现细节。
四、可配置项:四个 Option 与默认行为
otelttrpc的配置模型定义在 config.go:newConfig先填充 OpenTelemetry 全局默认值,再逐个应用传入的Option。默认情况下:
Propagators使用全局otel.GetTextMapPropagator();TracerProvider使用全局otel.GetTracerProvider();MeterProvider使用全局otel.GetMeterProvider();ReceivedEvent/SentEvent均为false,即默认不记录消息级事件,只在请求结束时附加汇总属性。
4.1 WithPropagators
func WithPropagators(p propagation.TextMapPropagator) Option设置用于在请求中注入/提取 trace 上下文的TextMapPropagator。若不提供,则使用全局 TextMapPropagator。常见做法是组合propagation.TraceContext{}与propagation.Baggage{}。
4.2 WithTracerProvider
func WithTracerProvider(tp trace.TracerProvider) Option设置创建 Tracer 所用的TracerProvider,用于隔离插桩的 trace 输出目标。不提供时回退到全局 TracerProvider。
4.3 WithMeterProvider
func WithMeterProvider(mp metric.MeterProvider) Option设置创建 Meter 所用的MeterProvider。Meter 负责注册rpc.server.duration指标(Int64Histogram,单位ms,使用语义约定 Schema URL);若指标注册失败,错误会被交给otel.Handle处理,不会导致拦截器崩溃。
4.4 WithMessageEvents
func WithMessageEvents(events ...Event) Option配置在 span 上记录消息收发事件(span.AddEvent)。合法值:
ReceivedEvents:为每条收到的消息记录事件;SentEvents:为每条发送的消息记录事件。
消息事件通过messageType.Event(interceptor.go#L57-L66)实现:事件名为"message",属性为message.type(SENT/RECEIVED)与message.id。仅在 span 处于 recording 状态时才真正写入,避免空开销。
五、上下文传播:metadata_supplier 的注入与提取
分布式 trace 的关键是让客户端与服务端的 span 在逻辑上串成一条链路。otelttrpc借助 ttRPC 的 metadata 机制完成跨进程传播,实现位于 metadata_supplier.go:
metadataSupplier结构体包装*ttrpc.MD,实现了 OpenTelemetry 的propagation.TextMapCarrier接口(源码中通过var _ propagation.TextMapCarrier = &metadataSupplier{}做了编译期断言),提供Get/Set/Keys三个方法。inject(客户端侧):从 context 取出(或新建)metadata,用 Propagators 把 trace 上下文写入其中;为避免并发读写 panic,会先Clone()一份;随后合并请求自带的 metadata(冲突时以上下文中的为准,见keep non-conflicting metadata from req注释),最后把结果写回req.Metadata并返回携带新 metadata 的 context。extract(服务端侧):从 context 取出 metadata,用 Propagators 还原出远端 trace 上下文,供tracer.Start建立父子 span 关系。
六、在 CRI-O 中的实际应用:NRI 链路的追踪
otelttrpc并不是一个"只存在于 vendor 目录"的孤立依赖——它在 CRI-O 中承担着 NRI(Node Resource Interface)插件通信的可观测性职责。源码证据如下:
- NRI 配置层:internal/config/nri/nri.go#L134-L149 中,
Config.ToOptions()在withTracing为真时,会为 NRI 客户端注入ttrpc.WithUnaryClientInterceptor(otelttrpc.UnaryClientInterceptor()),为 NRI 服务端注入ttrpc.WithUnaryServerInterceptor(otelttrpc.UnaryServerInterceptor()),从而让 CRI-O 与 NRI 插件之间的每次 ttRPC 调用都带上 OpenTelemetry span。 - 开关联动:server/server.go#L679 创建 NRI 接口时调用
s.config.NRI.WithTracing(s.config.EnableTracing),即 NRI 链路是否插桩,完全由 CRI-O 全局的enable_tracing开关控制。 - 全局 tracing 配置:
TracingConfig(pkg/config/config.go#L889-L900)包含enable_tracing、tracing_endpoint(默认127.0.0.1:4317)、tracing_sampling_rate_per_million(默认 0)三个字段;cmd/crio/main.go#L363-L372 在启动时据此调用opentelemetry.InitTracing初始化 exporter 与 TracerProvider,并作为otelgrpc.NewServerHandler的选项挂到 CRI gRPC 服务上。
启动方式(详见 tutorials/tracing.md):
sudo crio --enable-tracing --tracing-sampling-rate-per-million 1000000或在/etc/crio/crio.conf.d/01-tracing.conf中写:
[crio.tracing] enable_tracing = true tracing_endpoint = "127.0.0.1:4317" tracing_sampling_rate_per_million = 1000000采样率的语义:设为0时不采集任何 span;设为1000000时全量采集;介于两者之间时按当前实现只能以固定取模方式粗粒度抽样(无法精确选择某一子集)。
上图为 CRI-O 官方教程中的真实运行结果:启用 tracing 后,通过crictl ps等 CRI API 调用,即可在 Jaeger UI(http://localhost:16686)中看到由上述插桩生成的 trace 与 span;若 kubelet 侧也开启了 tracing,这些 span 还会因 CRI gRPC 调用中的 trace ID 传播而嵌套在 kubelet 的 trace 之下。
七、限制与注意事项
README 明确列出了当前插桩的边界,源码也印证了这一点:
- 仅支持 unary 调用:目前只有
UnaryClientInterceptor与UnaryServerInterceptor两个拦截器,只能插桩 unary 客户端与 unary 服务端方法;流式(streaming)接口的插桩尚未实现。 - 版本占位:当前 vendored 副本中 version.go 返回
"0.0.0",属于上游的占位版本号,会通过WithInstrumentationVersion写入插桩元数据。 - 对端属性受限:如前所述,
peerFromCtx目前无法真正取得对端地址,相关网络属性在多数场景下不会出现。 - 与导出链路解耦:插桩只负责生成 span 与指标;若进程没有配置 exporter / collector,数据不会落盘,拦截器本身不阻塞业务调用。
八、项目背景与许可
otelttrpc是containerd 官方子项目,遵循Apache License 2.0(许可证全文见 vendor/github.com/containerd/otelttrpc/LICENSE)。作为 containerd 子项目,其项目治理、维护者名单与贡献指南统一维护在 containerd/project 仓库中。当前 CRI-O 以 vendored 方式引入该包,因此上述源码可以直接在本仓库的vendor/github.com/containerd/otelttrpc/目录下阅读与验证。
- 云原生
- 容器运行时
【免费下载链接】cri-o
Open Container Initiative-based implementation of Kubernetes Container Runtime Interface
相关推荐
用 Twirp 构建 Protobuf RPC 服务:从 .proto 定义到自动生成客户端与服务端的 Go 实战指南
用 Twirp 构建 Protobuf RPC 服务:从 .proto 定义到自动生成客户端与服务端的 Go 实战指南 Twirp 是一个构建在 Protobu
RPC框架后端微服务如何用文献分析与总结工具告别通宵读文献?
如何用文献分析与总结工具告别通宵读文献? 凌晨一点,你的桌面上摊着 200 篇 PDF,开题报告的 deadline 还剩三天。你已经翻完了前六篇,唯一记住的是
开发工具代码生成API设计PouchDB 插件与外部项目生态指南:从客户端增强到服务端集成
PouchDB 插件与外部项目生态指南:从客户端增强到服务端集成 导读 PouchDB 在设计上就预留了开放的扩展点,官方与社区围绕它构建了大量插件、服务端组件
数据库数据同步
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考