# 链路功能

本文档介绍Sermant插件如何使用链路功能。

# 功能介绍

为了满足系统的高并发需求,软件架构日益复杂,系统中越来越多的组件开始走向分布式化,如单体架构拆分为微服务、服务内缓存变为分布式缓存、服务组件通信变为分布式消息,这些组件共同构成了繁杂的分布式系统,所以对分布式系统的流量链路中各个执行单元进行标记就显得尤为重要。通过链路标记,可获得在分布式系统流量链路中各个执行单元之间的依赖关系,执行顺序等,可用于分布式链路追踪,链路性能分析,分布式系统故障定位等多种场景。

Sermant提供了针对分布式系统中进行链路标记的能力,开发者在开发时只需要正确的选择正确的执行单元进行标记,并指定链路上下文信息在链路中的传递方式即可。

# 开发示例

本开发示例基于创建首个插件文档中创建的工程,在该工程中模拟了一个服务调用过程,并使用HashMap来模拟通信载体:

加载失败

SimulateServer模拟WEB服务器,其中handleRequest模拟处理请求的逻辑,consume用于调用下游服务。

SimulateProvider模拟WEB服务器的下游服务,其中handleConsume用于处理来自SimulateServer的调用。

如需对链路进行完整的链路标记,需要对对分布式系统中关键执行单元进行打标,本示例中主要涉及两种:

  • 对服务提供者执行单元进行链路标记:分布式系统各个节点的入口,此处需要通过分布式系统中的通信载体提取当前链路的上下文信息,如果无链路上下文信息,则当作新链路进行标记。
  • 对服务消费者执行单元进行链路标记:分布式系统各个节点的出口,此处需要将链路数据的标记放入分布式系统中的通信载体,保持链路的连续性。

基于以上基础,我们针对工程中的服务调用过程来进行链路标记:

首先在工程中template\template-plugin下创建针对SimulateServerhandleRequest拦截器,并在其中实现如下逻辑:

注:拦截器的创建方法参考创建首个插件,本开发示例不再赘述

TracingService tracingService = ServiceManager.getService(TracingService.class);

@Override
public ExecuteContext before(ExecuteContext context) throws Exception {
    TracingRequest request =
            new TracingRequest(context.getRawCls().getName(), context.getMethod().getName());
    ExtractService<HashMap<String, String>> extractService = (tracingRequest, carrier) -> {
        tracingRequest.setTraceId(carrier.get(TracingHeader.TRACE_ID.getValue()));
        tracingRequest.setParentSpanId(carrier.get(TracingHeader.PARENT_SPAN_ID.getValue()));
        tracingRequest.setSpanIdPrefix(carrier.get(TracingHeader.SPAN_ID_PREFIX.getValue()));
    };
    Optional<SpanEvent> spanEventOptional = tracingService.onProviderSpanStart(request, extractService, (HashMap<String, String>) context.getArguments()[0]);
    if (spanEventOptional.isPresent()) {
        SpanEvent spanEvent = spanEventOptional.get();
        LOGGER.info("TraceId:" + spanEvent.getTraceId());
        LOGGER.info("SpanId:" + spanEvent.getSpanId());
        LOGGER.info("ParentSpanId:" + spanEvent.getParentSpanId());
        LOGGER.info("Class:" + spanEvent.getClassName());
        LOGGER.info("Method:" + spanEvent.getMethod());
    }
    tracingService.onSpanFinally();
    return context;
}

@Override
public ExecuteContext after(ExecuteContext context) throws Exception {
    return context;
}

@Override
public ExecuteContext onThrow(ExecuteContext context) throws Exception {
    tracingService.onSpanError(context.getThrowable());
    return context;
}

SimulateServerhandleRequest即为该分布式系统中,SimulateServer这一节点的服务提供者执行单元,所以这里使用TracingService::onProviderSpanStart来标记该单元,并且通过ExtractService定义了从通信载体中提取链路信息的能力。

接着创建针对SimulateServerconsume拦截器,并在其中实现如下逻辑:

TracingService tracingService = ServiceManager.getService(TracingService.class);

@Override
public ExecuteContext before(ExecuteContext context) throws Exception {
    TracingRequest request =
            new TracingRequest(context.getRawCls().getName(), context.getMethod().getName());
    InjectService<HashMap<String, String>> injectService = (spanEvent, carrier) -> {
        carrier.put(TracingHeader.TRACE_ID.getValue(), spanEvent.getTraceId());
        carrier.put(TracingHeader.PARENT_SPAN_ID.getValue(), spanEvent.getSpanId());
        carrier.put(TracingHeader.SPAN_ID_PREFIX.getValue(), spanEvent.getNextSpanIdPrefix());
    };
    Optional<SpanEvent> spanEventOptional = tracingService.onConsumerSpanStart(request, injectService, (HashMap<String, String>) context.getArguments()[0]);
    if (spanEventOptional.isPresent()) {
        SpanEvent spanEvent = spanEventOptional.get();
        LOGGER.info("TraceId:" + spanEvent.getTraceId());
        LOGGER.info("SpanId:" + spanEvent.getSpanId());
        LOGGER.info("ParentSpanId:" + spanEvent.getParentSpanId());
        LOGGER.info("Class:" + spanEvent.getClassName());
        LOGGER.info("Method:" + spanEvent.getMethod());
    }
    tracingService.onSpanFinally();
    return context;
}

@Override
public ExecuteContext after(ExecuteContext context) throws Exception {
    return context;
}

@Override
public ExecuteContext onThrow(ExecuteContext context) throws Exception {
    tracingService.onSpanError(context.getThrowable());
    return context;
}

SimulateServerconsume即为该分布式系统中,SimulateServer这一节点的服务消费者执行单元,所以这里使用TracingService::onConsumerSpanStart来标记该执行单元,并且通过InjectService定义了将链路上下文信息注入通信载体中的能力。

创建针对SimulateProviderhandleConsume拦截器,并在其中实现如下逻辑:

TracingService tracingService = ServiceManager.getService(TracingService.class);

@Override
public ExecuteContext before(ExecuteContext context) throws Exception {
    TracingRequest request =
            new TracingRequest(context.getRawCls().getName(), context.getMethod().getName());
    ExtractService<HashMap<String, String>> extractService = (tracingRequest, carrier) -> {
        tracingRequest.setTraceId(carrier.get(TracingHeader.TRACE_ID.getValue()));
        tracingRequest.setParentSpanId(carrier.get(TracingHeader.PARENT_SPAN_ID.getValue()));
        tracingRequest.setSpanIdPrefix(carrier.get(TracingHeader.SPAN_ID_PREFIX.getValue()));
    };
    Optional<SpanEvent> spanEventOptional = tracingService.onProviderSpanStart(request, extractService, (HashMap<String, String>) context.getArguments()[0]);
    if (spanEventOptional.isPresent()) {
        SpanEvent spanEvent = spanEventOptional.get();
        LOGGER.info("TraceId:" + spanEvent.getTraceId());
        LOGGER.info("SpanId:" + spanEvent.getSpanId());
        LOGGER.info("ParentSpanId:" + spanEvent.getParentSpanId());
        LOGGER.info("Class:" + spanEvent.getClassName());
        LOGGER.info("Method:" + spanEvent.getMethod());
    }
    tracingService.onSpanFinally();
    return context;
}

@Override
public ExecuteContext after(ExecuteContext context) throws Exception {
    return context;
}

@Override
public ExecuteContext onThrow(ExecuteContext context) throws Exception {
    tracingService.onSpanError(context.getThrowable());
    return context;
}

SimulateProviderhandleConsume即为该分布式系统中SimulateServer这一节点的服务提供者执行单元,所以这里使用TracingService::onProviderSpanStart来标记该执行单元,并且通过ExtractService定义了从通信载体中提取链路信息的能力。

开发完成后,可参照创建首个插件时的打包构建流程,在工程根目录下执行 mvn package,执行完成后在根目录执行 cd agent/,并在其中携带Sermant运行测试应用,执行如下命令 java -javaagent:sermant-agent.jar -jar Application.jar

$ java -javaagent:sermant-agent.jar -jar Application.jar
[INFO] Loading core library... 
[INFO] Building argument map... 
[INFO] Loading sermant agent... 
[INFO] Load sermant done. 
Good morning!
Good afternoon!
Good night!

在插件中定义的执行逻辑已被增强到测试应用中。接下来,查看程序运行时产生的日志:

  1. 执行如下命令 cd logs/sermant/core/app/${yyyy-mm-dd}/进入运行日志存放目录,其中 ${yyyy-mm-dd}指代运行时基于日期生成的目录名。
  2. 打开日志文件sermant-0.log检查日志内容,可以在其中看到如下日志,通过该日志的SpanIdParentSpanId可以还原该应用的链路关系:
[TemplateTracingDeclarer.java] [before:55] [main] TraceId:137232c4-ce6e-4161-b44d-88cd819145e5
[TemplateTracingDeclarer.java] [before:55] [main] SpanId:0
[TemplateTracingDeclarer.java] [before:56] [main] ParentSpanId:null
[TemplateTracingDeclarer.java] [before:57] [main] Class:com.huaweicloud.template.SimulateServer
[TemplateTracingDeclarer.java] [before:58] [main] Method:handleRequest

[TemplateTracingDeclarer.java] [before:88] [main] TraceId:137232c4-ce6e-4161-b44d-88cd819145e5
[TemplateTracingDeclarer.java] [before:89] [main] SpanId:1
[TemplateTracingDeclarer.java] [before:90] [main] ParentSpanId:0
[TemplateTracingDeclarer.java] [before:91] [main] Class:com.huaweicloud.template.SimulateServer
[TemplateTracingDeclarer.java] [before:92] [main] Method:consume

[TemplateTracingDeclarer.java] [before:122] [main] TraceId:137232c4-ce6e-4161-b44d-88cd819145e5
[TemplateTracingDeclarer.java] [before:123] [main] SpanId:1-0-0
[TemplateTracingDeclarer.java] [before:124] [main] ParentSpanId:1
[TemplateTracingDeclarer.java] [before:125] [main] Class:com.huaweicloud.template.SimulateProvider
[TemplateTracingDeclarer.java] [before:126] [main] Method:handleConsume

根据日志中携带的链路标记信息可分析出以下结论

  • 三组数据的TraceId一致,代表这是同一条链路的链路数据。
  • SimulateServerhandleRequest为链路入口,因为其ParentSpanIdnull
  • SimulateServerconsumeParentSpanId0,可见其被SimulateServerhandleRequest所调用。
  • SimulateProviderhandleConsumeParentSpanId1,可见其被SimulateServerconsume所调用。
  • SimulateProviderhandleConsumeSpanId1-0-0,相较于ParentSpanId多了两位可见其和SimulateProvider并非同进程。相差位的值为0,也可以推断出这是该链路中上游对其发起的第一次调用。

注:链路标记的生成规则可参考规则

由此可见,在分布式系统中针对各个执行单元进行链路标记,可以清晰的了解在分布式系统中各个执行单元之间的依赖关系,执行顺序等。

# API&配置

  • Sermant封装了针对分布式系统中对各类执行单元链路标记操作,抽象为基础服务,使用时需获取其实例。
TracingService tracingService = ServiceManager.getService(TracingService.class);
  • 针对服务提供者执行单元的链路标记操作,分布式系统各个节点的入口,此处需要通过分布式系统中的通信载体提取当前链路的信上下文信息,如果无链路上下文信息,则当作新的链路进行标记。
tracingService.onProviderSpanStart(TracingRequest tracingRequest, ExtractService<T> extractService, T carrier);
  • 针对服务消费者执行单元的链路标记操作,分布式系统各个节点的出口,此处需要将链路数据的上下文信息放入分布式系统中的通信载体,保持链路的连续性。
tracingService.onConsumerSpanStart(TracingRequest tracingRequest, InjectService<T> injectService, T carrier);
  • 针对服务内部执行单元的链路标记操作,分布式系统各个节点的内部调用,只需要关注内部的调用顺序,无需和分布式系统中的通信载体交互。
tracingService.onNormalSpanStart(TracingRequest tracingRequest);
  • 针对各执行单元结束时的链路标记步操作各个单元结束时,需要使用该标记步骤。
tracingService.onSpanFinally();
  • 针对各执行单元异常的链路标记操作,各个单元执行遇到异常时,需要使用该标记步骤。
tracingService.onSpanError(Throwable throwable);

# 函数式接口

在进行链路标记的过程中,需要从分布式系统通信载体中提取和注入链路上下文信息,这两种能力的具体实现,可通过下述两个函数式接口来定义:

  • 从分布式系统通信载体中提取链路上下文信息需实现:
@FunctionalInterface
public interface ExtractService<T> {
    /**
     * 跨进程链路追踪,需要将链路信息从协议载体中取出,
     */
    void getFromCarrier(TracingRequest tracingRequest, T carrier);
}
  • 从分布式系统通信载体中注入链路上下文信息需实现:
@FunctionalInterface
public interface InjectService<T> {
    /**
     * 跨进程链路追踪,需要将链路信息内容放入协议载体
     */
    void addToCarrier(SpanEvent spanEvent, T carrier);
}

# 规则

在链路标记的数据中,SpanId是一个很重要的数据,其表明了各个执行单元之间的执行顺序和父子逻辑,Sermant提供的链路标记的SpanId生成规则如下:

加载失败

其中A服务是链路的入口,SpanId为0,在这里会生成一条链路的TraceId。重点关注B服务和C服务的SpanId,最重要的是后两位值

  • B服务的后两位值0-0,其中第一位表示B服务是A服务的第1次调用,第二位表示该信息属于B服务中的第一个执行单元。
  • C服务的后两位值1-0,其中第一位表示C服务是A服务的第2次调用,第二位表示该信息属于C服务中的第一个执行单元。

除分布式系统入口的执行单元的链路信息没有ParentSpanId,其他执行的单元链路信息皆有,者久可以清晰的知道他的上游执行单元是谁,通过这种关系,最终就可以串联出完整的链路信息。

通过上述描述,Sermant提供的SpanId生成规则应该很清晰了。

# 说明

针对链路上下文信息,在分布式系统中传递的上下文信息为key:value的形式,以下针对链路上下文信息的key做说明:

  • sermant-trace-id,用于存放链路的TraceId,用于标识一条链路。
TracingHeader.TRACE_ID.getValue()
  • sermant-parent-span-id,用于存放当前SpanId传递给下游,告知下游其上游的SpanId值,用于串联上下游执行单元。
TracingHeader.PARENT_SPAN_ID.getValue()
  • sermant-span-id-prefix,用于存放下游定义自身SpanId的前缀,用于标识该下游是自己的第几个分支。
TracingHeader.SPAN_ID_PREFIX.getValue()
上次更新: 2024/4/1 03:31:21