Apache APISIX zipkin 插件实战:基于 Zipkin v1/v2 API 的分布式链路追踪接入指南
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
zipkin 是 Apache APISIX 内置的分布式链路追踪插件,它按照 Zipkin API 规范收集并上报请求的 trace 数据,既可与 Zipkin Collector 对接,也可上报到同样兼容 Zipkin v1/v2 API 的 Apache SkyWalking、Jaeger 等追踪后端。读完本文,你将掌握 zipkin 插件的全部配置项与默认值、两种 span 版本下的 Span 结构差异、通过 Admin API 在路由上启停插件的方法,以及如何把 zipkin 上下文注入访问日志与错误日志,实现全链路可观测。
插件概述
Zipkin 是一个开源的分布式追踪系统。APISIX 的zipkin插件负责在网关侧为每个被追踪的请求生成 trace/span,并按照 Zipkin API 规范 将链路数据上报到 Zipkin Collector。由于 Zipkin v1、v2 两种 API 已成为事实上的行业标准,该插件同样可以对接支持这两种 API 的 Apache SkyWalking(通过其 Zipkin Receiver)和 Jaeger(通过其 Zipkin 兼容端点),以及其他兼容 Zipkin v1/v2 API 格式的追踪系统。
插件的主逻辑位于 apisix/plugins/zipkin.lua,核心子模块(编解码、采样器、上报器)位于 apisix/plugins/zipkin/ 目录下,具体包括:
codec.lua:负责在 HTTP 头与 OpenTracing SpanContext 之间进行注入(inject)与提取(extract),即 B3 传播协议的编解码;random_sampler.lua:随机采样器,按sample_ratio决定请求是否被采样;reporter.lua:负责把 Span 批量编码为 Zipkin v2 JSON 格式并上报到指定端点。
属性配置说明
zipkin 插件的 Schema 定义在 apisix/plugins/zipkin.lua,各属性如下:
| 名称 | 类型 | 必填 | 默认值 | 合法值 | 说明 |
|---|---|---|---|---|---|
| endpoint | string | 是 | 无 | Zipkin HTTP 上报端点,例如http://127.0.0.1:9411/api/v2/spans | |
| sample_ratio | number | 是 | 无 | [0.00001, 1] | 请求采样频率。设为1表示采样全部请求 |
| service_name | string | 否 | "APISIX" | 上报到 Zipkin 时展示的服务名 | |
| server_addr | string | 否 | $server_addr | 上报端点的 IPv4 地址,可指定你的外部 IP | |
| span_version | integer | 否 | 2 | [1, 2] | span 类型版本 |
从源码的 Schema 校验逻辑可以印证这些约束:sample_ratio的取值为minimum = 0.00001, maximum = 1;server_addr必须匹配 IPv4 正则^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$;span_version仅允许取枚举值1或2(源码中以常量ZIPKIN_SPAN_VER_1、ZIPKIN_SPAN_VER_2表示),且endpoint、sample_ratio为必填项。另外在check_schema中还会通过core.utils.check_https校验endpoint的安全性,确保不会上报到非法的目标地址。
在 t/plugin/zipkin.t 测试中可以看到这些校验规则的实测结果,例如sample_ratio = -0.1会报错expected -0.1 to be at least 1e-05,sample_ratio = 2会报错expected 2 to be at most 1,server_addr = 'badip'会因不匹配 IPv4 正则而被拒绝。
不同 span 版本生成的 Span 结构
每个被追踪的请求默认(span_version = 2)会生成如下两个 Span:
request ├── proxy: 从请求开始到 header filter 阶段开始 └── response: 从 header filter 阶段开始到 log 阶段开始对于旧版本(将span_version设置为1),则生成以下 Span 树:
request ├── rewrite ├── access └── proxy └── body_filter需要注意:Span 的名称并不对应 Nginx 的 phase 名称。例如 v2 版本下的proxyspan 实际是从请求开始到 header filter 开始,而responsespan 是从 header filter 到 log 阶段;v1 版本下rewrite、access、proxy、body_filter这些命名同样是 APISIX 内部定义的语义化名称,并非直接映射到 OpenResty 的 rewrite_by_lua、access_by_lua 等执行阶段。从 zipkin.lua 的源码可以确认:v1 模式下 rewrite 阶段的 span 在_M.rewrite内创建并立即结束,access 阶段的 span 在_M.access内创建并结束,然后开启 proxy span;v2 模式下则直接创建apisix.proxy子 span,并在 header_filter 阶段结束时结束它、开启apisix.response_span。
上游服务的追踪上下文对接示例
zipkin 插件会在请求转发给上游时,通过 B3 传播协议把 trace 上下文注入 HTTP 头(如x-b3-traceid、x-b3-spanid、x-b3-parentspanid、x-b3-sampled等)。如果你的上游服务希望把自身的链路信息挂接到 APISIX 发起的这条 trace 上,就需要在服务侧提取这些头部并创建子 Span。下面是文档提供的 Go 语言示例(使用 Gin 框架和 openzipkin 客户端库):
func GetTracer(serviceName string, port int, enpoitUrl string, rate float64) *zipkin.Tracer { // create a reporter to be used by the tracer reporter := httpreporter.NewReporter(enpoitUrl) // set-up the local endpoint for our service host is ip:host thisip, _ := GetLocalIP() host := fmt.Sprintf("%s:%d", thisip, port) endpoint, _ := zipkin.NewEndpoint(serviceName, host) // set-up our sampling strategy sampler, _ := zipkin.NewCountingSampler(rate) // initialize the tracer tracer, _ := zipkin.NewTracer( reporter, zipkin.WithLocalEndpoint(endpoint), zipkin.WithSampler(sampler), ) return tracer } func main(){ r := gin.Default() tracer := GetTracer(...) // use middleware to extract parentID from http header that injected by APISIX r.Use(func(c *gin.Context) { span := this.Tracer.Extract(b3.ExtractHTTP(c.Request)) childSpan := this.Tracer.StartSpan(spanName, zipkin.Parent(span)) defer childSpan.Finish() c.Next() }) }核心思路是:上游服务用b3.ExtractHTTP从请求头中提取 APISIX 注入的 B3 上下文,再以zipkin.Parent(span)挂起子 Span,从而把网关与后端服务的调用链串成一条完整的 trace。
通过 Admin API 启用插件
以下示例为指定 Route 启用 zipkin 插件。首先可以从config.yaml中取出admin_key并保存到环境变量:
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')然后调用 Admin API 创建/更新路由并挂载插件:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "methods": ["GET"], "uri": "/index.html", "plugins": { "zipkin": { "endpoint": "http://127.0.0.1:9411/api/v2/spans", "sample_ratio": 1, "service_name": "APISIX-IN-SG", "server_addr": "192.168.3.50" } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'上述配置将sample_ratio设为1,表示该路由上的所有请求都会被采样上报。Admin API 默认监听9180端口,X-API-KEY用于身份认证,具体鉴权配置可参考 conf/config.yaml 中的deployment.admin部分。
实例演示:在 Zipkin UI 中查看 trace
首先需要有一个正在运行的 Zipkin 实例,可以非常方便地用 Docker 启动:
docker run -d -p 9411:9411 openzipkin/zipkin然后向 APISIX 发起请求,即可看到链路数据被写入 Zipkin:
curl http://127.0.0.1:9080/index.htmlHTTP/1.1 200 OK ...接着在浏览器中打开 Zipkin 的 Web UI(地址为http://127.0.0.1:9411/zipkin)即可查询刚才生成的 trace:
将链路上报到 Jaeger
该插件同样支持把 trace 上报到 Jaeger。首先需要一个正在运行的 Jaeger 实例,同样可以用 Docker 启动,关键在于开启其 Zipkin 兼容端点(COLLECTOR_ZIPKIN_HOST_PORT=:9411):
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 16686:16686 \ -p 9411:9411 \ jaegertracing/all-in-one:1.31配置方式与对接 Zipkin 时完全一致——只需把endpoint指向 Jaeger 的 Zipkin 兼容接收地址即可:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "methods": ["GET"], "uri": "/index.html", "plugins": { "zipkin": { "endpoint": "http://127.0.0.1:9411/api/v2/spans", "sample_ratio": 1, "service_name": "APISIX-IN-SG", "server_addr": "192.168.3.50" } }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'注意:这里endpoint仍然填写http://127.0.0.1:9411/api/v2/spans,因为 Jaeger 通过9411端口提供 Zipkin v2 兼容接口,APISIX 无需感知后端的真实身份。
配置完成后发起请求:
curl http://127.0.0.1:9080/index.htmlHTTP/1.1 200 OK ...之后即可访问 Jaeger UI(http://127.0.0.1:16686)查看 trace。
删除插件
要移除zipkin插件,只需把 Route 配置中plugins对象里的 zipkin 配置删除(置空即可)。APISIX 会自动热加载,无需重启即可生效:
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' { "methods": ["GET"], "uri": "/index.html", "plugins": { }, "upstream": { "type": "roundrobin", "nodes": { "127.0.0.1:1980": 1 } } }'使用 nginx 变量输出 trace 上下文
zipkin 插件会设置以下 nginx 变量,方便你在访问日志、错误日志中携带链路标识:
zipkin_context_traceparent—— W3C trace context 格式,例如:00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01zipkin_trace_id—— 当前 Span 的 Trace Idzipkin_span_id—— 当前 Span 的 Span Id
这些变量并非默认导出,需要在conf/config.yaml中开启plugin_attr.zipkin.set_ngx_var: true,并把这些变量加入access_log_format:
http: enable_access_log: true access_log: "/dev/stdout" access_log_format: '{"time": "$time_iso8601","zipkin_context_traceparent": "$zipkin_context_traceparent","zipkin_trace_id": "$zipkin_trace_id","zipkin_span_id": "$zipkin_span_id","remote_addr": "$remote_addr","uri": "$uri"}' access_log_format_escape: json plugins: - zipkin plugin_attr: zipkin: set_ngx_var: true在 conf/config.yaml.example 中可以看到这一配置项的默认形态(set_ngx_var: false,即默认不导出)。开启后,zipkin_context_traceparent、zipkin_trace_id、zipkin_span_id这三个变量才会被注册,对应的 nginx 模板片段位于 apisix/cli/ngx_tpl.lua,模板会根据zipkin_set_ngx_var是否为真来决定是否执行set $zipkin_context_traceparent '';等变量声明。
在 Lua 业务代码中,也可以直接通过ngx.var读取这些变量来拼接日志,例如在打印错误日志时带上zipkin_trace_id:
log.error(ngx.ERR,ngx_var.zipkin_trace_id,"error message")从源码 zipkin.lua 可以看到变量赋值的实现:当plugin_info.set_ngx_var为真时,插件会把 SpanContext 的 trace_id、span_id 分别写入zipkin_trace_id、zipkin_span_id,并按 W3C traceparent 格式(00-<trace_id>-<span_id>-<flags>)拼装出zipkin_context_traceparent。
源码级原理剖析:插件如何工作
1. 采样决策与 B3 头解析
在_M.rewrite阶段,插件会做三件事:创建/复用 tracer、解析客户端传入的追踪头、决定本次请求是否采样。
- tracer 复用:tracer 对象按
server_addr .. server_port缓存在插件级 lrucache 中(zipkin.lua),因为服务端地址和端口在进程生命周期内不会变化,避免每个请求都重复创建 tracer 与上报器。 - B3 头解析:优先解析单头的
b3头(支持0、traceId-spanId、traceId-spanId-sampled、traceId-spanId-sampled-parentSpanId及 debug 标志d等形态),否则退化为解析多头的x-b3-traceid、x-b3-spanid、x-b3-parentspanid、x-b3-sampled。如果b3头非法,会直接返回400并记录错误日志(测试 zipkin.t 中验证了非法b3头返回 400 且不会在 log 阶段因 opentracing 上下文为 nil 而报错)。此外x-b3-flags: 1表示 debug 强制采样,会覆盖采样策略。 - 采样优先级:客户端显式传入的
x-b3-sampled(1/true或0/false)优先于插件配置的sample_ratio;否则由 random_sampler.lua 中的math.random() < sample_ratio决定。采样结果会作为x-b3-sampledbaggage 传递,并影响后续是否上报。
2. Span 的创建与生命周期
requestspan 在 rewrite 阶段以ngx.req.start_time()作为起始时间戳创建,并打上component、span.kind、http.method、http.url、peer.ipv4、peer.port等标签;http.status_code标签则是在 log 阶段通过core.response.get_upstream_status(ctx)获取上游响应码后补充的(zipkin.lua)。
v2 模式下:rewrite 阶段创建apisix.proxy子 span → header_filter 阶段结束 proxy span 并创建apisix.response_span→ log 阶段结束 response span 与根 span。
v1 模式下:rewrite 阶段创建并立即结束apisix.rewrite→ access 阶段创建并结束apisix.access,随后开启apisix.proxy→ header_filter 阶段开启apisix.body_filter→ log 阶段结束 body_filter、proxy 与根 span。
3. 传播上下文到上游
在_M.access阶段,插件调用inject_header,通过 codec 的new_injector(codec.lua)把 SpanContext 编码为x-b3-traceid、x-b3-spanid、x-b3-parentspanid、x-b3-sampled等头部注入请求;同时,如果 SpanContext 携带 baggage(如 Jaeger 的uberctx-*头),也会以 URL 转义后的形式一并透传给上游。
4. 批量上报与失败重试
reporter.lua 使用apisix.utils.batch-processor做批量上报:batch_max_size = 1000(每批最多 1000 个 span)、buffer_duration = 60(最多缓存 60 秒)、inactive_timeout = 5(空闲 5 秒即触发 flush)、retry_delay = 1(失败后延迟 1 秒重试)。上报时通过cjson.encode_number_precision(16)保证 64 位 trace_id 不丢精度,并开启 HTTP keepalive(keepalive 5000ms、连接池 5)。时间戳与时长统一转换为微秒(* 1000000)以符合 Zipkin v2 JSON 规范,Span 的kind会映射为 Zipkin 的CLIENT/SERVER/PRODUCER/CONSUMER枚举。
小结
zipkin 插件为 APISIX 网关提供了开箱即用的分布式追踪能力:通过endpoint+sample_ratio两个必填项即可快速接入 Zipkin,并天然兼容 Jaeger、SkyWalking 等支持 Zipkin API 的后端;通过span_version可以切换新旧两种 Span 结构;通过set_ngx_var可以把 trace 上下文引入访问日志与业务日志,实现从网关到后端的全链路可观测。如果想要深入验证插件行为,可以参考 t/plugin/zipkin.t、t/plugin/zipkin2.t、t/plugin/zipkin3.t 中的完整测试用例。
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考