一、背景
在上一篇《通过编码方式构建SkyWalking 的Trace-上篇》中介绍了为什么需要通过编码方式构建SkyWalking Trace的背景,并梳理了编程方式需要提供的能力合集。每一步都走的不顺利,昨天只摸索出了EntrySpan的构建方法,和部分的ExitSpan能力,今天时间有限,加上也依然有点曲折,所以今天就变成了中篇,好了基本情况同步完毕,开始正题。
二、所需能力梳理
满足 IM 埋点的功能,需要通过显示的编码来实现以下功能:
- EntrySpan
- 如何创建
EntrySpan
,指定名称 - 如何通过 ref 关联上游服务,即
Extract
(Build the reference between this segment and a cross-process segment) - 如何打 Tag
- 如何写 Log
- 如何关闭 span
- ExitSpan
- 如何创建 ExitSpan
- 如何指定 Peer
- 如何打 Tag
- 如何写 Log
- 如何关闭 span
- 如何构建 carrier 信息,传递给下游服务,由下游通过
Inject
carrier 信息后,Segment内部就完成了ref的构建
- 跨线程传递 Trace 信息
- 离开线程时:如何捕获当前线程的 Trace 上下文
- 进入线程后:如何将其他线程的 Trace 上下文注入到当前线程
先给结论:今天精力有限已调研好的能力只有第 2 部分,参加日更活动,下班后趁着热乎赶紧将今天学习的内容整理出来;为了满足一些同学能够快速使用这个能力的需求,下边先给结论,再讲过程和原理。对于着急使用的同学可快速得到帮助;本篇介绍ExitSpan的创建,以及跨进程ExitSpan和EntrySpan之间ref如何关联。第3部分放到下一篇介绍。
三、目标
先了解一下本篇模拟的场景和构建的trace
效果,否则示例代码看起来很懵。
3.1 目标
目标是是构建这样两个Segment
- 每个
Segment
中有一个EntrySpan
,一个ExitSpan
- 第一个
Segment
的ExitSpan-1
对应第二个Segment
的EntrySpan-2
四、编码方式构建
4.1 添加依赖
依赖的版本请根据自家的情况填写
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-opentracing</artifactId> <version>xxx</version> </dependency> 复制代码
4.2 一个有效的示例
import io.opentracing.ActiveSpan; import io.opentracing.SpanContext; import io.opentracing.Tracer; import io.opentracing.propagation.Format; import io.opentracing.propagation.TextMap; import io.opentracing.propagation.TextMapExtractAdapter; import io.opentracing.tag.Tags; import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer; import java.util.HashMap; import java.util.Map; public class TracerTest { public static void main(String[] args) throws InterruptedException { TextMapInjectAdapterCustom textMap = makeEntryTrace(); String sw8 = textMap.getSW8(); makeEntryTrace2(textMap, sw8); Thread.sleep(40000); } private static TextMapInjectAdapterCustom makeExitTrace(Tracer tracer, String operationName) { //构建并激活ExitSpan Tracer.SpanBuilder spanBuilder = tracer.buildSpan(operationName); spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);//设置为Exit Span spanBuilder.withTag(Tags.PEER_HOST_IPV4.getKey(), "127.0.0.1");//必须设置peer,否则不是Exit Span ActiveSpan activeSpan = spanBuilder.startActive(); //inject carrier,下游的EntrySpan要用这个carrier 构建 ref SpanContext exitSpanContext = activeSpan.context(); TextMapInjectAdapterCustom headerCarrier = new TextMapInjectAdapterCustom(new HashMap()); tracer.inject(exitSpanContext, Format.Builtin.TEXT_MAP, headerCarrier); //关闭ExitSpan activeSpan.close(); return headerCarrier; } private static TextMapInjectAdapterCustom makeEntryTrace() { Tracer tracer = new SkywalkingTracer(); //1. 构建EntrySpan ActiveSpan activeSpan = tracer.buildSpan("/ReceiveMessage") .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)// 注意必须设置为entrySpan,才算是EntrySpan .startActive(); //2. 构建 Carrier 并 通过 extract 注入到 当前Segment 的 ref //注意 注意 注意 这个代码必须要 构建EntrySpan之后。 String sw8 = "1-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMTE2-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMDAw-1-c2t5d2Fsa2luZy1kZW1v-MTI3LjAuMC4x-L2R1YmJv-MTI3LjAuMC4xOjIwODgw"; Map map = new HashMap(); map.put("sw8", sw8);//先只构建一个sw8,其他两个暂不处理 TextMap headerCarrier = new TextMapExtractAdapter(map); tracer.extract(Format.Builtin.TEXT_MAP, headerCarrier); //设置标签 activeSpan.setTag("a", "a"); //当前EntrySpan中写日志 activeSpan.log("log 1"); //给当前EntrySpan指定名称,有些情况需要这种在运行到一定阶段后,才能从某个变量中获得值,用于构建span的名称 activeSpan.setOperationName("/ReceiveMessage");//重新指定名称 // 创建ExitSpan,并返回ExitSpan中的Carrier,给下游的EntrySpan使用 TextMapInjectAdapterCustom textMap = makeExitTrace(tracer, "/sendMessage"); //关闭span,因为只有一个EntrySpan,则会关闭整个Segment,上报给OAP activeSpan.deactivate(); return textMap; } private static void makeEntryTrace2(TextMapInjectAdapterCustom headerCarrier, String sw8) { Tracer tracer = new SkywalkingTracer(); //1. 构建EntrySpan ActiveSpan activeSpan = tracer.buildSpan("/ReceiveMessage2") .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)// 注意必须设置为entrySpan,才算是EntrySpan .startActive(); //2.1 构建 Carrier 并 通过 extract 注入到 当前Segment 的 ref // 关键在于这里,我扩展了 toTextMapExtractAdapterCustom()方法 TextMapExtractAdapterCustom textMapExtractAdapterCustom = headerCarrier.toTextMapExtractAdapterCustom(); tracer.extract(Format.Builtin.TEXT_MAP, textMapExtractAdapterCustom); //2.2 使用原生的sw8 K-V来构建,在 makeEntryTrace()中有示例,此处不在重复 //创建ExitSpan,指定名称为“/sendMessage2" makeExitTrace(tracer, "/sendMessage2"); //关闭span,因为只有一个EntrySpan,则会关闭整个Segment,上报给OAP activeSpan.deactivate(); } } 复制代码
自定义了一个TextMapInjectAdapterCustom
import io.opentracing.propagation.TextMap; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public final class TextMapInjectAdapterCustom implements TextMap { private final Map<String, String> map; public TextMapInjectAdapterCustom(final Map<String, String> map) { this.map = map; } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException("TextMapInjectAdapter should only be used with Tracer.inject()"); } @Override public void put(String key, String value) { this.map.put(key, value); } public TextMapExtractAdapterCustom toTextMapExtractAdapterCustom() { Map<String, String> extractMap = new HashMap<>(); extractMap.putAll(this.map); TextMapExtractAdapterCustom textMapExtractAdapterCustom = new TextMapExtractAdapterCustom(extractMap); return textMapExtractAdapterCustom; } public String getSW8() { return map.get("sw8"); } } 复制代码
4.3 示例效果
如果就是为了找个示例使用,那么看到这里就可以了,效果图你看出问题没(因为第一个Segment没有等第二个Segment就直接关闭了)?后边内容是介绍为什么会有上边的示例代码,笔者在探索验证过程中遇到了哪些问题。
5. 补充Segment的关联设计
1) 一个问题
一个Trace
中所有Segment
的 TID 是相同的,即通过TID把相关的Segment
串联起来,请想象一下,通过TID把相关Segment
罗列出来之后,这些Segment
之间的先后顺序是怎样,如何绘制请求在Segment
的Span
之间游走的轨迹呢?
2) ref 的关联设计
- 下游服务
Segment
中的EntrySpan
总是与上游服务Segment
中的某个ExitSpan
关联 - 下游服务
Segment
中的处理的请求源自上游Segment
,如EntrySpan-2
对应于ExitSpan-2
- 对下游服务
Segment
来说ref关系满足请求的一入一出原则;对上游服务Segment
来说满足一出多入(0..n)出原则 - 新
Segment
内EntrySpan
的parentSpanId
是上游Segment
对应ExitSpan
的spanId
,即EntrySpan-2
的parentSpanId
是ExitSpan-2
的spanId
,此处很重要。
六、小心别踩坑
6.1 EntrySpan
的 extract
无法使用 ExitSpan
所inject
的 carrier 信息.
1)错误示范
//---ExitSpan中-------- //在创建ExitSpan时,inject到 TextMapInjectAdapter 类型的headerCarrier示例, TextMapInjectAdapter headerCarrier = new TextMapInjectAdapter(new HashMap()); tracer.inject(exitSpanContext, Format.Builtin.TEXT_MAP, headerCarrier); ... //---EntrySpan中-------- //把这个TextMapInjectAdapter 类型的headerCarrier示例,给EntrySpan extract使用 tracer.extract(Format.Builtin.TEXT_MAP, headerCarrier); 复制代码
2)错误提示
Exception in thread "main" java.lang.UnsupportedOperationException: TextMapInjectAdapter should only be used with Tracer.inject() at io.opentracing.propagation.TextMapInjectAdapter.iterator(TextMapInjectAdapter.java:39) at TracerTest.makeEntryTrace2(TracerTest.java:145) at TracerTest.main(TracerTest.java:26) 复制代码
3)原因梳理 TextMapInjectAdapter#iterator
方法不让用,看下边代码
public final class TextMapInjectAdapter implements TextMap { private final Map<String,String> map; public TextMapInjectAdapterCustom(final Map<String,String> map) { this.map = map; } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException("TextMapInjectAdapter should only be used with Tracer.inject()"); } } 复制代码
这个封装中iterator
不能用,就无法从map中读出来carrier信息,读不出来那EntrySpan
中就拿不到ExitSpan
的trace
上下文。
4)自己改造 自定一个TextMap
的实现类,扩展读取的方法
toTextMapExtractAdapterCustom
:直接转换并返回一个EntrySpan可以使用的对象getSW8
:返回核心的sw8的值,程序中拿到这个值之后,自己可以在线程之间传递。
public final class TextMapInjectAdapterCustom implements TextMap { private final Map<String, String> map; public TextMapInjectAdapterCustom(final Map<String, String> map) { this.map = map; } @Override public Iterator<Map.Entry<String, String>> iterator() { throw new UnsupportedOperationException("TextMapInjectAdapter should only be used with Tracer.inject()"); } @Override public void put(String key, String value) { this.map.put(key, value); } public TextMapExtractAdapterCustom toTextMapExtractAdapterCustom() { Map<String, String> extractMap = new HashMap<>(); extractMap.putAll(this.map); TextMapExtractAdapterCustom textMapExtractAdapterCustom = new TextMapExtractAdapterCustom(extractMap); return textMapExtractAdapterCustom; } public String getSW8() { return map.get("sw8"); } } 复制代码
6.2 创建的Span
不是ExitSpan
类型
1)错误示范
//构建并激活ExitSpan Tracer.SpanBuilder spanBuilder = tracer.buildSpan(operationName); spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);//设置为Exit Span ActiveSpan activeSpan = spanBuilder.startActive(); 复制代码
2)错误原因
org.apache.skywalking.apm.toolkit.activation.opentracing.span.ConstructorWithSpanBuilderInterceptor#onConstruct
中判断peer的情况,peer 为空的话,就按照LocalSpan类型创建
// peer 为空的话,就按照LocalSpan类型创建 else if (spanBuilder.isExit() && (!StringUtil.isEmpty(spanBuilder.getPeer()))) { span = ContextManager.createExitSpan(spanBuilder.getOperationName(), buildRemotePeer(spanBuilder)); } else { span = ContextManager.createLocalSpan(spanBuilder.getOperationName()); } 复制代码
3)指定peer即可
//构建并激活ExitSpan Tracer.SpanBuilder spanBuilder = tracer.buildSpan(operationName); spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);//设置为Exit Span spanBuilder.withTag(Tags.PEER_HOST_IPV4.getKey(), "127.0.0.1");//必须设置peer,否则不是Exit Span ActiveSpan activeSpan = spanBuilder.startActive(); 复制代码
其他问题还有一小些,这里就不叨扰大家了。
七、总结
SkyWalking 主打非侵入式的 Agent 探针能力,从网络中没有顺利的找到自主编程式构建 Trace 的资料,还不确定这种方法是不被推荐、不常用或宣传不到位的哪种原因。当然 Java Agent 方式也是笔者很推崇的,因之前经历过基于 Cat 这种侵入式的埋点,当 Agent 能力需要升级时,要推动业务方配合升级是很麻烦的。但本次遇到的服务无通用性且自身有常规迭代的场景中,似乎用户自主通过编程 API 构建 Trace 信息,倒显得很合适。
本篇的内容看似简单,实际需要对 SkyWalking Agent 的工作机制、源码以及调试技巧等较为熟悉,否则遇到这些问题会无从下手,后边笔者会陆续基于 SkyWalking 整理出更多跟监控相关的文章,敬请期待。