链路追踪原理-SpanContext

Http:

链路追踪的核心流程确实确实可以简化为这样的流程:从第一个服务生成全局唯一的 traceId 开始,通过拦截器在服务间传递 traceId 和 spanId,最终将这些信息写入日志,实现全链路的可视化和问题追踪

可以再进一步拆解这个过程,让细节更清晰:

1. 首次请求:生成根 traceId 和 spanId

  • 当用户请求首次到达服务 1(如网关或入口服务)时,Spring Cloud Sleuth 的拦截器(如 TracingFilter)会拦截请求,判断这是 “新请求”(无 traceId)。
  • 此时会生成全局唯一的 traceId(如 a1b2c3d4)和第一个 spanId(如 s1,称为 “根 Span”),并将它们封装到 SpanContext 中,绑定到当前线程的 ThreadLocal
  • MDC 写入:Sleuth 会自动将 traceId 和 spanId 放入日志框架的 MDC(Mapped Diagnostic Context)中,因此日志中会自动带上这两个字段(需在日志格式中配置 %X{traceId} %X{spanId})。

2. 服务间调用:传递 traceId 和 spanId

  • 服务 1需要调用服务 2时(如通过 HTTP/Feign):
    • 拦截器会从当前线程的 SpanContext 中取出 traceId=a1b2c3d4 和 spanId=s1
    • 自动将这些信息写入 HTTP 请求头(如 X-B3-TraceId: a1b2c3d4X-B3-SpanId: s1),随请求一起发送到服务 2。
  • 服务 2接收请求时:
    • 拦截器从请求头中解析出 traceId=a1b2c3d4 和 parentSpanId=s1(服务 1 的 spanId)。
    • 生成服务 2 自己的 spanId=s2,并将 parentSpanId 设置为 s1,形成 “父子关系”。
    • 同样将 traceId 和 s2 写入 MDC,确保服务 2 的日志也携带相同的 traceId

3. 多级调用:链路串联

  • 服务 2调用服务 3时,重复上述过程:服务 2 的 spanId=s2 会作为 parentSpanId 传递给服务 3,服务 3 生成 spanId=s3traceId 始终保持 a1b2c3d4
  • 最终,整个链路的所有服务日志中,traceId 都是同一个,spanId 则通过 parentSpanId 形成调用关系(s1 → s2 → s3)。

4. 日志与 Zipkin 展示

  • 日志层面:通过 MDC,所有服务的日志都包含 traceId,可通过 grep "a1b2c3d4" 快速筛选出同一链路的所有日志,定位跨服务问题。
  • Zipkin 层面:各服务会将 traceIdspanId、调用耗时等信息上报到 Zipkin 服务器,Zipkin 根据 traceId 聚合所有 Span,生成可视化的调用链路图(包含每个服务的耗时、状态等)。

--------------------------------------

ZIPKIN原理:

Zipkin 作为分布式追踪系统,底层通过追踪数据的生成、传递、收集、存储和展示五个核心流程实现功能,其设计依赖分布式系统的追踪规范(如 OpenTracing)和高效的通信 / 存储机制。以下从底层原理拆解 Zipkin 的工作流程:

一、底层核心概念回顾

在深入底层前,先明确 3 个核心概念(Zipkin 基于 Google Dapper 论文设计):

  • Trace:一个分布式请求的完整链路,用全局唯一的 traceId 标识(如 a1b2c3d4)。
  • Span:链路中的一个 “操作单元”(如一次服务调用、一次数据库查询),用 spanId 标识,包含耗时、状态、标签等信息,通过 parentSpanId 关联父操作。
  • AnnotationSpan 中的时间点标记(如 cs 客户端发送、sr 服务端接收、ss 服务端响应、cr 客户端接收),用于计算耗时。

二、底层工作流程:从追踪生成到可视化

Zipkin 的底层流程可分为 5 个阶段:追踪数据生成 → 跨服务传递 → 本地收集 → 上报存储 → 查询展示

1. 阶段 1:追踪数据生成(Span 创建)

当请求进入系统时,由应用中的追踪代理(如 Brave) 生成 Trace 和 Span 数据,核心逻辑:

  • 根 Span 生成:首个接收请求的服务(如网关)会创建根 Span,生成全局唯一的 traceId 和根 spanIdparentSpanId 为 null)。
  • 子 Span 生成:后续服务调用时,基于上游传递的 traceId 和 parentSpanId,生成当前服务的 spanId,并记录 Annotation 时间戳(如 sr 记录服务端接收时间,ss 记录服务端响应时间,二者差值即为服务处理耗时)。
  • 底层实现:由 Brave(Zipkin 的 Java 实现库)通过拦截器 / 切面自动完成,无需业务代码侵入。例如:
    • HTTP 拦截器(TracingFilter):拦截请求,在请求进入时创建 Span,在响应时标记 Span 完成。
    • 数据库拦截器(TracingDataSource):拦截 SQL 执行,创建 “数据库操作 Span”,关联到当前服务的 Span
2. 阶段 2:跨服务传递(SpanContext 传播)

traceIdspanId 等核心信息(封装为 SpanContext)需跨服务传递,底层依赖协议注入

  • 传递载体:根据通信协议,将 SpanContext 注入到请求的 “元数据” 中:
    • HTTP/RPC:注入请求头(如 X-B3-TraceIdX-B3-SpanId,遵循 B3 协议)。
    • 消息队列:注入消息的 Headers(如 Kafka 的 headers 字段、RabbitMQ 的 messageProperties)。
    • 自定义协议:需手动注入到协议的扩展字段中(如 Socket 消息的额外字段)。
  • 底层实现:Brave 提供 Propagator 接口,不同协议有对应的实现(如 HttpPropagator 处理 HTTP 头,KafkaPropagator 处理 Kafka 消息),负责 SpanContext 的 “注入”(发送端)和 “提取”(接收端)。
3. 阶段 3:本地追踪数据收集

每个服务生成的 Span 数据会先在本地暂存,等待批量上报,底层通过线程安全的缓冲区实现:

  • 本地存储Span 数据生成后,先存储在内存缓冲区(如 ConcurrentLinkedQueue),避免频繁 IO 影响性能。
  • 批量处理:缓冲区达到阈值(如 100 条)或定时(如 1 秒)触发批量处理,将 Span 数据序列化为 JSON 格式。
  • 采样机制:为避免高并发下的数据量过大,Zipkin 默认采用采样策略(如 1/1000 采样率),仅上报部分 Trace。采样决策通过 Sampler 接口实现,可自定义(如对特定用户 ID 全量采样)。
4. 阶段 4:追踪数据上报与存储

本地收集的 Span 数据会上报到 Zipkin 服务器,服务器将其持久化存储,底层依赖高效的网络传输和存储引擎

  • 上报方式:应用通过 HTTP 或 Kafka 向 Zipkin 服务器发送数据:
    • HTTP 上报:简单直接,适合小规模场景(默认方式)。
    • Kafka 上报:通过消息队列异步上报,适合高并发场景(避免 Zipkin 服务器压力过大)。
  • 服务器接收:Zipkin 服务器(基于 Spring Boot 实现)提供 /api/v2/spans 接口,接收 JSON 格式的 Span 数据,解析后写入存储。
  • 存储引擎:Zipkin 支持多种存储后端(可配置):
    • 内存存储:仅用于测试,服务重启后数据丢失。
    • Elasticsearch:适合大规模、高可用场景(生产环境常用)。
    • MySQL:适合小规模、数据量不大的场景。
    • Cassandra:适合写入密集型场景。存储时,Span 数据按 traceId 索引,方便后续查询。
5. 阶段 5:查询与可视化展示

用户通过 Zipkin UI 查询追踪数据,底层依赖索引查询和链路聚合

  • 查询接口:Zipkin 服务器提供 /api/v2/trace/{traceId} 等接口,根据 traceId 从存储中查询所有关联的 Span 数据。
  • 链路聚合:查询到的 Span 数据按 parentSpanId 排序,构建出完整的调用链路(如 Span1 → Span2 → Span3)。
  • UI 展示:前端页面(基于 React)将聚合后的链路可视化,展示每个 Span 的耗时、状态、标签等信息,支持按服务名、时间范围筛选。

三、核心组件与技术栈

Zipkin 底层依赖以下关键组件和技术:

  • 客户端(应用侧)
    • Brave:Java 语言的追踪库,负责 Span 生成、SpanContext 传递、数据本地收集。
    • 拦截器 /instrumentation:对常见框架(Spring MVC、Feign、MySQL、Kafka 等)的拦截器,实现自动追踪。
  • 服务端(Zipkin 服务器)
    • 基于 Spring Boot 开发,提供 HTTP 接口接收数据。
    • 存储模块:抽象出 StorageComponent 接口,适配 Elasticsearch、MySQL 等存储。
    • 查询模块:基于存储实现 TraceQueryService,支持按 traceId、服务名等查询。
  • 通信协议
    • B3 协议:定义 traceIdspanId 在 HTTP 头中的传递格式(主流分布式追踪通用协议)。
    • JSON:Span 数据的序列化格式(简单易解析)。

四、底层设计的核心目标

Zipkin 底层设计围绕两个核心目标:

  1. 低侵入性:通过拦截器和 instrumentation 实现自动追踪,无需修改业务代码。
  2. 高性能:通过采样机制、本地缓冲、批量上报减少对应用性能的影响(通常 overhead < 1%)。

总结

Zipkin 底层通过 “追踪代理生成数据 → 协议注入传递上下文 → 本地缓冲批量上报 → 服务器存储与聚合 → UI 可视化展示” 的流程,实现分布式链路的追踪。核心依赖 Brave 库的自动拦截、B3 协议的跨服务传递、以及可扩展的存储引擎,最终为开发者提供跨服务问题排查和性能分析的能力。

-------------------------------------------

RPC协议怎么处理:

在 RPC 调用(如 Dubbo、gRPC 等)中,traceId 等追踪信息的传递方式确实与 HTTP 不同(不依赖 HTTP 头),但核心思路一致:通过 RPC 协议的 “附加元数据” 机制,将 SpanContext(包含 traceIdspanId 等)从服务 1 传递到服务 2。不同 RPC 框架的具体实现细节略有差异,但底层原理相通。

一、RPC 传递 traceId 的核心原理

RPC 框架(如 Dubbo、gRPC)通常会定义 **“调用上下文”**(用于传递非业务数据的附加信息),traceId 等追踪信息就通过这个上下文传递。具体流程如下:

  1. 服务 1(调用方)发送 RPC 请求时

    • 追踪框架(如 Sleuth + Brave)拦截 RPC 调用,从当前线程的 SpanContext 中提取 traceIdspanIdparentSpanId 等信息。
    • 将这些信息写入 RPC 框架的 “附加元数据”(如 Dubbo 的 RpcContext、gRPC 的 Metadata)中,随业务请求一起发送。
  2. 服务 2(接收方)处理 RPC 请求时

    • 追踪框架拦截 RPC 请求,从 “附加元数据” 中解析出 traceIdspanId 等信息,重建 SpanContext
    • 基于重建的 SpanContext 生成服务 2 的子 SpanparentSpanId 设为服务 1 的 spanId),确保链路关联。

二、主流 RPC 框架的具体实现(以 Dubbo 和 gRPC 为例)

1. Dubbo 框架中传递 traceId

Dubbo 是 Java 生态常用的 RPC 框架,通过 RpcContext 传递附加信息,追踪框架(如 Sleuth)会自动集成 Dubbo 的拦截器实现 traceId 传递。

(1)依赖集成(Spring Cloud 环境)

需引入 Sleuth 对 Dubbo 的集成依赖,自动拦截 Dubbo 调用:

xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-dubbo</artifactId>
</dependency>
(2)底层传递机制
  • 调用方(服务 1):Dubbo 的 Invocation 拦截器(TracingFilter)会在发送请求前,将 SpanContext 信息(traceIdspanId 等)放入 RpcContext 的 attachments(附加参数)中:

    java

    运行

    // 伪代码:Dubbo 调用方拦截器
    public class DubboTracingFilter implements Filter {
        @Override
        public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
            // 从当前线程获取 SpanContext
            SpanContext context = tracer.currentSpan().context();
            // 将 traceId、spanId 放入 RpcContext 附加参数
            RpcContext.getContext().setAttachment("X-B3-TraceId", context.traceIdString());
            RpcContext.getContext().setAttachment("X-B3-SpanId", context.spanIdString());
            // 继续执行 RPC 调用
            return invoker.invoke(invocation);
        }
    }
    
  • 接收方(服务 2):接收方的拦截器从 RpcContext 的 attachments 中提取 traceId 等信息,重建 SpanContext 并生成子 Span

    java

    运行

    // 伪代码:Dubbo 接收方拦截器
    public class DubboServerTracingFilter implements Filter {
        @Override
        public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
            // 从 RpcContext 附加参数中提取 traceId、spanId
            String traceId = RpcContext.getContext().getAttachment("X-B3-TraceId");
            String spanId = RpcContext.getContext().getAttachment("X-B3-SpanId");
            // 重建 SpanContext
            SpanContext context = TraceContext.newBuilder()
                .traceId(TraceId.fromString(traceId))
                .spanId(SpanId.fromString(spanId))
                .build();
            // 生成子 Span 并绑定到当前线程
            Span childSpan = tracer.newChild(context).start();
            try (Tracer.SpanInScope ws = tracer.withSpanInScope(childSpan)) {
                return invoker.invoke(invocation); // 处理业务逻辑
            } finally {
                childSpan.finish();
            }
        }
    }
    
  • 效果:服务 2 的日志和追踪数据中,traceId 与服务 1 一致,spanId 关联到服务 1 的 spanId,实现链路串联。

2. gRPC 框架中传递 traceId

gRPC 是跨语言的 RPC 框架,通过 Metadata(元数据)传递附加信息,追踪框架(如 Brave)提供专门的拦截器支持。

(1)依赖集成

引入 Brave 对 gRPC 的集成依赖:

xml

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-grpc</artifactId>
</dependency>
(2)底层传递机制
  • 调用方(服务 1):gRPC 的客户端拦截器(TracingClientInterceptor)会将 SpanContext 信息写入 Metadata(gRPC 的元数据载体):

    java

    运行

    // 伪代码:gRPC 客户端拦截器
    public class TracingClientInterceptor implements ClientInterceptor {
        @Override
        public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
            // 获取当前 SpanContext
            SpanContext context = tracer.currentSpan().context();
            // 创建 gRPC Metadata,写入 traceId、spanId
            Metadata metadata = new Metadata();
            metadata.put(Metadata.Key.of("X-B3-TraceId", Metadata.ASCII_STRING_MARSHALLER), 
                        context.traceIdString());
            metadata.put(Metadata.Key.of("X-B3-SpanId", Metadata.ASCII_STRING_MARSHALLER), 
                        context.spanIdString());
            // 发送带 Metadata 的请求
            return next.newCall(method, callOptions.withOption(MetadataConstants.HEADER_METADATA_KEY, metadata));
        }
    }
    
  • 接收方(服务 2):服务端拦截器从 Metadata 中提取信息,重建 SpanContext 并生成子 Span

    java

    运行

    // 伪代码:gRPC 服务端拦截器
    public class TracingServerInterceptor implements ServerInterceptor {
        @Override
        public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata metadata, ServerCallHandler<ReqT, RespT> next) {
            // 从 Metadata 中提取 traceId、spanId
            String traceId = metadata.get(Metadata.Key.of("X-B3-TraceId", Metadata.ASCII_STRING_MARSHALLER));
            String spanId = metadata.get(Metadata.Key.of("X-B3-SpanId", Metadata.ASCII_STRING_MARSHALLER));
            // 重建 SpanContext 并生成子 Span
            SpanContext context = TraceContext.newBuilder()
                .traceId(TraceId.fromString(traceId))
                .spanId(SpanId.fromString(spanId))
                .build();
            Span childSpan = tracer.newChild(context).start();
            try (Tracer.SpanInScope ws = tracer.withSpanInScope(childSpan)) {
                return next.startCall(call, metadata); // 处理业务
            } finally {
                childSpan.finish();
            }
        }
    }
    

三、RPC 与 HTTP 传递方式的异同

维度HTTP 传递RPC 传递(如 Dubbo/gRPC)
载体HTTP 头(Header)RPC 附加元数据(Dubbo 的 RpcContext、gRPC 的 Metadata
拦截点HTTP 过滤器(如 TracingFilterRPC 框架的拦截器(如 Dubbo Filter、gRPC Interceptor
标准化遵循 B3 协议(X-B3-* 头)通常也兼容 B3 协议(用相同的 key 如 X-B3-TraceId
侵入性无(框架自动处理)无(集成追踪框架后自动处理)

四、总结

RPC 调用中传递 traceId 的核心是利用 RPC 框架自身的 “附加元数据” 机制(如 Dubbo 的 RpcContext、gRPC 的 Metadata),通过追踪框架的拦截器自动注入和提取 SpanContext 信息。虽然载体与 HTTP 不同,但最终目的一致:确保 traceId 在跨服务调用中不中断,实现全链路追踪。

对于开发者而言,只需集成对应 RPC 框架的追踪插件(如 Sleuth-Dubbo、Brave-gRPC),即可自动完成 traceId 的传递,无需手动处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值