蚂蚁金服分布式链路跟踪组件 SOFATracer 链路透传原理与SLF4J MDC 的扩展能力分析 | 剖析

简介: 在线压测,我们在已有的系统中,模拟一些请求(压测流量)对我们的系统进行压力测试,那么在整个链路中我们是如何让所有的系统都识别出当前的请求是压测流量而不是正式流量的呢?压测流量的标记又是如何在整个链路传递的呢? 又例如我们已经有了链路数据分析能力,能够快速定位到某个请求是在 A 系统里出的问题,那么我们怎么从 A 系统的业务日志里找到当前请求对应的业务日志呢? 带着这些问题,让我们先来看看 SOFATracer 的链路透传以及支持 SLF4J MDC 扩展能力。

SOFA
Scalable Open Financial Architecture
是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。

SOFATracer 是一个用于分布式系统调用跟踪的组件,通过统一的 TraceId 将调用链路中的各种网络调用情况以日志的方式记录下来,以达到透视化网络调用的目的,这些链路数据可用于故障的快速发现,服务治理等。

本文为《剖析 | SOFATracer 框架》第二篇。《剖析 | SOFATracer 框架》系列由 SOFA 团队和源码爱好者们出品,项目代号:SOFA:TracerLab,目前领取已经完成,感谢大家的参与。

SOFATracer:https://github.com/alipay/sofa-tracer

SOFATracer-数据上报.jpg

SOFATracer 是一个用于分布式系统调用跟踪的组件,其核心作用就是能够在分布式场景下将请求经过的各个的链路环节的相关数据记录下来,通过这些数据将各个调用链路相关的组件串联起来。

在日常的开发中,我们除了跟踪链路外,可能还会遇到一些场景:

例如在线压测,我们在已有的系统中,模拟一些请求(压测流量)对我们的系统进行压力测试,那么在整个链路中我们是如何让所有的系统都识别出当前的请求是压测流量而不是正式流量的呢?压测流量的标记又是如何在整个链路传递的呢?

又例如我们已经有了链路数据分析能力,能够快速定位到某个请求是在 A 系统里出的问题,那么我们怎么从 A 系统的业务日志里找到当前请求对应的业务日志呢?

带着这些问题,让我们先来看看 SOFATracer 的链路透传以及支持 SLF4J MDC 扩展能力。

SOFATracer 链路透传原理

SOFATracer 的链路透传具体包括两个点:

  • 跨进程的透传,即如何将链路数据从一个进程传递到下游进程中
  • 线程中的透传

    • 当前请求跨进程调用结束之后,当前如何恢复 tracer 上下文信息
    • 如何实现跨线程的透传,如在当前线程中起一个异步线程的场景

跨进程链路透传原理

跨进程透传就是将上游系统的链路数据透传到下游系统中,以便于提取出全局的链路标记,如 TracerId 、采样标记等,来实现将服务串联起来并且保持传输过程中某些属性的一致性。SOFATracer 基于 Opentracing 规范实现,因此在链路透传部分,也是基于此规范;下面就先从 Opentracing 规范中的透传开始说起。

Opentracing 中的定义

在 OT 原文有这么一段描述 传送门

Programmers adding tracing support across process boundaries must understand the Tracer.Inject(...)and Tracer.Extract(...) capabilities of the OpenTracing specification. They are conceptually powerful, allowing the programmer to write _correct_general_ cross-process propagation code without being bound to a particular OpenTracing implementation; that said, with great power comes great opportunity for confusion. 

大概意思就是:如果开发者要给应用添加跨进程的追踪能力, 首先要理解 OpenTracing 规范中的 Tracer.Inject(...) 和 Tracer.Extract(…) 的功能。它们在概念抽象上非常强大,而且允许开发者编写正确的、通用的跨进程传输的代码,而不需要绑定到特定的 OpenTracing 实现上去。 

总的来说就是 Opentracing 的 Tracer 接口定义了跨进程的能力,但是就是没具体实现,不同的基于此规范实现的组件,需要遵循此规范来实现具体的透传逻辑,下面是 Tracer 接口定义的用于透传的两个方法:

接口 描述
 void inject(SpanContext spanContext, Formatformat, C carrier); 把 spanContext 以指定的 format 的格式注入到 carrier 中
 SpanContext extract(Format format, C carrier); 以指定的 format 的格式从 carrier 中解析出 SpanContext

进程透传实现分析

SOFATracer 的 Tracer 的实现类是 SofaTracer, UML 图如下:

从图中可以看出 SofaTracer 除了有跨进程传输的能力,还扩展了数据上报的能力( Reporter )和采样能力( Sampler )。数据上报能力可以参考 SOFATracer 数据上报机制和源码分析|剖析 这篇文章;采样将在下一篇文章中进行剖析。

跨进程透传的就是 SpanContext 的内容, carrier 为传输的载体, SpanContext 的实现类为 SofaTracerSpanContext, UML 图:

image.png

跨进程透传处理流程

SOFATracer 中跨进程传输的总体流程如下图所示:

透传原理的实质就是:调用方编码将指定内容传输到被调方, 被调方解码获取内容的过程。

跨进程透传的方式有很多, 在这里以客户端向服务端发起 HTTP 请求的方式来演示跨进程传输, fork 代码, 打开 sample/tracer-sample-with-httpclient 示例工程运行 HttpClientDemoApplication ,打开 logs/tracelog/spring-mvc-stat.log 即可看到链路日志, 运行结果 :

{"time":"2019-01-07 19:42:50.134","stat.key":{"method":"GET","local.app":"HttpClientDemo","request.url":"http://localhost:8080/httpclient"},"count":1,"total.cost.milliseconds":1563,"success":"true","load.test":"F"}
{"time":"2019-01-07 20:09:46.285","stat.key":{"method":"GET","local.app":"HttpClientDemo","request.url":"http://localhost:8080/httpclient"},"count":1,"total.cost.milliseconds":71,"success":"true","load.test":"F"}
{"time":"2019-01-07 20:14:52.628","stat.key":{"method":"GET","local.app":"HttpClientDemo","request.url":"http://localhost:8080/httpclient"},"count":2,"total.cost.milliseconds":111,"success":"true","load.test":"F"}

透传链路如下:

1、客户端

首先找到客户端拦截的入口类 com.alipay.sofa.tracer.plugins.httpclient.interceptor.SofaTracerHttpInterceptor & com.alipay.sofa.tracer.plugins.httpclient.interceptor.SofaTracerAsyncHttpInterceptor
以 SofaTracerHttpInterceptor 为例:

// 拦截请求
  public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
      //lazy init
      RequestLine requestLine = httpRequest.getRequestLine();
      String methodName = requestLine.getMethod();
      // 生成SpanContext和Span
      SofaTracerSpan httpClientSpan = httpClientTracer.clientSend(methodName);
      // 把 SpanContext inject到Carrier中
      super.appendHttpClientRequestSpanTags(httpRequest, httpClientSpan);
  }

生成 Span 的最后一步是 com.alipay.common.tracer.core.SofaTracer.SofaTracerSpanBuilder#start

public Span start() {
      SofaTracerSpanContext sofaTracerSpanContext = null;
      // 判断当前Span是否为链路中的root节点, 如果不是则创建子Span上下文, 否则创建一个RootSpan上下文
      if (this.references != null && this.references.size() > 0) {
          sofaTracerSpanContext = this.createChildContext();
      } else {
          sofaTracerSpanContext = this.createRootSpanContext();
      }
      long begin = this.startTime > 0 ? this.startTime : System.currentTimeMillis();
      // 构建Span
      SofaTracerSpan sofaTracerSpan = new SofaTracerSpan(SofaTracer.this, begin,
                   this.references, this.operationName, sofaTracerSpanContext, this.tags);
      // 采样行为计算
      boolean isSampled = calculateSampler(sofaTracerSpan);
      sofaTracerSpanContext.setSampled(isSampled);
      return sofaTracerSpan;
  }

最后就是把 SpanContext 注入到 Carrier 中以 HTTP HEAD 的方式透传到下游。

关于数据注入载体和从载体中提取数据可以参考 com.alipay.common.tracer.core.registry.AbstractTextB3Formatter 类的实现。

2、服务端

找到服务端的拦截入口 SpringMvcSofaTracerFilter ,功能很简单:

  • 获取上游传来的 SpanContext
  • 构建服务端的 Span,在这里和客户端做了同样的判断, 判断当前 Span 是否为 RootSpan,这个操作很重要,如果是 RootSpan 则意味着一条新的链路要被构建;如果不是 RootSpan ,则会将当前服产生的 Span 通过 tracerId 关联到当前链路中来。

线程透传原理

在介绍线程透传原理之前先来看个例子;对于 MVC 组件来说,如果我们想使用一个 Span 来记录 mvc 的执行过程。一般我可以把 Span 的开始放在 Filter 中,filterChain.doFilter 方法执行之前产生,然后再 finally 块中来结束这个 Span,大概如下:

 // Span span = null      //  1
try{
  // to create a new span
  span = serverReceive()
  // do something
  filterChain.doFilter(servletRequest, responseWrapper);
  // do something
 }finally{
  // to finish current span
  serverSend();
 }

假如现在有个问题是,在 serverReceive 和 serverSend 这段过程中涉及到了其他组件也产生了 Span,比如说发起了一次 httpclient 调用。大概对应的 tracer 如下:

|mvcSpan|
     .
     |httpclientSpan|
            ...
     |httpclientSpan|
  .
|mvcSpan|

这是典型的 child_of 关系, httpclientSpan 、 child_of mvcSpan 且都在同一个线程中执行。OK,解法:

  • 1、显示的申明一个 Span ,如上面代码段中 1 的位置。这样 Span 的作用域足够大,可以在 finally 中通过显示调用 span#finish 来结束。
  • 2、使用 ThreadLocal 机制,在 serverReceive 中将当前 Span 放到 ThreadLocal 中,httpclientSpan 作用时,从 ThreadLocal 中先拿出 mvcSpan,然后作为 httpclientSpan 的父 Span 。此时将 httpclientSpan 塞到 ThreadLocal 中。当 httpclientSpan 结束时,再将 mvcSpan 复原到 ThreadLocal 中。

对于解法1 ,如果想在 httpclientSpan 的处理逻辑中使用 mvcSpan 怎么办呢?通过参数传递?那如果链路很长呢?显然这种方式是不可取的。因此 SOFATracer 在实现上是基于 解法2 的方案来实现的。

综合上面的案例,线程透传可以从以下两个角度来理解:

  • 1、当前线程中如果发生了跨进程调用(如 RPC 调用),那么跨进程调用结束之后如何恢复 Tracer 上下文信息
  • 2、当前线程执行过程中,又起了异步线程来执行一些子任务(如任务调度),如何将当前线程 Tracer 上下文传递到子线程中

下面就针对这两个问题,来分析下 SOFATracer 的线程透传实现。

线程透传实现分析

在 SOFATracer 中定义了一个 SofaTraceContext 接口,允许应用程序访问和操纵当前 Span 的状态,默认实现是 SofaTracerThreadLocalTraceContext; UML 图:

image.png

SofaTracerThreadLocalTraceContext 实际上就是使用了 ThreadLocal 来存储当前线程 Tracer 上下文信息的。下面以 AbstractTracer#serverReceive 代码片段来看下 SOFATracer 中存入 Span 的逻辑:

public SofaTracerSpan serverReceive(SofaTracerSpanContext sofaTracerSpanContext) {
      // 省略 ...
      SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
      try {
          // ThreadLocal 初始化, 存入当前的Span
          sofaTraceContext.push(sofaTracerSpanServer);
      } catch (Throwable throwable) {
          // 省略 ...
      } finally {
          // 省略 ...
      }
      return sofaTracerSpanServer;
  }
  • 通过 SofaTraceContextHolder 或到 SofaTraceContext  的实例对象,本质上就是 SofaTracerThreadLocalTraceContext 的单例对象
  • 将当前 Span 放入到 SofaTracerThreadLocalTraceContext,也就是存入 ThreadLocal 中

如果在后面的业务处理过程中需要用到此 Span ,那么就可以通过SofaTraceContextHolder.getSofaTraceContext().getCurrentSpan() 这样简单的方式获取到当前 Span 。

那么既然是通过 ThreadLocal 来进行 tracer 上下文的存储,为了保证 ThreadLocal 不被污染,同时防止内存泄漏,需要在当前 Span 结束时清理掉当前 线程上下文 中的数据。下面通过 AbstractTracer#serverSend 代码片段来看下 SOFATracer 中清理线程上下文中透传数据的逻辑:

public void serverSend(String resultCode) {
  try {
    // 或取当前 SofaTraceContext 实例
    SofaTraceContext sofaTraceContext = SofaTraceContextHolder.getSofaTraceContext();
    // 取出 span 信息,这里相当于就是恢复 tracer上下文状态信息
    SofaTracerSpan serverSpan = sofaTraceContext.pop();
    if (serverSpan == null) {
        return;
    }
    //log
    serverSpan.log(LogData.SERVER_SEND_EVENT_VALUE);
    // 结果码
    serverSpan.setTag(CommonSpanTags.RESULT_CODE, resultCode);
    serverSpan.finish();    
    } finally {
      //处理完成要清空 TL
      this.clearTreadLocalContext();
  }
}

所以在整个线程处理过程中,SOFATracer 在 tracer 上下文 处理上均是基于 Threadlocal 来完成的。

PS:SofaTraceContext  中封装了一系列用于操作 threadlocal 的工具方法,上面提到的 getCurrentSpan 和 pop 的区别在于,getCurrentSpan 从 threadlocal 中取出 Span 信息之后不会清理,也就是后面还可以通过getCurrentSpan 拿到当前线程上下文中的 Span 数据,因此在业务处理过程中,如果需要向 Span 中添加一些链路数据,可以通过 getCurrentSpan 方法进行设置。pop 方法与 getCurrentSpan 实际上都是通过 threadlocal#get 来取数据的,当时 pop 取完之后会进行 clear 操作,因此 pop 一般用于在请求结束时使用。 SpringMvcSofaTracerFilter 中在 finally 块中调用了 serverSend ,serverSend 中就是使用的 pop 方法。

跨线程透传

前一小节介绍了 tarcer 上下文 如何实现在线程中透传及恢复,那么对于另外一种场景,即在当前线程处理过程中新起了子线程的情况,父线程如何将当前 tracer 上下文信息传递到子线程中去呢?对于这种情况,SOFATracer 也提供了支持,下面就来看下,SOFATracer 是如何实现跨线程传递的。

跨线程传递相对于跨进行传递来说要简单的多,我们不需要考虑载体、格式化方式等;无论是父线程还是子线程,在存储 tracer 上下文 信息的实现上都是一样的,都是基于 ThreadLocal 来存储。但是为了保证当前 tracer 上下文的状态能够在不同的线程中保持一致,不受干扰,SOFATracer 在将 tracer 上下文传递到子线程中时,可以选择使用的是当前父线程  tracer 上下文 的克隆版本:

public SofaTracerSpanContext cloneInstance() {
  // 重新构建一个 SofaTracerSpanContext 对象实例
  // 这里会以当前父线程中的 tracerId,spanId,parentId以及采样信息 作为构建构建参数
  SofaTracerSpanContext spanContext = new SofaTracerSpanContext(this.traceId, this.spanId,
  this.parentId, this.isSampled);
  // 系统透传数据
  spanContext.addSysBaggage(this.sysBaggage);
  // 业务透传数据
  spanContext.addBizBaggage(this.bizBaggage);
  spanContext.childContextIndex = this.childContextIndex;
  return spanContext;
}

这里会根据当前 SofaTracerSpanContext 实例的基本信息,重新 new 一个新的对象出来,是一种深拷贝的方式,实现了不同线程 tracer 上下文处理的隔离。

另外,SOFATracer 还提供了 SofaTracerRunnable&SofaTracerCallable 这两个类 ,封装了底层手动将 Span 复制到被调线程的 ThreadLocal 中去的过程;需要注意的是这个传递的是 Span ,并非是 SpanContext,因此也就没有上面隔离一说,具体使用参考官方文档:异步线程处理。这里以 SofaTracerRunnable 类来进行具体实现分析,SofaTracerCallable 大家可以自己去研究下。

image.png

关于异步线程处理的案例,可以参考 SOFATracer 的测试用例 。

private void initRunnable(Runnable wrappedRunnable, SofaTraceContext traceContext) {
    // 任务 runnable
  this.wrappedRunnable = wrappedRunnable;
  // tracer 上下文,可以由外部指定,如果没有指定则使用 SofaTraceContextHolder 获取
  this.traceContext = traceContext;
  if (!traceContext.isEmpty()) {
    // 将当前上下文中的 span 赋值给子线程
      this.currentSpan = traceContext.getCurrentSpan();
  } else {
      this.currentSpan = null;
  }
}

这上面这段代码片段来看,在构建 SofaTracerRunnable 对象实例时,会把当前父线程中的 traceContext 、currentSpan 等传递到子线程中。SofaTracerRunnable#run 方法中,会根据线程 ID 进行判断,如果与父线程的线程ID不等,则会将 currentSpan push 到 traceContext (注:currentSpan 和 traceContext 均是子线程属性),run 方法则是委托给用户传递进来的 wrappedRunnable 来执行。

Opentracing 0.30.x 版本对于线程透传的支持

对于在低版本 Opentracing 规范中并没有对线程传递的支持,但是在 0.30.0 版本以后有支持。SOFATracer 目前是基于 Opentracing 0.22.0 版本实现的;但是对于 Opentracing 新 API 中提供的线程透传的特性的理解也会有助于 SOFATracer 在线程透传方面的改进

在之前的文章中对于 Span 的层级关系有过介绍,如果按照时序关系来展示大概如下:

image.png

这里以 A、B、D 来看,三个 Span 是逐级嵌套的;如果把这个模型理解成为一个栈的话,那么各个 Span 的产生过程即为入栈的过程,如下:

image.png

由于栈的特性是 FILO ,因此当 span C 出栈时就意味着 span C 的生命周期结束了,此时会触发 Span 数据的上报。这里其实也很好的解释了 ChildOf 这种关系的描述:父级 Span 某种程度上取决于子 Span  (子 Span 的结果可能会对父 Span 产生影响) ;父 Span 的生命周期时间是包含了子 Span 生命周期时间的。

在 SOFATracer 0.30.x 版本中提供了对上述思路的封装,用于解决 Span 在线程中传递的问题。两个核心的接口是Scope 和 ScopeManager ,Opentracing 中对这两个接口均提供了默认的实现类:ThreadLocalScope 和 ThreadLocalScopeManager 。

  • 使用 ThreadLocal 来存储不同线程的 Scope 对象,在多线程环境下可以通过获取到当前线程的 Scope 来获取当前线程的活动的 Span。
  • 管理着当前线程所有曾被激活还未释放的 Span(处于生命周期内的 Span )

ThreadLocalScopeManager & ThreadLocalScope 的设计

ScopeManager 解决的是 Span 在线程中传递的问题。但是 ScopeManager 本身直接操作 Span 又会显得有些不彻底。这个不彻底怎么理解呢?结合 SOFATracer 的实现,我的理解是:

  • SOFATracer 中也是使用 ThreadLocal 的机制实现 Span 在线程中传递的。ThreadLocal 中就是 set & get 。Span 之间的父子关系以及当前 ThreadLocal 中应该存哪个 Span 都需要我们自己在代码中来管理。这种方式完全 OK,但是如果对于一个标准/规范来说,如果只是定义一个这样的 ThreadLocal 完全是没有意义的。
  • 自己管理 ThreadLocal 中 Span 的关系是一个复杂的过程,尤其是在链路较长的情况下。

基于上述两点,ot-api 没有采用直接在 ScopeManager 中基于 ThreadLocal 使用 set&get span 的操作方案。而是使用了 Scope,对应的实现类是 ThreadLocalScope;那么好处在哪呢?

ThreadLocalScope 的设计使用了栈的思想,这个怎么理解呢?在一个线程中,每一个 Span 的产生到结束,里面在嵌套子 Span 的产生到结束,这种嵌套关系可以很容器联想到栈的概念;参考上图,这个过程很好理解,栈的操作,有进有出,一进一出就是一个 Span 的生命周期。

相比于 SOFATracer 的实现来看,Opentracing 提供的线程透传实现更具有全局性;ThreadLocalScope 为 Span 在线程中传递提供了新的设计思路,但是如果仅基于 Span + ThreadLocal 来实现,是很难的。

MDC 的扩展能力分析

SLF4J 提供了 MDC(Mapped Diagnostic Contexts)功能,可以支持用户定义和修改日志的输出格式以及内容。SOFATracer 集成了 SLF4J MDC 功能,方便用户在只简单修改日志配置文件的情况下就可以输出当前 Tracer 上下文的 TraceId 和 SpanId。

SLF4J MDC 机制

MDC ( Mapped Diagnostic Contexts ),这个接口是为了便于我们诊断线上问题而出现的方法工具类。 MDC 的实现也是利用了 ThreadLocal 机制。 在代码中,只需要将指定的值 put 到线程上下文的 Map 中,然后在对应的地方使用 get 方法获取对应的值。

先看一个 logback.xml 的输出模板配置:

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  <encoder charset="UTF-8">
    <pattern>[%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5p) %logger.%M\(%F:%L\)] %X{THREAD_ID} %msg%n</pattern>
  </encoder>
</appender>

在日志模板 logback.xml 中,使用 %X{} 来占位,内容替换为对应的 MDC 中 key 的值,在模板解析时会从 MDC 中去取 key 对应的 value 来替换占位符以达到自定义日志格式的效果。

MDC 在 SOFATracer 中的应用

SOFATracer 对 MDC 的扩展在 com.alipay.common.tracer.extensions.log.MDCSpanExtension,这个类利用了 SpanExtension 的扩展功能来实现。MDC 扩展的代码也比较简单,就是对 MDC 线程上下文值的存储和删除操作,看两段主要的:

  // span 开始时的MDC操作
  public void logStartedSpan(Span currentSpan) {
      if (currentSpan != null) {
          SofaTracerSpan span = (SofaTracerSpan) currentSpan;
          SofaTracerSpanContext sofaTracerSpanContext = span.getSofaTracerSpanContext();
          if (sofaTracerSpanContext != null) {
              // 把当前span的traceId 和 spanId 放到 MDC 中
              MDC.put(MDCKeyConstants.MDC_TRACEID, sofaTracerSpanContext.getTraceId());
              MDC.put(MDCKeyConstants.MDC_SPANID, sofaTracerSpanContext.getSpanId());
  }}}
  // Span结束时的MDC操作
  public void logStoppedSpan(Span currentSpan) {
      // 把当前span的traceId 和 spanId 从 MDC 中移除
      MDC.remove(MDCKeyConstants.MDC_TRACEID);
      MDC.remove(MDCKeyConstants.MDC_SPANID);
      if (currentSpan != null) {
          SofaTracerSpan span = (SofaTracerSpan) currentSpan;
          SofaTracerSpan parentSpan = span.getParentSofaTracerSpan();
          if (parentSpan != null) {
              SofaTracerSpanContext sofaTracerSpanContext = parentSpan.getSofaTracerSpanContext();
              if (sofaTracerSpanContext != null) {
                  // 把父span的traceId 和 spanId 放入 MDC 中
                  MDC.put(MDCKeyConstants.MDC_TRACEID, sofaTracerSpanContext.getTraceId());
                  MDC.put(MDCKeyConstants.MDC_SPANID, sofaTracerSpanContext.getSpanId());
  }}}}

然后修改 logback.xml 的格式表达式:


<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  <encoder charset="UTF-8">
    <!-- %X{SOFA-TraceId} %X{SOFA-SpanId} 对应的就是tracerId和spanId的占位符-->
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{SOFA-TraceId},%X{SOFA-SpanId}]  %logger{50} - %msg%n</pattern>
  </encoder>
</appender>

对应的 Demo 在 tracer-samples/tracer-sample-with-slf4j 下。

小结

回头看文章开头的两个问题,基于 SOFATracer 的数据透传和 MDC 扩展能力已经有了解决方案:

在线压测的时候,我们只需要在入口往 SOFATracer 内设置一个压测标识,通过 SOFATracer 的链路数据透传能力,将压测标识透传到整个调用链路,每个调用链路相关的组件识别这个压测标识进行对应的处理即可。

在业务日志中找到请求相关的日志,只需要在业务日志输出的时候,同步输出 SpanId 和  TracerId,就能标记业务日志的位置,再通过和 Tracer 信息的结合,快速定位问题。

本篇主要剖析了 SOFATracer 在数据透传和 Slf4j MDC 扩展功能两个点;在链路数据透传部分,又分别对 跨进程透传、线程透传和 Opentracing 提供的线程透传等分别作了详细的介绍和分析。Slf4j MDC 扩展部分介绍了 MDC 机制以及 MDC 在 SOFATracer 中的应用。通过本篇,希望可以帮助大家更好的理解 SOFATracer 在链路透传方面的基本原理和实现。

文中涉及到的所有链接:

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
目录
相关文章
|
22天前
|
存储 分布式计算 Hadoop
Hadoop【基础知识 01】【分布式文件系统HDFS设计原理+特点+存储原理】(部分图片来源于网络)
【4月更文挑战第3天】Hadoop【基础知识 01】【分布式文件系统HDFS设计原理+特点+存储原理】(部分图片来源于网络)
37 3
|
22天前
|
存储 分布式计算 监控
Hadoop【基础知识 01+02】【分布式文件系统HDFS设计原理+特点+存储原理】(部分图片来源于网络)【分布式计算框架MapReduce核心概念+编程模型+combiner&partitioner+词频统计案例解析与进阶+作业的生命周期】(图片来源于网络)
【4月更文挑战第3天】【分布式文件系统HDFS设计原理+特点+存储原理】(部分图片来源于网络)【分布式计算框架MapReduce核心概念+编程模型+combiner&partitioner+词频统计案例解析与进阶+作业的生命周期】(图片来源于网络)
70 2
|
11天前
|
存储 NoSQL 分布式数据库
【Flink】Flink分布式快照的原理是什么?
【4月更文挑战第21天】【Flink】Flink分布式快照的原理是什么?
|
26天前
|
消息中间件 存储 监控
解析RocketMQ:高性能分布式消息队列的原理与应用
RocketMQ是阿里开源的高性能分布式消息队列,具备低延迟、高吞吐和高可靠性,广泛应用于电商、金融等领域。其核心概念包括Topic、Producer、Consumer、Message和Name Server/Broker。RocketMQ支持异步通信、系统解耦、异步处理和流量削峰。关键特性有分布式架构、顺序消息、高可用性设计和消息事务。提供发布/订阅和点对点模型,以及消息过滤功能。通过集群模式、存储方式、发送和消费方式的选择进行性能优化。RocketMQ易于部署,可与Spring集成,并与Kafka等系统对比各有优势,拥有丰富的生态系统。
113 4
|
7天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
29 2
|
26天前
|
NoSQL Java Redis
redis分布式锁
redis分布式锁
|
2天前
|
监控 NoSQL 算法
探秘Redis分布式锁:实战与注意事项
本文介绍了Redis分区容错中的分布式锁概念,包括利用Watch实现乐观锁和使用setnx防止库存超卖。乐观锁通过Watch命令监控键值变化,在事务中执行修改,若键值被改变则事务失败。Java代码示例展示了具体实现。setnx命令用于库存操作,确保无超卖,通过设置锁并检查库存来更新。文章还讨论了分布式锁存在的问题,如客户端阻塞、时钟漂移和单点故障,并提出了RedLock算法来提高可靠性。Redisson作为生产环境的分布式锁实现,提供了可重入锁、读写锁等高级功能。最后,文章对比了Redis、Zookeeper和etcd的分布式锁特性。
28 16
探秘Redis分布式锁:实战与注意事项
|
4天前
|
NoSQL Java 大数据
介绍redis分布式锁
分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。
|
4天前
|
缓存 NoSQL Java
【亮剑】如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
5天前
|
NoSQL Redis 微服务
分布式锁_redis实现
分布式锁_redis实现