为什么Nginx需要传递追踪头
在微服务架构中,Jaeger等分布式追踪系统依赖请求头中的追踪上下文来串联整个调用链。当客户端发起请求经过Nginx反向代理到达后端服务时,如果Nginx没有将追踪头(如traceparent、X-B3-TraceId)原样转发,后端生成的span将无法关联到同一个trace,导致Jaeger UI中链路断裂或丢失节点。
常见的追踪头规范包括W3C Trace Context(traceparent、tracestate)和Zipkin B3(X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId、X-B3-Sampled、X-B3-Flags)。Nginx默认的proxy_pass行为会转发所有请求头,但某些情况下需要显式配置以确保头不被丢弃或重写。例如使用proxy_set_header进行定制时,如果没有正确传递这些头,就会丢失追踪信息。
此外,Nginx自身不产生span,但可以集成OpenTracing模块来生成Nginx层的span,这时候也需要配置如何传递上下文。下文从手动配置和插件两种方式展开,详细说明如何在Nginx中可靠地传递Jaeger所需的追踪头。

手动配置Nginx转发追踪头
最直接的方法是在nginx配置中使用proxy_set_header指令,把客户端的追踪头传递给上游。需要明确指定各个头的名称。对于W3C Trace Context,有两个关键头:traceparent和tracestate。对于B3,则有多个如X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId、X-B3-Sampled、X-B3-Flags。
下面的配置展示了如何同时支持两种规范。注意,如果客户端没有发送某个头,变量为空会导致发送空值头。为了避免发送无意义的空头,可以使用map指令定义变量,当原值为空时保持空值,但proxy_set_header仍会发送空头,后端通常会忽略空头。更严格的做法是使用第三方模块或Lua脚本进行条件判断,但基础场景下直接映射即可满足需求。
# 在 http 或 server 块中定义 map,避免发送空头
map $http_traceparent $traceparent_header {
default $http_traceparent;
"" "";
}
map $http_tracestate $tracestate_header {
default $http_tracestate;
"" "";
}
map $http_x_b3_traceid $b3_traceid_header {
default $http_x_b3_traceid;
"" "";
}
map $http_x_b3_spanid $b3_spanid_header {
default $http_x_b3_spanid;
"" "";
}
map $http_x_b3_parentspanid $b3_parentspanid_header {
default $http_x_b3_parentspanid;
"" "";
}
map $http_x_b3_sampled $b3_sampled_header {
default $http_x_b3_sampled;
"" "";
}
map $http_x_b3_flags $b3_flags_header {
default $http_x_b3_flags;
"" "";
}
server {
listen 80;
server_name ippipp.com;
location / {
proxy_pass http://backend;
# W3C Trace Context
proxy_set_header traceparent $traceparent_header;
proxy_set_header tracestate $tracestate_header;
# B3
proxy_set_header X-B3-TraceId $b3_traceid_header;
proxy_set_header X-B3-SpanId $b3_spanid_header;
proxy_set_header X-B3-ParentSpanId $b3_parentspanid_header;
proxy_set_header X-B3-Sampled $b3_sampled_header;
proxy_set_header X-B3-Flags $b3_flags_header;
# 其他必要头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
以上配置虽然能够传递,但存在一个隐患:如果客户端发送的追踪头是合法的但格式不符合预期,Nginx无法进行校验或调整。而且这种方式需要手动维护所有可能的头名称,当新规范出现时需要更新配置。另外,map中default和空字符串的写法略显冗余,某些团队为了简洁只配置W3C traceparent,因为它已是当前标准,Jaeger客户端普遍支持。
当Nginx作为入口时,如果客户端没有发送traceparent,是否希望Nginx生成一个新的trace?如果希望,单纯使用proxy_set_header无法实现,需要借助Lua、njs脚本或nginx-opentracing模块来自动创建追踪上下文。
使用nginx-opentracing模块实现自动传递
nginx-opentracing是一个开源模块,能够将Nginx集成到OpenTracing生态中,自动为每个请求创建span,并负责提取和注入追踪上下文。它支持多种tracer,包括Jaeger(通过jaeger-client-cpp)。使用该模块可以省去手动配置proxy_set_header的麻烦,模块会自动处理头传递,同时还能生成Nginx层的span,让链路更完整。
安装nginx-opentracing通常需要编译Nginx并添加动态模块。以Ubuntu为例,可以从OpenTracing官方仓库下载预编译模块或使用包管理器。安装后需要在nginx.conf中加载模块,并配置tracer。下面是一个使用Jaeger tracer的示例。
# 加载模块(根据实际路径)
load_module modules/ngx_http_opentracing_module.so;
http {
# 定义OpenTracing配置
opentracing_load_tracer /usr/local/lib/libjaegertracing_plugin.so /etc/jaeger-nginx-config.json;
# 开启追踪
opentracing on;
# 可选:设置span操作名
opentracing_operation_name $request_method $uri;
# 可选:设置标签
opentracing_tag http_user_agent $http_user_agent;
server {
listen 80;
location / {
opentracing_propagate_context;
proxy_pass http://backend;
}
}
}
其中jaeger-nginx-config.json是Jaeger tracer的配置文件,内容类似:
{
"service_name": "nginx",
"sampler": {
"type": "const",
"param": 1
},
"reporter": {
"localAgentHostPort": "127.0.0.1:6831"
},
"headers": {
"jaegerDebugHeader": "jaeger-debug-id",
"jaegerBaggageHeader": "jaeger-baggage",
"traceBaggageHeaderPrefix": "uberctx-",
"traceContextHeaderName": "uber-trace-id"
}
}
nginx-opentracing模块默认支持W3C traceparent格式,通过配置可以指定传播格式。当请求到达Nginx时,模块会从请求头中提取追踪上下文(如traceparent或B3),如果不存在则会创建新的root span。在转发请求时,模块自动将上下文注入到上游请求头中,无需手动设置proxy_set_header。这使得配置更加简洁,且不易出错。
需要注意的是,如果后端服务使用的追踪头格式与模块默认不同,可能需要调整配置或使用opentracing_propagate_context指令强制传播特定格式。此外,该模块要求Nginx开启多线程支持,并依赖动态库,部署时需要仔细核对版本兼容性。
验证追踪头传递是否正确
配置完成后,需要验证Nginx是否成功传递追踪头。最简单的方法是启动一个简单的HTTP服务器(如Python的http.server或使用nc)作为上游,打印接收到的请求头。可以通过curl模拟带追踪头的请求,观察后端接收情况。
例如,使用Python启动一个简单的HTTP服务:
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
print("Headers received:")
for k, v in self.headers.items():
print(f"{k}: {v}")
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
if __name__ == "__main__":
server = HTTPServer(("127.0.0.1", 8000), Handler)
print("Listening on 8000")
server.serve_forever()
然后在Nginx配置中将proxy_pass指向127.0.0.1:8000,使用curl发送带有traceparent的请求:
curl -H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" http://localhost/
观察Python服务输出,应该能看到traceparent头被原样传递。如果使用nginx-opentracing模块,还可以在Jaeger UI中看到Nginx作为一个独立的服务出现在链路中,并且上下的span能够正确关联。
如果没有收到头,需要检查Nginx配置中是否覆盖了proxy_set_header默认行为,或者map变量是否为空。另外,某些CDN或中间层可能会剥离追踪头,需要确保链路中每个环节都支持传递。对于B3格式,可以额外使用X-B3-TraceId进行验证。
总结
Nginx在分布式追踪中扮演关键角色,正确传递追踪头是保证链路完整的基础。手动配置proxy_set_header可以满足简单需求,但需要维护头列表;使用nginx-opentracing模块则能自动化处理并生成span。建议在生产环境中优先采用模块方案,配合Jaeger实现端到端的可观测性。
无论采用哪种方式,都要在部署后验证追踪头是否完整传递,并监控Jaeger UI中的链路数据,确保每个请求都能关联到正确的trace。随着系统规模扩大,统一追踪头规范(推荐W3C Trace Context)可以大幅减少配置与排查成本。