【SkyWalking】SkyWalking是如何实现跨进程传播链路数据?,全链路传递userId等自定义数据
一、简介
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
协议的整体设计:
下面详细讲解协议的Header项:
1 Standard Header项
该Header项是上下文传播必须包含的。
- Header名称:
sw8
. - Header值:由
-
分隔的8个字段组成。Header值的长度应该小于2KB。
Header值中具体包含以下8个字段:
- 采样(Sample),0 或 1,0 表示上下文存在,但是可以(也很可能)被忽略而不做采样;1 表示这个trace需要采样并发送到后端。
- 追踪ID(Trace Id),是 Base64 编码的字符串,其内容是由
.
分割的三个 long 类型值, 表示此trace的唯一标识。 - 父追踪片段ID(Parent trace segment Id),是 Base64 编码的字符串,其内容是字符串且全局唯一。
- 父跨度ID(Parent span Id),是一个从 0 开始的整数,这个跨度ID指向父追踪片段(segment)中的父跨度(span)。
- 父服务名称(Parent service),是 Base64 编码的字符串,其内容是一个长度小于或等于50个UTF-8编码的字符串。
- 父服务实例标识(Parent service instance),是 Base64 编码的字符串,其内容是一个长度小于或等于50个UTF-8编码的字符串。
- 父服务的端点(Parent endpoint),是 Base64 编码的字符串,其内容是父追踪片段(segment)中第一个入口跨度(span)的操作名,由长度小于或等于50个UTF-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值
当前值包括的字段:
- 追踪模式(Tracing Mode),空、0或1,默认为空或0。表示在这个上下文中生成的所有跨度(span)应该跳过分析。在默认情况下,这个应该在上下文中传播到服务端,除非它在跟踪过程中被更改。
- 客户端发送的时间戳:用于异步RPC,如MQ。一旦设置,消费端将计算发送和接收之间的延迟,并使用key
transmission.latency
自动在span中标记延迟。
示例值:1-1621344125000
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
,下面是该类跨进程传播的核心代码:
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()负责跨进程上下文透传。
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项相关的上下文数据,具体参考下面的代码:
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的源码:
/**
* 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
一、背景
1.由来
通过上篇文章 juejin.cn/post/728005… 分析了跨进程传播链路数据的设计原理。
本篇讲解一下,如何通过SkyWalking提供的跨进程传播协议实现全链路传递业务数据。
2.需求场景
需求场景如:
1.希望将userId、机构Id、租户id等字段能全链路传递,这样方便根据这些字段进行搜索、数据分析。
2.希望借助SkyWalking的跨进程、跨线程传递的协议,来实现全链路压测、全链路灰度的特定标记的全链路传递。
二、如何实现
自定义数据的全链路传递,推荐借助于sw8协议里Correlation Header项来实现,上文也提到过,Correlation Header项支持跨进程、跨线程传递数据。
下面是其使用方式。
1.引入apm-toolkit-trace工具
引入apm-toolkit-trace maven依赖
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.13.0</version>
</dependency>
2.修改agent配置
这一步是可选的,如果希望将自定义的参数自动记录到tag中,则修改agent/config/agent.config配置文件。如果不需要记录到tag中,则可以不用修改下面的配置,对应的配置如下;
# 注意:auto_tag_keys各个key以逗号分割,且前后一定不要有空格!否则key名就会有空格
correlation.auto_tag_keys=${SW_CORRELATION_AUTO_TAG_KEYS:xxx,xxxx}
TIP:只会自动记录到链路中TraceSegment的第一个span的tag中。
3.使用TraceContext API
3.1 TraceContext API
通过SkyWalking的TraceContext API可以设置全局透传的数据,对应的API如下:
// org.apache.skywalking.apm.toolkit.trace.TraceContext
/**
* Put the custom key/value into trace context. 将自定义的K/V放入trace上下文
* @return previous value if it exists.
*/
public static Optional<String> putCorrelation(String key, String value) {
return Optional.empty();
}
/**
* Try to get the custom value from trace context. 从trace上下文获取指定的自定义key所对应value
* @return custom data value.
*/
public static Optional<String> getCorrelation(String key) {
return Optional.empty();
}
3.2 示例
假设有三个应用,他们调用关系是gateway-->app1-->app2,调用方式是通过http访问。我们做如下改造:
1.在gateway应用,将userId设置到trace context的correlation context,代码示例:
/** 请求处理的某个环节,将userId设置到context */
public Result handle() {
....
TraceContext.putCorrelation("sw8_userId", "123321");
....
}
其中"sw8_userId"是自定义的key。
2.同时我们也希望userId记录到span的tag中,所以按照前文所述,修改agent/config/agent.config配置文件,增加配置
# 注意:auto_tag_keys各个key以逗号分割,且前后一定不要有空格!否则key名就会有空格
correlation.auto_tag_keys=${SW_CORRELATION_AUTO_TAG_KEYS:sw8_userId}
整体的调用链路和自定义数据的传递如下图所示:
4.观察效果
通过SkyWalking控制台,我们可以看到完整的调用链路,如下图:
在span的详情里,也可以看到tags中包含了sw8_userId。
5.扩展
如果希望支持以sw8_userId作为链路的搜索条件,则可以修改/oapServer/skywalking-server-xxx/config/application.yml文件里如下配置:
core:
selector: ${SW_CORE:default}
default:
# 可搜索tag里增加sw8_userId
searchableTracesTags: ${SW_SEARCHABLE_TAG_KEYS:http.method,http.status_code,rpc.status_code,db.type,db.instance,mq.queue,mq.topic,mq.broker,sw8_userId}
这样就可以在控制台上以sw8_userId做为链路搜索条件,如下图:
三、原理分析
1.疑惑
我们用到了TraceContext.putCorrelation(key, value),但是,方法里面只有return Optional.empty()
,并没有设置context数据。
那么SkyWalking Agent是如何实现设置context数据的?
// org.apache.skywalking.apm.toolkit.trace.TraceContext
/**
* Put the custom key/value into trace context. 将自定义的K/V放入trace上下文
* @return previous value if it exists.
*/
public static Optional<String> putCorrelation(String key, String value) {
return Optional.empty();
}
2.源码分析
你应该猜到了,SkyWalking Agent是基于Java agent的字节码增强技术实现各种插件,那TraceContext.putCorrelation(key, value)这块肯定是用到了字节码增强技术。
关于Java agent的内容可以看一下我之前的文章:JVM系列-Java agent超详细知识梳理
TraceContext.putCorrelation 的字节码增强对应的插桩类是apm-sniffer/apm-toolkit-activation/apm-toolkit-trace-activation/
模块下的TraceContextActivation.java,对应代码:
public static final String ENHANCE_CLASS = "org.apache.skywalking.apm.toolkit.trace.TraceContext";
public static final String ENHANCE_PUT_CORRELATION_METHOD = "putCorrelation";
public static final String INTERCEPT_PUT_CORRELATION_CLASS = "org.apache.skywalking.apm.toolkit.activation.trace.CorrelationContextPutInterceptor";
/** 增强TraceContext类 */
protected ClassMatch enhanceClass() {
return NameMatch.byName(ENHANCE_CLASS);
}
/** 拦截putCorrelation方法,对应的Interceptor是CorrelationContextPutInterceptor */
new StaticMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named(ENHANCE_PUT_CORRELATION_METHOD);
}
@Override
public String getMethodsInterceptor() {
return INTERCEPT_PUT_CORRELATION_CLASS;
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
从上面代码可知putCorrelation方法对应的方法增强类Interceptor是CorrelationContextPutInterceptor,对应源码:
public class CorrelationContextPutInterceptor implements StaticMethodsAroundInterceptor {
@Override
public void beforeMethod(Class clazz, Method method, Object[] allArguments, Class<?>[] parameterTypes, MethodInterceptResult result) {
final String key = (String) allArguments[0];
final String value = (String) allArguments[1];
// 设置K/V到trace context
final Optional<String> previous = ContextManager.getCorrelationContext().put(key, value);
result.defineReturnValue(previous);
}
}
核心代码就是ContextManager.getCorrelationContext().put(key, value)
,设置K/V到trace context。
设置到trace context后,就可以一直跨进程、跨线程传递了。
四、总结
上文讲述了如何借助apm-toolkit-trace API,实现通过sw8协议Correlation Header项全链路传递自定义数据。
并通过源码分析了Agent通过字节码增强技术,补充TraceContext.putCorrelation设置K/V到trace context的核心代码。
全链路压测、全链路灰度的特定标记的全链路传递也可以借助sw8协议实现,但本人未在工作中实际实践过,如果大家有相关经验,可以一起分享。
链接:https://juejin.cn/post/7282687884205129787
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。