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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
性能测试 PTS,5000VUM额度
简介: 在线压测,我们在已有的系统中,模拟一些请求(压测流量)对我们的系统进行压力测试,那么在整个链路中我们是如何让所有的系统都识别出当前的请求是压测流量而不是正式流量的呢?压测流量的标记又是如何在整个链路传递的呢? 又例如我们已经有了链路数据分析能力,能够快速定位到某个请求是在 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进行规格选择与性能压测。
目录
相关文章
|
2月前
|
存储 Dubbo Java
分布式 RPC 底层原理详解,看这篇就够了!
本文详解分布式RPC的底层原理与系统设计,大厂面试高频,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
分布式 RPC 底层原理详解,看这篇就够了!
|
28天前
|
机器学习/深度学习 存储 运维
分布式机器学习系统:设计原理、优化策略与实践经验
本文详细探讨了分布式机器学习系统的发展现状与挑战,重点分析了数据并行、模型并行等核心训练范式,以及参数服务器、优化器等关键组件的设计与实现。文章还深入讨论了混合精度训练、梯度累积、ZeRO优化器等高级特性,旨在提供一套全面的技术解决方案,以应对超大规模模型训练中的计算、存储及通信挑战。
64 4
|
3月前
|
分布式计算 Hadoop 网络安全
Hadoop-08-HDFS集群 基础知识 命令行上机实操 hadoop fs 分布式文件系统 读写原理 读流程与写流程 基本语法上传下载拷贝移动文件
Hadoop-08-HDFS集群 基础知识 命令行上机实操 hadoop fs 分布式文件系统 读写原理 读流程与写流程 基本语法上传下载拷贝移动文件
47 1
|
3月前
|
存储 缓存 数据处理
深度解析:Hologres分布式存储引擎设计原理及其优化策略
【10月更文挑战第9天】在大数据时代,数据的规模和复杂性不断增加,这对数据库系统提出了更高的要求。传统的单机数据库难以应对海量数据处理的需求,而分布式数据库通过水平扩展提供了更好的解决方案。阿里云推出的Hologres是一个实时交互式分析服务,它结合了OLAP(在线分析处理)与OLTP(在线事务处理)的优势,能够在大规模数据集上提供低延迟的数据查询能力。本文将深入探讨Hologres分布式存储引擎的设计原理,并介绍一些关键的优化策略。
157 0
|
3月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
5月前
|
NoSQL Redis
基于Redis的高可用分布式锁——RedLock
这篇文章介绍了基于Redis的高可用分布式锁RedLock的概念、工作流程、获取和释放锁的方法,以及RedLock相比单机锁在高可用性上的优势,同时指出了其在某些特殊场景下的不足,并提到了ZooKeeper作为另一种实现分布式锁的方案。
135 2
基于Redis的高可用分布式锁——RedLock
|
28天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
87 5
|
2月前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
65 8
|
2月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
61 16
|
2月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
44 5