通过编码方式构建SkyWalking 的Trace-中篇

简介: 通过编码方式构建SkyWalking 的Trace-中篇

一、背景

在上一篇《通过编码方式构建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
  1. ExitSpan
  • 如何创建 ExitSpan
  • 如何指定 Peer
  • 如何打 Tag
  • 如何写 Log
  • 如何关闭 span
  • 如何构建 carrier 信息,传递给下游服务,由下游通过 Inject carrier 信息后,Segment内部就完成了ref的构建
  1. 跨线程传递 Trace 信息
  • 离开线程时:如何捕获当前线程的 Trace 上下文
  • 进入线程后:如何将其他线程的 Trace 上下文注入到当前线程

先给结论:今天精力有限已调研好的能力只有第 2 部分,参加日更活动,下班后趁着热乎赶紧将今天学习的内容整理出来;为了满足一些同学能够快速使用这个能力的需求,下边先给结论,再讲过程和原理。对于着急使用的同学可快速得到帮助;本篇介绍ExitSpan的创建,以及跨进程ExitSpan和EntrySpan之间ref如何关联。第3部分放到下一篇介绍。

三、目标

先了解一下本篇模拟的场景和构建的trace效果,否则示例代码看起来很懵。

3.1 目标

目标是是构建这样两个Segment

  • 每个Segment中有一个EntrySpan,一个ExitSpan
  • 第一个SegmentExitSpan-1对应第二个SegmentEntrySpan-2

2Zmh5D.gif

四、编码方式构建

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 示例效果

2Zmh5D.gif

如果就是为了找个示例使用,那么看到这里就可以了,效果图你看出问题没(因为第一个Segment没有等第二个Segment就直接关闭了)?后边内容是介绍为什么会有上边的示例代码,笔者在探索验证过程中遇到了哪些问题。

5. 补充Segment的关联设计

1) 一个问题

一个Trace中所有Segment的 TID 是相同的,即通过TID把相关的Segment串联起来,请想象一下,通过TID把相关Segment罗列出来之后,这些Segment之间的先后顺序是怎样,如何绘制请求在SegmentSpan之间游走的轨迹呢?

2Zmh5D.gif

2) ref 的关联设计

  • 下游服务Segment中的EntrySpan总是与上游服务Segment中的某个ExitSpan关联
  • 下游服务Segment中的处理的请求源自上游Segment,如EntrySpan-2对应于ExitSpan-2
  • 对下游服务Segment来说ref关系满足请求的一入一出原则;对上游服务Segment来说满足一出多入(0..n)出原则
  • SegmentEntrySpanparentSpanId 是上游Segment对应ExitSpanspanId,即EntrySpan-2parentSpanIdExitSpan-2spanId,此处很重要。

六、小心别踩坑

6.1  EntrySpanextract 无法使用 ExitSpaninject 的 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中就拿不到ExitSpantrace上下文。

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


相关文章
|
6月前
|
存储 Go
Go 浅析主流日志库:从设计层学习如何集成日志轮转与切割功能
本文将探讨几个热门的 go 日志库如 logrus、zap 和官网的 slog,我将分析这些库的的关键设计元素,探讨它们是如何支持日志轮转与切割功能的配置。
281 0
Go 浅析主流日志库:从设计层学习如何集成日志轮转与切割功能
|
前端开发 Java API
skywalking番外01 - 如何扩展%tid的logback占位符
skywalking番外01 - 如何扩展%tid的logback占位符
354 0
|
存储 监控 Java
一篇文章带你搞懂SkyWalking调用链追踪框架
介绍了Skywalking的作用,安装方法,架构设计等等
一篇文章带你搞懂SkyWalking调用链追踪框架
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之配置Logback以在控制台输出日志的问题如何解决
Java应用结构规范问题之配置Logback以在控制台输出日志的问题如何解决
|
Kubernetes Java Linux
轻量级日志系统Loki原理简介和使用(3)
轻量级日志系统Loki原理简介和使用(3)
952 0
轻量级日志系统Loki原理简介和使用(3)
|
6月前
|
Dubbo Java 应用服务中间件
微服务框架(十七)Dubbo协议及编码过程源码解析
  此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为Dubbo协议、线程模型、和其基于Netty的NIO异步通讯机制及源码
|
6月前
|
消息中间件 Dubbo Java
Simple RPC - 01 框架原理及总体架构初探
Simple RPC - 01 框架原理及总体架构初探
82 0
|
XML Java 数据库
JUL 日志 - 最简单易用的Java日志框架
JUL是最容易上手的Java日志框架,最适合初学者,本文一篇教会如何使用
316 0
JUL 日志  - 最简单易用的Java日志框架
|
人工智能 监控 Java
SpringBoot实战(十六):集成Skywalking调用链监控系统
SpringBoot实战(十六):集成Skywalking调用链监控系统
775 0
|
中间件
go-micro集成链路跟踪的方法和中间件原理2
go-micro集成链路跟踪的方法和中间件原理2
91 0