ITPub博客

首页 > 数据库 > 数据库开发技术 > SkyWalking 的Trace还能通过编码构建 -中篇

SkyWalking 的Trace还能通过编码构建 -中篇

原创 数据库开发技术 作者:架构染色 时间:2022-12-02 22:20:22 0 删除 编辑

相关文章《Skywalking on the way-千亿级的数据储能、毫秒级的查询耗时》

欢迎关注公众号【架构染色】交流学习

一、背景

在上一篇《通过编码方式构建 SkyWalking 的 Trace-上篇》中介绍了为什么需要通过编码方式构建 SkyWalking Trace 的背景,并梳理了编程方式需要提供的能力合集。每一步都走的不顺利,昨天只摸索出了 EntrySpan 的构建方法,和部分的 ExitSpan 能力,今天时间有限,加上也依然有点曲折,所以今天就变成了中篇,好了基本情况同步完毕,开始正题。

二、所需能力梳理

满足 IM 埋点的功能,需要通过显示的编码来实现以下功能:

  1. 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 添加依赖

    pom 依赖的版本请根据自家的情况填写

    <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 整理出更多跟监控相关的文章,敬请期待。

    最后说一句(请关注,莫错过)

    如果这篇文章对您有帮助,或有所启发的话,欢迎关注公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。

    原文作者:架构染色;如有侵权,请联系管理员删除。

    请登录后发表评论 登录
    全部评论

    注册时间:2022-08-23

    • 博文量
      49
    • 访问量
      23706