【SkyWalking】SkyWalking是如何实现跨进程传播链路数据?

一、简介

1 为什么写这篇文章

写这篇文章是为了让自己和大家梳理这些内容:

1.SkyWalking的链路串联依赖跨进程数据传播,他的跨进程传播协议是怎样的?

2.如果我想借助SkyWalking的跨进程传播协议实现类似全链路压测、全链路业务数据(如全局userId等)传递,该如何实现?

2 跨进程传播协议-简介

SkyWalking 跨进程传播协议是用于上下文的传播,之前经历过sw3协议、sw6协议,本文介绍是当前(2023年)最新的sw8协议。

该协议适用于不同语言、系统的探针之间传递上下文。

二、协议

Header项分为三类:

  • Standard Header项,Header名称:sw8
  • Extension Header项,Header名称:sw8-x
  • Correlation Header项,Header名称:sw8-correlation

1 Standard Header项

该Header项是上下文传播必须包含的。

  • Header名称:sw8.
  • Header值:由-分隔的8个字段组成。Header值的长度应该小于2KB。

Header值中具体包含以下8个字段:

  1. 采样(Sample),0 或 1,0 表示上下文存在,但是可以(也很可能)被忽略而不做采样;1 表示这个trace需要采样并发送到后端。
  2. 追踪ID(Trace Id),是 Base64 编码的字符串,其内容是由 . 分割的三个 long 类型值, 表示此trace的唯一标识。
  3. 父追踪片段ID(Parent trace segment Id),是 Base64 编码的字符串,其内容是字符串且全局唯一。
  4. 父跨度ID(Parent span Id),是一个从 0 开始的整数,这个跨度ID指向父追踪片段(segment)中的父跨度(span)。
  5. 父服务名称(Parent service),是 Base64 编码的字符串,其内容是一个长度小于或等于50个UTF-8编码的字符串。
  6. 父服务实例标识(Parent service instance),是 Base64 编码的字符串,其内容是一个长度小于或等于50个UTF-8编码的字符串。
  7. 父服务的端点(Parent endpoint),是 Base64 编码的字符串,其内容是父追踪片段(segment)中第一个入口跨度(span)的操作名,由长度小于或等于50个UTF-8编码的字符组成。
  8. 本请求的目标地址(Peer),是 Base64 编码的字符串,其内容是客户端用于访问目标服务的网络地址(不一定是 IP + 端口)。

示例值: 1-TRACEID-SEGMENTID-3-PARENT_SERVICE-PARENT_INSTANCE-PARENT_ENDPOINT-IPPORT

2 Extension Header项

该Header项是可选的。扩展Header项是为高级特性设计的,它提供了部署在上游和下游服务中的探针之间的交互功能。

  • Header名称:sw8-x

  • Header值:由-分割,字段可扩展。

扩展Header值

当前值包括的字段:

  1. 追踪模式(Tracing Mode),空、0或1,默认为空或0。表示在这个上下文中生成的所有跨度(span)应该跳过分析。在默认情况下,这个应该在上下文中传播到服务端,除非它在跟踪过程中被更改。

3 Correlation Header项

该Header项是是可选的。并非所有语言的探针都支持,已知的是Java的探针是支持该协议。

该Header项用于跨进程传递用户自定义数据,例如userId、orgId。

这个协议跟OpenTracing 的 Baggage很类似,但是Correlation Header项相比,在默认设置下会更有更严格的限制,例如,只能存放3个字段,且有字段长度限制,这个是为了安全、性能等考虑。

数据格式:

  • Header名称:sw8-correlation

  • Header值:由,分割一对对key、value,每对key、value逗号分割,key、value的由Base64编码。

示例值:a2V5MQ==:dmFsdWUx,a2V5LTI=:dmFsdWUy

三、跨进程传播协议的源码分析

1 OpenTracing规范

SkyWalking是基于OpenTracing标准的追踪系统,参考吴晟老师翻译的OpenTracing规范的文章opentracing之Inject和Extract,OpenTracing定义了跨进程传播的几个要素:

  • SpanContext :SpanContext代表跨越进程边界,传递到下级span的状态。在SkyWalking中的实现类是org.apache.skywalking.apm.agent.core.context.TracingContext
  • Carrier:传递跨进程数据的搬运工,负责将追踪状态从一个进程"carries"(携带,传递)到另一个进程
  • Inject 和 Extract :SpanContexts可以通过Inject(注入)操作向Carrier 增加,或者通过Extract(提取)Carrier中获取,跨进程通讯数据(例如:HTTP头)。通过这种方式,SpanContexts可以跨越进程边界,并提供足够的信息来建立跨进程的span间关系(因此可以实现跨进程连续追踪)

2 通过dubbo插件分析跨进程数据传播

我们以SkyWalking java agent的dubbo-2.7.x-plugin插件为例,其中跨进程传播数据的核心代码在org.apache.skywalking.apm.plugin.asf.dubbo.DubboInterceptor,下面是该类跨进程传播的核心代码:

java 复制代码
public class DubboInterceptor implements InstanceMethodsAroundInterceptor {

    /**
     * Consumer: The serialized trace context data will
     * inject to the {@link RpcContext#attachments} for transport to provider side.
     * <p>
     * Provider: The serialized trace context data will extract from
     * {@link RpcContext#attachments}. current trace segment will ref if the serialization context data is not null.
     */
    @Override
    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
                             MethodInterceptResult result) throws Throwable {
        ......
        if (isConsumer) { // 1、consumer端
            // ContextCarrier
            final ContextCarrier contextCarrier = new ContextCarrier();
            // 1.1 createExitSpan()内部会调用TracerContext.inject(carrier),将TracerContext中的context数据inject(注入)到ContextCarrier的context中
            span = ContextManager.createExitSpan(generateOperationName(requestURL, invocation), contextCarrier, host + ":" + port);

            CarrierItem next = contextCarrier.items();
            // 1.2 遍历ContextCarrier,从ContextCarrier的context获取数据,注入到dubbo的attachment,从consumer端传递到provider端
            while (next.hasNext()) {
                next = next.next();
                rpcContext.setAttachment(next.getHeadKey(), next.getHeadValue());
                if (invocation.getAttachments().containsKey(next.getHeadKey())) {
                    invocation.getAttachments().remove(next.getHeadKey());
                }
            }
        } else { // 2 provider端
            // 2.1 从consumer端传递到provider端的attachment中获取跨进程协议数据,然后设置到context
            ContextCarrier contextCarrier = new ContextCarrier();
            CarrierItem next = contextCarrier.items();
            while (next.hasNext()) {
                next = next.next();
                next.setHeadValue(rpcContext.getAttachment(next.getHeadKey()));
            }
            // 2.2 createEntrySpan()内部会调用TracerContext.extract(carrier),将ContextCarrier的context数据extract(提取)到将TracerContext中的context中
            span = ContextManager.createEntrySpan(generateOperationName(requestURL, invocation), contextCarrier);
            span.setPeer(rpcContext.getRemoteAddressString());
        }
    }
}

从上面的源码可以看出在服务调用方和被调用方,都会用到ContextCarrier,他是临时搬运工,负责两个进程的TracerContext数据的传递。

下面分析ContextCarrier等类的核心源码。

3 分析跨进程传播协议的核心源码

TracingContext

org.apache.skywalking.apm.agent.core.context.TracingContext是OpenTracing的SpanContext的一种实现,里面包含了span的上下文,包含在segment、correlationContext、extensionContext,而inject()、extract()负责跨进程上下文透传。

java 复制代码
public class TracingContext implements AbstractTracerContext {

     /**
     * The final {@link TraceSegment}, which includes all finished spans.
     */
    private TraceSegment segment;
  
    @Getter(AccessLevel.PACKAGE)
    private final CorrelationContext correlationContext;
    @Getter(AccessLevel.PACKAGE)
    private final ExtensionContext extensionContext;

     /**
     * Prepare for the cross-process propagation. How to initialize the carrier, depends on the implementation.
     *
     * @param carrier to carry the context for crossing process.
     */
    void inject(ContextCarrier carrier);

    /**
     * Build the reference between this segment and a cross-process segment. How to build, depends on the
     * implementation.
     *
     * @param carrier carried the context from a cross-process segment.
     */
    void extract(ContextCarrier carrier);
}

ContextCarrier

ContextCarrier作为传递跨进程数据的搬运工,负责将追踪状态从一个进程"carries"(携带,传递)到另一个进程,其中包含了sw8协议里的Standard Header项、Extension Header项、Correlation Header项相关的上下文数据,具体参考下面的代码:

java 复制代码
public class ContextCarrier implements Serializable {
    /**
     * extensionContext包含了在某些特定场景中用于增强分析的可选上下文,对应sw8的Extension Header项
     */
    private ExtensionContext extensionContext = new ExtensionContext();
    /**
     * 用户的自定义上下文容器。此上下文与主追踪上下文一同传播。对应sw8的Correlation Header项
     */
    private CorrelationContext correlationContext = new CorrelationContext();

    /**
     * @return 存在于当前tracing上下文中的item清单
     */
    public CarrierItem items() {
        SW8ExtensionCarrierItem sw8ExtensionCarrierItem = new SW8ExtensionCarrierItem(extensionContext, null);
        SW8CorrelationCarrierItem sw8CorrelationCarrierItem = new SW8CorrelationCarrierItem(
            correlationContext, sw8ExtensionCarrierItem);
        SW8CarrierItem sw8CarrierItem = new SW8CarrierItem(this, sw8CorrelationCarrierItem);
        return new CarrierItemHead(sw8CarrierItem);
    }

    /**
     * Extract the extension context to tracing context
     */
    void extractExtensionTo(TracingContext tracingContext) {
        tracingContext.getExtensionContext().extract(this);
        // The extension context could have field not to propagate further, so, must use the this.* to process.
        this.extensionContext.handle(tracingContext.activeSpan());
    }

    /**
     * Extract the correlation context to tracing context
     */
    void extractCorrelationTo(TracingContext tracingContext) {
        tracingContext.getCorrelationContext().extract(this);
        // The correlation context could have field not to propagate further, so, must use the this.* to process.
        this.correlationContext.handle(tracingContext.activeSpan());
    }

    /**
     * 序列化sw8的Standard Header项,使用 '-' 分割各个字段
     * Serialize this {@link ContextCarrier} to a {@link String}, with '|' split.
     * @return the serialization string.
     */
    String serialize(HeaderVersion version) {
        if (this.isValid(version)) {
            return StringUtil.join(
                '-',
                "1",
                Base64.encode(this.getTraceId()),
                Base64.encode(this.getTraceSegmentId()),
                this.getSpanId() + "",
                Base64.encode(this.getParentService()),
                Base64.encode(this.getParentServiceInstance()),
                Base64.encode(this.getParentEndpoint()),
                Base64.encode(this.getAddressUsedAtClient())
            );
        }
        return "";
    }

    /**
     * 反序列化sw8的Standard Header项
     * Initialize fields with the given text.
     * @param text carries {@link #traceSegmentId} and {@link #spanId}, with '|' split.
     */
    ContextCarrier deserialize(String text, HeaderVersion version) {
        if (text == null) {
            return this;
        }
        if (HeaderVersion.v3.equals(version)) {
            String[] parts = text.split("-", 8);
            if (parts.length == 8) {
                try {
                    // parts[0] is sample flag, always trace if header exists.
                    this.traceId = Base64.decode2UTFString(parts[1]);
                    this.traceSegmentId = Base64.decode2UTFString(parts[2]);
                    this.spanId = Integer.parseInt(parts[3]);
                    this.parentService = Base64.decode2UTFString(parts[4]);
                    this.parentServiceInstance = Base64.decode2UTFString(parts[5]);
                    this.parentEndpoint = Base64.decode2UTFString(parts[6]);
                    this.addressUsedAtClient = Base64.decode2UTFString(parts[7]);
                } catch (IllegalArgumentException ignored) {

                }
            }
        }
        return this;
    }
}

CorrelationContext

ContextCarrier里包含里sw8的Correlation Header项存放于CorrelationContext,这个类非常有用,适合我们去在全链路跨进程传递自定义的数据。

sw8协议里的Standard Header项、Extension Header项是比较固定的协议格式,我们可以扩展这些协议,例如Standard Header项,当前固定是8位的,对应8个字段,我们可以扩展为9位,第九位可以定义为userId。但是如果要这样改造,就得修改ContextCarrier类序列化、反序列的逻辑,要重新发布agent,并考虑好新旧版本兼容性问题、以及不同语言的agent是否兼容。

而sw8的Correlation Header项使用起来就非常方便。先看下对应实现了CorrelationContext的源码:

java 复制代码
/**
 * Correlation context, use to propagation user custom data.
 * Correlation上下文,用于传播用户自定义数据
 */
public class CorrelationContext {

    private final Map<String, String> data;

    /**
     * Add or override the context. 添加或覆盖上下文数据
     *
     * @param key   to add or locate the existing context
     * @param value as new value
     * @return old one if exist.
     */
    public Optional<String> put(String key, String value) {
        // 可以存放于span的tag中
        if (AUTO_TAG_KEYS.contains(key) && ContextManager.isActive()) {
            ContextManager.activeSpan().tag(new StringTag(key), value);
        }
        // setting
        data.put(key, value);
        return Optional.empty();
    }

    /**
     * @param key to find the context 获取上下文数据
     * @return value if exist.
     */
    public Optional<String> get(String key) {
        return Optional.ofNullable(data.get(key));
    }

    /**
     * Serialize this {@link CorrelationContext} to a {@link String} 序列化
     *
     * @return the serialization string.
     */
    String serialize() {
        if (data.isEmpty()) {
            return "";
        }

        return data.entrySet().stream()
                   .map(entry -> Base64.encode(entry.getKey()) + ":" + Base64.encode(entry.getValue()))
                   .collect(Collectors.joining(","));
    }

    /**
     * Deserialize data from {@link String} 反序列化
     */
    void deserialize(String value) {
        if (StringUtil.isEmpty(value)) {
            return;
        }

        for (String perData : value.split(",")) {
            // Only data with limited count of elements can be added
            if (data.size() >= Config.Correlation.ELEMENT_MAX_NUMBER) {
                break;
            }
            final String[] parts = perData.split(":");
            if (parts.length != 2) {
                continue;
            }
            data.put(Base64.decode2UTFString(parts[0]), Base64.decode2UTFString(parts[1]));
        }
    }

    /**
     * Prepare for the cross-process propagation. Inject the {@link #data} into {@link
     * ContextCarrier#getCorrelationContext()}
     */
    void inject(ContextCarrier carrier) {
        carrier.getCorrelationContext().data.putAll(this.data);
    }

    /**
     * Extra the {@link ContextCarrier#getCorrelationContext()} into this context.
     */
    void extract(ContextCarrier carrier) {
        ......
    }

    /**
     * Clone the context data, work for capture to cross-thread. 克隆数据,用于跨线程传递
     */
    @Override
    public CorrelationContext clone() {
        final CorrelationContext context = new CorrelationContext();
        context.data.putAll(this.data);
        return context;
    }

    /**
     * Continue the correlation context in another thread.传递到另外的线程
     *
     * @param snapshot holds the context.
     */
    void continued(ContextSnapshot snapshot) {
        this.data.putAll(snapshot.getCorrelationContext().data);
    }
}

通过源码可知,CorrelationContext通过Map<String, String>来存放数据,CorrelationContext数据支持跨线程、跨进程透传。

四、小结

分析Dubbo插件的跨进程核心代码,了解了跨进程传播协议的核心实现逻辑。

其实在其他分布式追踪系统(如Zipkin、Jager)、全链路灰度系统等涉及到跨进程数据传播的系统中,也是使用了类似于上面SkyWalking协议的思路。

下篇文章再讲解一下如何扩展和使用跨进程传播协议,来为自己的系统全局透传业务数据、甚至可以借此协议实现全链路灰度、全链路压测的功能。

参考

SkyWalking Cross Process Propagation Headers Protocol

SkyWalking Cross Process Correlation Headers Protocol

详解 Apache SkyWalking 的跨进程传播协议

相关推荐
蓝色天空的银码星1 分钟前
Spring循环依赖源码调试详解,用两级缓存代替三级缓存
java·spring·缓存
江湖十年4 分钟前
Go 1.25 终于迎来了容器感知 GOMAXPROCS
后端·面试·go
Cyclic10015 分钟前
IOS购买订阅通知信息解析说明Java
java·开发语言·ios
云布道师19 分钟前
AI时代下阿里云基础设施的稳定性架构揭秘
人工智能·阿里云·架构
这里有鱼汤31 分钟前
别傻了,这些量化策略AI 10 秒就能帮你写好
后端·python
坐观垂钓者32 分钟前
使用EasyExcel 导出复杂的合并单元格
java·excel
Victor3562 小时前
Redis(16)Redis的有序集合(Sorted Set)类型有哪些常用命令?
后端
Victor3562 小时前
Redis(17)如何在Redis中设置键的过期时间?
后端
22jimmy3 小时前
JavaWeb(二)CSS
java·开发语言·前端·css·入门·基础
vvilkim5 小时前
Java主流框架全解析:从企业级开发到云原生
java·运维·云原生