CDN架构下排查性能问题最让人头疼的一件事,就是用户反馈页面慢,但你看CDN监控是正常的,看源站日志也是正常的,中间那段链路完全是个黑盒。请求从浏览器发出,经过CDN边缘节点、中间层、回源链路,最后到达源站,任何一跳都可能有延迟,但传统监控手段只能看到分段数据,无法串成一条完整链路。OpenTelemetry提供的分布式追踪能力正好可以解决这个问题,它通过标准化的Trace上下文传播协议,让每一段耗时都挂到同一个TraceID下面,形成一条真正意义上的全链路视图。

一、为什么CDN场景下的链路追踪这么难做
要理解CDN全链路追踪的难点,得先看清楚一个CDN请求到底经历了什么。用户在浏览器发起请求后,DNS会将域名解析到距离最近的边缘节点,边缘节点如果缓存未命中,就会触发回源,回源可能经过多层中间节点,最终到达源站。整个过程中,请求会跨越浏览器、CDN边缘、传输网络、源站应用这几个完全不同的运行环境,每个环境对Trace上下文的处理能力参差不齐。
标准的应用间调用追踪依赖W3C Trace Context规范,也就是通过traceparent这个Header把TraceID和SpanID传递下去。但很多CDN节点默认会剥离或忽略非标准的请求头,即使透传了,CDN内部的处理耗时也不会生成Span,这就导致链路在CDN这一段天然是断的。另外,CDN的访问日志和源站的应用日志分属两套体系,时间戳精度、字段格式都不统一,想靠日志关联几乎不可能。
所以做CDN全链路追踪,核心要解决三件事:第一,浏览器端要能发起Trace并注入上下文;第二,CDN层要么支持Header透传并在日志中记录,要么用日志采样关联的方式补齐;第三,源站应用要正确接收并继续传播上下文。下面分别展开。
二、浏览器端接入OpenTelemetry SDK
链路的起点在用户浏览器。OpenTelemetry提供了专门面向Web的SDK,可以自动采集页面加载、XHR请求、用户交互等信号。接入方式推荐通过CDN引入或者打包到构建流程中,下面的例子展示了如何在页面初始化时配置一个指向自建Collector的导出器。
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
const provider = new WebTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(
new OTLPTraceExporter({
// 使用HTTP协议上报,避免跨域CORS问题需在Collector端配置
url: 'https://collector.ipipp.com/v1/traces',
})
));
provider.register();
// 自动为所有XHR请求注入traceparent头
registerInstrumentations({
instrumentations: [
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [/.*\.ipipp\.com/],
}),
],
});
配置好之后,浏览器发出的每一个AJAX请求都会自动带上traceparentHeader,形如00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01,其中包含了全局唯一的TraceID。这里有个容易被忽略的细节:跨域请求需要CDN服务端在CORS响应中声明允许这个Header,也就是Access-Control-Allow-Headers中要包含traceparent和tracestate,否则预检请求会直接失败,反而造成新的性能问题。
对于静态资源请求(比如图片、脚本文件),浏览器SDK无法自动注入Header,这类请求可以改用服务端渲染时预埋TraceID,或者通过Server-Timing响应头把CDN侧的耗时信息带回浏览器,再由SDK采集上报。这种迂回方案虽然不如Header传播优雅,但在纯静态资源场景下非常实用。
三、CDN边缘节点与回源链路的上下文透传
CDN这一层是整条链路的关键断点。现在主流的CDN厂商,比如Cloudflare、Akamai、阿里云、腾讯云,都提供了不同程度的Trace支持。Cloudflare的Server-Timing头会返回边缘耗时和缓存命中情况,阿里云CDN支持配置自定义回源头,可以把traceparent原样透传到源站。配置上只需要在CDN控制台添加回源HTTP头规则,将客户端请求中的traceparent复制到回源请求。
如果使用的是自建CDN或者基于Nginx、Envoy的边缘网关,透传就完全可控了。以Nginx为例,通过$http_traceparent变量可以拿到客户端传来的Header,再通过proxy_set_header传递给上游,同时利用log_format把这个值记录到访问日志中,用于事后关联分析。
log_format trace '$http_traceparent $request_time $upstream_response_time '
'$upstream_cache_status $server_addr';
server {
listen 443 ssl;
server_name static.ipipp.com;
location / {
# 透传trace上下文到回源请求
proxy_set_header traceparent $http_traceparent;
proxy_set_header tracestate $http_tracestate;
# 回源到源站
proxy_pass http://origin-backend;
access_log /var/log/nginx/trace.log trace;
}
}
更进一步,如果想在边缘节点生成真正的Span而不是只做透传,可以考虑在网关层部署OpenTelemetry的采集模块。Envoy原生集成了OpenTelemetry Tracer,配置EnvoyFilter或直接在Bootstrap中指定tracing provider,就能让每个经过Envoy的请求自动产生Span并上报。Nginx生态则可以用OpenTracing兼容的模块配合OTLP exporter实现类似效果,只是运维成本略高。
还有一种情况必须考虑:CDN厂商既不支持透传自定义头,也不提供任何Trace能力。这时只能退而求其次,采用日志关联方案。在浏览器端把TraceID拼接进请求URL的查询参数(例如?__trace_id=xxx),CDN访问日志天然会记录完整URL和边缘处理耗时,源站也从查询参数中提取TraceID做关联。这种方案的缺点是污染URL且可能影响缓存命中率,需要权衡使用。
四、源站应用接收并延续Trace上下文
链路的最后一环是源站应用。以一个Java Spring Boot服务为例,接入opentelemetry-spring-boot-starter后,框架会自动从入站请求的traceparentHeader中提取上下文,并创建服务端Span。应用内部的下游调用,比如访问数据库、调用其他微服务,也都会自动挂到同一个Trace上。
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ContentController {
private final Tracer tracer;
@Autowired
public ContentController(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("origin-service");
}
@GetMapping("/api/content")
public String getContent(@RequestHeader(value = "traceparent", required = false) String traceparent) {
// starter已自动完成上下文提取,这里可手动补充业务属性
var span = tracer.spanBuilder("build-content").startSpan();
try (var scope = span.makeCurrent()) {
span.setAttribute("cdn.cached", "miss");
return renderContent();
} finally {
span.end();
}
}
}
PHP、Go、Node.js的应用接入方式类似,核心都是引入对应语言的OpenTelemetry SDK,配置OTLP导出到统一的Collector。这里强烈建议在源站前面部署一层OpenTelemetry Collector,让所有节点的Span先汇聚到Collector,再做采样、过滤和转发,这样后端存储(Jaeger、Tempo、SkyWalking均可)可以灵活切换,前端SDK的配置也不用改动。
五、采样策略与落地建议
全链路采集的数据量非常大,浏览器端每次页面加载可能产生几十个Span,CDN层的QPS又是万级起步,不做采样的话后端存储很快就会被冲垮。推荐采用parentbased_traceidratio采样器,比例控制在百分之一到百分之五之间,同时配合尾部采样:在Collector上根据Span属性做决策,比如只保留耗时超过500毫秒的慢请求、携带错误标记的请求,这样既控制了成本,又保证了问题请求的完整链路必然被记录。
落地顺序上建议分三步走:先打通源站应用的Trace,这是收益最快的部分,能看清应用内部和下游依赖的耗时;再接入浏览器端SDK,把用户视角的真实耗时纳入链路;最后攻坚CDN层,根据厂商能力选择Header透传、Server-Timing或日志关联方案。三步完成后,一条从用户点击到源站数据库查询的完整Trace就真正串起来了,下次再遇到「用户说慢但监控正常」的问题,直接按TraceID检索,每一跳的耗时一目了然。
CDN全链路追踪OpenTelemetry分布式链路追踪修改时间:2026-09-16 12:56:47