一、需求背景:
随着自研的 Native 端 SkyWalking Agent 落地后,其能力、稳定性逐渐被肯定,隔壁移动部门的核心 WZ 业务开启了全面接入,链路信息覆盖了界面切换、用户操作以及请求交互,实时、精准的 Trace 数据让同事的日常巡检和问题排查变轻松了许多,杂事少了想法就多了,这不正准备将追踪能力应用到 IM 系统上,以便加强对 IM 数据的检测,提升 IM 问题的响应速度。
在去年有做过 IM 服务端的插件,但由于历史原因,服务端的线程特别多,一个请求就要二十多个 Segment。当时SkyWalking系统还未调优,IM 的数据量太大,另外服务端也有重构的规划,就没有上线。这次需配合移动端把链路补齐,考虑到定制插件不通用、IM 系统自身会迭代、插件的调试更新较麻烦等几个关键因素,IM 的负责人更期望通过显式的编码构建 Trace,以满足其快速实现、快速响应的目标。
二、寻找方案
印象之中,去年在整理 SkyWalking 的插件套件的时,似乎skywalking-agent\activations
中有一个插件具有这个能力,翻看之后,找到了它apm-toolkit-opentracing-activation
,从文档中找到一些简单的介绍:
- Dependency the toolkit, such as using maven or gradle
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-opentracing</artifactId> <version>{project.release.version}</version> </dependency> 复制代码
- Use our OpenTracing tracer implementation
Tracer tracer = new SkywalkingTracer(); Tracer.SpanBuilder spanBuilder = tracer.buildSpan("/yourApplication/yourService"); 复制代码
内容有点少,再从百度翻查了几页,也只是看到简单的介绍“通过代码方式对 trace 信息进行补充”,而其功能详解和使用示例方面则没有收获。
不过有源码就不担心,因为 SkyWalking 的源码中给的注释还是很清晰的,于是开始探索验证。
三、所需能力梳理
满足 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
- 跨线程传递 Trace 信息
- 离开线程时:如何捕获当前线程的 Trace 上下文
- 进入线程后:如何将其他线程的 Trace 上下文注入到当前线程
先给结论:今天已调研好的能力只有 1 和 2 的一部分,因为参加日更活动,还需在下班后把今天学习的内容整理出来;所以就计划分成上下两篇来介绍,而且为了满足一些同学能够快速使用这个能力的需求,下边先给结论,再讲过程和原理。对于着急使用的同学可快速得到帮助;本篇就先介绍如何创建能关联上游服务的 EntrySpan
四、编码方式创建EntrySpan
4.1 添加依赖
依赖的版本请根据自家的情况填写
<dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-opentracing</artifactId> <version>xxx</version> </dependency> 复制代码
4.2 一个有效的示例
private static void 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之后。 Map map = new HashMap(); String sw8 = "1-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMTEx-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMDAw-1-c2t5d2Fsa2luZy1kZW1v-MTI3LjAuMC4x-L2R1YmJv-MTI3LjAuMC4xOjIwODgw"; map.put("sw8",sw8);//先只构建一个sw8,其他两个暂不处理 TextMap headerCarrier = new TextMapExtractAdapter(map); tracer.extract( Format.Builtin.TEXT_MAP, headerCarrier); //设置标签 activeSpan.setTag("a","a"); activeSpan.setTag("b","b"); activeSpan.setTag("c","c"); //当前EntrySpan中写日志 activeSpan.log("log 1"); //给当前EntrySpan指定名称,有些情况需要这种在运行到一定阶段后,才能从某个变量中获得值,用于构建span的名称 activeSpan.setOperationName("/ReceiveMessage");//重新指定名称 //关闭span,因为只有一个EntrySpan,则会关闭整个Segment,上报给OAP activeSpan.deactivate(); System.out.println("成功关闭EntrySpan,Segment已上报可通过 TID : 95163e8514ec4bfd80d262cff6bc3bea.103.16696261378140111 查询"); } 复制代码
如果就是为了找个示例使用,那么看到这里就可以了,因一些不可抗拒的原因无法截图,所以没提供。后边内容是介绍为什么会有上边的示例代码,笔者在探索验证过程中遇到了哪些问题。
五、小心别踩坑
5.1 Sample values 只是示例,可不能用
1)错误示范
官网找个sw8
的 Sample values,于是代码如下:
String sw8 = "1-TRACEID-SEGMENTID-3-PARENT_SERVICE-PARENT_INSTANCE-PARENT_ENDPOINT-IPPORT" map.put("sw8","xxx");//先只构建一个sw8,其他两个暂不处理 复制代码
2)错误原因
org.apache.skywalking.apm.agent.core.context.ContextManager#extract
中校验这个值不合法。
public static void extract(ContextCarrier carrier) { if (carrier == null) { throw new IllegalArgumentException("ContextCarrier can't be null."); } //注意上边的Sample值,这里校验不通过。 if (carrier.isValid()) { get().extract(carrier); } } 复制代码
3)老老实实找个正确的值,这里提供一个示例
String sw8 = "1-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMTEx-OTUxNjNlODUxNGVjNGJmZDgwZDI2MmNmZjZiYzNiZWEuMTAzLjE2Njk2MjYxMzc4MTQwMDAw-1-c2t5d2Fsa2luZy1kZW1v-MTI3LjAuMC4x-L2R1YmJv-MTI3LjAuMC4xOjIwODgw"; 复制代码
5.2 extract 会开玩笑
1)错误示范
io.opentracing.Tracer#extract
的注释给出 Example, 指引如下
Example: Tracer tracer = ... TextMap httpHeadersCarrier = new AnHttpHeaderCarrier(httpRequest); SpanContext spanCtx = tracer.extract(Format.Builtin.HTTP_HEADERS, httpHeadersCarrier); ... = tracer.buildSpan('...').asChildOf(spanCtx).startActive(); 复制代码
所以,根据示例代码应该分三步写:
- 构建 carrier
- extract carrier,得到一个 spanContext
- 传入 spanContext 创建 span
错误代码如下
//1. 构建 Carrier Map map = new HashMap(); map.put("sw8","xxx");//先只构建一个sw8,其他两个暂不处理 TextMap headerCarrier = new TextMapExtractAdapter(map); //2. 并 通过 extract 注入到 当前Segment 的 ref SpanContext extract = tracer.extract( Format.Builtin.TEXT_MAP, headerCarrier); //3. 构建EntrySpan ActiveSpan activeSpan = tracer.buildSpan("/ReceiveMessage") .asChildOf(extract) .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)// 注意必须设置为entrySpan,才算是EntrySpan .startActive(); 复制代码
2)错误原因
org.apache.skywalking.apm.agent.core.context.ContextManager#extract
中 get()
为 null。
public static void extract(ContextCarrier carrier) { if (carrier == null) { throw new IllegalArgumentException("ContextCarrier can't be null."); } if (carrier.isValid()) { // get() 为null get().extract(carrier); } } 复制代码
为啥 get()为 null,因为org.apache.skywalking.apm.agent.core.context.ContextManager
中的CONTEXT
还是空的,为啥是空的呢?因为CONTEXT
上下文都还没初始化。
private static AbstractTracerContext get() { return CONTEXT.get(); } 复制代码
3)通过创建span
,先初始化CONTEXT
//1. 先构建EntrySpan ActiveSpan activeSpan = tracer.buildSpan("/ReceiveMessage") .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)// 注意必须设置为entrySpan,才算是EntrySpan .startActive(); 复制代码
其他问题还有一些,毕竟忙活了大半天,但笔者感觉有些问题消耗精力是因为缺少引导资料,这里就不叨扰大家了。
六、总结
SkyWalking 主打非侵入式的 Agent 探针能力,从网络中没有顺利的找到自主编程式构建 Trace 的资料,还不确定这种方法是不被推荐、不常用或宣传不到位的哪种原因。当然 Java Agent 方式也是笔者很推崇的,因之前经历过基于 Cat 这种侵入式的埋点,当 Agent 能力需要升级时,要推动业务方配合升级是很麻烦的。但本次遇到的服务无通用性且自身有常规迭代的场景中,似乎用户自主通过编程 API 构建 Trace 信息,倒显得很合适。
本篇的内容看似简单,实际需要对 SkyWalking Agent 的工作机制、源码以及调试技巧等较为熟悉,否则遇到这些问题会无从下手,后边笔者会陆续基于 SkyWalking 整理出更多跟监控相关的文章,敬请期待。