3
头图

微服务架构下,服务之间的关系错综复杂。从调用一个 HTTP API 到最终返回结果,中间可能发生了多个服务间的调用。而这些被调用的服务,可能部署在不同的服务器上,由不同的团队开发,甚至可能使用了不同的编程语言。在这样的环境中,排查性能问题或者定位故障就很麻烦。

Zipkin 是什么

Zipkin 是一个分布式链路追踪系统(distributed tracing system)。它可以收集并展示一个 HTTP 请求从开始到最终返回结果之间完整的调用链。

基本概念

  • Trace 代表一个完整的调用链。一个 trace 对应一个随机生成的唯一的 traceId。例如一个 HTTP 请求到响应是一个 trace。一个 trace 内部包含多个 span。
  • Span Trace 中的一个基本单元。一个 span 同样对应一个随机生成的唯一的 spanId。例如一个 HTTP 请求到响应过程中,内部可能会访问型数据库执行一条 SQL,这是一个新的 span,或者内部调用另外一个服务的 HTTP API 也是一个新的 span。一个 trace 中的所有 span 是一个树形结构,树的根节点叫做 root span。除 root span 外,其他 span 都会包含一个 parentId,表示父级 span 的 spanId。
  • Annotation 每个 span 中包含多个 annotation,用来记录关键事件的时间点。例如一个对外的 HTTP 请求从开始到结束,依次有以下几个 annotation:

    • cs Client Send,客户端发起请求的,这是一个 span 的开始
    • sr Server Receive,服务端收到请求开始处理
    • ss Server Send,服务端处理请求完成并响应
    • cr Client Receive,客户端收到响应,这个 span 到此结束

    记录了以上的时间点,就可以很容易分析出一个 span 每个阶段的耗时:

    • cr - cs 是整个流程的耗时
    • sr - cs 以及 cr - ss 是网络耗时
    • ss - sr 是被调用服务处理业务逻辑的耗时

    然而,srss 两个 annotation 依赖被调用方,如果被调用方没有相应的记录,例如下游服务没有对接 instrumentation 库,或者像执行一条 SQL 这样的场景,被调用方是一个数据库服务,不会记录 srss,那么这个 span 就只有 cscr

相关文档:

B3 Propagation

当上游服务通过 HTTP 调用下游服务,如何将两个服务中的所有 span 串联起来,形成一个 trace,这就需要上游服务将 traceId 等信息传递给下游服务,而不能让下游重新生成一个 traceId。

Zipkin 通过 B3 传播规范(B3 Propagation),将相关信息(如 traceId、spanId 等)通过 HTTP 请求 Header 传递给下游服务:

   Client Tracer                                                  Server Tracer     
┌───────────────────────┐                                       ┌───────────────────────┐
│                       │                                       │                       │
│   TraceContext        │          Http Request Headers         │   TraceContext        │
│ ┌───────────────────┐ │         ┌───────────────────┐         │ ┌───────────────────┐ │
│ │ TraceId           │ │         │ X-B3-TraceId      │         │ │ TraceId           │ │
│ │                   │ │         │                   │         │ │                   │ │
│ │ ParentSpanId      │ │ Inject  │ X-B3-ParentSpanId │ Extract │ │ ParentSpanId      │ │
│ │                   ├─┼────────>│                   ├─────────┼>│                   │ │
│ │ SpanId            │ │         │ X-B3-SpanId       │         │ │ SpanId            │ │
│ │                   │ │         │                   │         │ │                   │ │
│ │ Sampling decision │ │         │ X-B3-Sampled      │         │ │ Sampling decision │ │
│ └───────────────────┘ │         └───────────────────┘         │ └───────────────────┘ │
│                       │                                       │                       │
└───────────────────────┘                                       └───────────────────────┘

相关文档:

Brave 是什么

GitHub 仓库: https://github.com/openzipkin...

Brave is a distributed tracing instrumentation library.

翻译: Brave 是分布式链路追踪的埋点库。

instrumentation 这个单词本意是"仪器、仪表、器乐谱写",为了更加便于理解,这里我翻译为"埋点"。埋点的意思就是在程序的关键位置(即上面介绍的各个 annotation)做一些记录。

在 GitHub 仓库的 instrumentation 目录中,可以看到官方已经提供了非常多的 instrumentation。

另外在 https://zipkin.io/pages/trace... 文档中,还有其他非 Java 语言的 instrumentation 以及非官方提供的 instrumentation,可以根据需要来选择。其他 instrumentation 本文不做介绍,本文重点是 Zipkin 官方提供的 Java 语言 instrumentation : Brave 。

Spring MVC 项目配置 Brave

本文以 Web 服务为例,不涉及像 Dubbo 这样的 RPC 服务。

假设现有一个 Spring MVC 项目想要对接 Zipkin,需要使用 Brave 埋点,并将相关数据提交到 Zipkin 服务上。

Maven 依赖管理

首先加入一个 dependencyManagement,这样就不需要在各个依赖包中添加版本号了:

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.zipkin.brave</groupId>
        <artifactId>brave-bom</artifactId>
        <version>5.11.2</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
</dependencyManagement>

最新版本号可以在这里查看:
https://mvnrepository.com/art...

需要注意的是,不同版本配置方法会略有差异,具体可以参考官方文档。本文使用的 Brave 版本号为 5.11.2

创建 Tracing 对象

添加依赖:

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-context-slf4j</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-sender-okhttp3</artifactId>
</dependency>

下面提供了两种配置方式(Java 配置方式XML 配置方式)创建 Tracing 对象,需要根据项目的实际情况选择其中一种。

Java 配置方式

如果现有的项目是 Spring Boot 项目或者非 XML 配置的 Spring 项目,可以采用这种方式。

@Configuration
public class TracingConfiguration {

    @Bean
    public Tracing tracing() {
        Sender sender = OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans");
        Reporter<Span> spanReporter = AsyncReporter.create(sender);

        Tracing tracing = Tracing.newBuilder()
                .localServiceName("my-service")
                .spanReporter(spanReporter)
                .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder()
                        .addScopeDecorator(MDCScopeDecorator.get()).build())
                .build();
        return tracing;
    }
}

XML 配置方式

如果现有项目是采用 XML 配置的 Spring 项目,可以采用这种方式。

相对于 Java 配置方式,需要多添加一个 brave-spring-beans 依赖:

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-spring-beans</artifactId>
</dependency>

该模块提供了一系列 Spring FactoryBean,用于通过 XML 来创建对象:

<bean id="sender" class="zipkin2.reporter.beans.OkHttpSenderFactoryBean">
    <property name="endpoint" value="http://localhost:9411/api/v2/spans"/>
</bean>

<bean id="correlationScopeDecorator" class="brave.spring.beans.CorrelationScopeDecoratorFactoryBean">
    <property name="builder">
        <bean class="brave.context.slf4j.MDCScopeDecorator" factory-method="newBuilder"/>
    </property>
</bean>

<bean id="tracing" class="brave.spring.beans.TracingFactoryBean">
    <property name="localServiceName" value="my-service"/>
    <property name="spanReporter">
        <bean class="zipkin2.reporter.beans.AsyncReporterFactoryBean">
            <property name="sender" ref="sender"/>
        </bean>
    </property>
    <property name="currentTraceContext">
        <bean class="brave.spring.beans.CurrentTraceContextFactoryBean">
            <property name="scopeDecorators" ref="correlationScopeDecorator"/>
        </bean>
    </property>
</bean>

代码分析

上面两种方式本质上是一样的,都是创建了一个 Tracing 对象。

该对象是单实例的,如果想要在其他地方获取到这个对象,可以通过静态方法 Tracing tracing = Tracing.current() 来获取。

Tracing 对象提供了一系列 instrumentation 所需要的工具,例如 tracing.tracer() 可以获取到 Tracer 对象,Tracer 对象的作用后面会有详细介绍。

创建 Tracing 对象一些相关属性:

  • localServiceName 服务的名称
  • spanReporter 指定一个 Reporter<zipkin2.Span> 对象作为埋点数据的提交方式,这里通常会使用静态方法 AsyncReporter.create(Sender sender) 来创建一个 AsyncReporter 对象,当然如果有特殊需求也可以自己实现 Reporter 接口来自定义提交方式。创建 AsyncReporter 对象需要提供一个 Sender,下面列出了一些官方提供的 Sender 可供选择:

    • zipkin-sender-okhttp3 使用 OkHttp3 提交,使用方法:sender = OkHttpSender.create("http://localhost:9411/api/v2/spans"),本文中的示例使用的就是这种方式
    • zipkin-sender-urlconnection 使用 Java 自带的 java.net.HttpURLConnection 提交,使用方法:sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans")
    • zipkin-sender-activemq-client 使用 ActiveMQ 消息队列提交,使用方法:sender = ActiveMQSender.create("failover:tcp://localhost:61616")
    • zipkin-sender-kafka 使用 Kafka 消息队列提交,使用方法:sender = KafkaSender.create("localhost:9092")
    • zipkin-sender-amqp-client 使用 RabbitMQ 消息队列提交,使用方法:sender = RabbitMQSender.create("localhost:5672")
  • currentTraceContext 指定一个 CurrentTraceContext 对象来设置 TraceContext 对象的作用范围,通常会使用 ThreadLocalCurrentTraceContext,也就是用 ThreadLocal 来存放 TraceContextTraceContext 包含了一个 trace 的相关信息,例如 traceId。

    由于在 Spring MVC 应用中,一个请求的业务逻辑通常在同一个线程中(暂不考虑异步 Servlet)。一个请求内部的所有业务逻辑应该共用一个 traceId,自然是把 TraceContext 放在 ThreadLocal 中比较合理。这也意味着,默认情况下 traceId 只在当前线程有效,跨线程会失效。当然,跨线程也有对应的方案,本文后续会有详细介绍。

  • CurrentTraceContext 中可以添加 ScopeDecorator ,通过 MDC (Mapped Diagnostic Contexts) 机制关联一些日志框架:

    以 Logback 为例(本文中案例使用的方式),可以配置下面的 pattern 在日志中输出 traceId 和 spanId:

    <pattern>%d [%X{traceId}/%X{spanId}] [%thread] %-5level %logger{36} - %msg%n</pattern>

Spring MVC 埋点

添加依赖:

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-spring-webmvc</artifactId>
</dependency>

创建 HttpTracin 对象

首先创建 HttpTracing 对象,用于 HTTP 协议链路追踪。

Java 配置方式:

@Bean
public HttpTracing httpTracing(Tracing tracing){
    return HttpTracing.create(tracing);
}

XML 配置方式:

<bean id="httpTracing" class="brave.spring.beans.HttpTracingFactoryBean">
    <property name="tracing" ref="tracing"/>
</bean>

添加 DelegatingTracingFilter

DelegatingTracingFilter 用于处理外部调用的 HTTP 请求,记录 sr(Server Receive) 和 ss(Server Send) 两个 annotation。

非 Spring Boot 项目可以在 web.xml 中添加 DelegatingTracingFilter

<filter>
  <filter-name>tracingFilter</filter-name>
  <filter-class>brave.spring.webmvc.DelegatingTracingFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>tracingFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

如果是 Spring Boot 项目可以用 FilterRegistrationBean 来添加 DelegatingTracingFilter

@Bean
public FilterRegistrationBean delegatingTracingFilterRegistrationBean() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new DelegatingTracingFilter());
    registration.setName("tracingFilter");
    return registration;
}

到此,Spring MVC 项目已经完成了最基本的 Brave 埋点和提交 Zipkin 的配置。如果有现有的 Zipkin 服务,将创建 OkHttpSender 提供的接口地址换成实际地址,启动服务后通过 HTTP 请求一下服务,就会在 Zipkin 上找到一个对应的 trace。

其他 instrumentation 介绍

由于每个服务内部还会调用其他服务,例如通过 HTTP 调用外部服务的 Api、连接远程数据库执行 SQL,此时还需要用到其他 instrumentation。

由于篇幅有限,下面仅介绍几个常用的 instrumentation。

brave-instrumentation-mysql

brave-instrumentation-mysql 可以为 MySQL 上执行的每条 SQL 语句生成一个 span,用于分析 SQL 的执行时间。

添加依赖:

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-mysql</artifactId>
</dependency>

使用方法:在 JDBC 连接地址末尾加上参数 ?statementInterceptors=brave.mysql.TracingStatementInterceptor 即可。

该模块用于 mysql-connector-java 5.x 版本,另外还有 brave-instrumentation-mysql6brave-instrumentation-mysql8 可分别用于 mysql-connector-java 6+ 和 mysql-connector-java 8+ 版本。

brave-instrumentation-okhttp3

brave-instrumentation-okhttp3 用于 OkHttp 3.x,在通过 OkHttpClient 请求外部 API 时,生成 span,并且通过 B3 传播规范将链路信息传递给被调用方。

添加依赖:

<dependency>
    <groupId>io.zipkin.brave</groupId>
    <artifactId>brave-instrumentation-okhttp3</artifactId>
</dependency>

使用方法:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .dispatcher(new Dispatcher(
                        httpTracing.tracing().currentTraceContext()
                                .executorService(new Dispatcher().executorService())
                ))
                .addNetworkInterceptor(TracingInterceptor.create(httpTracing))
                .build();

如果你使用的 HTTP 客户端库不是 OkHttp 而是 Apache HttpClient 的话,可以使用 brave-instrumentation-httpclient

更多玩法

获取当前 traceId 和 spanId

Span currentSpan = Tracing.currentTracer().currentSpan(); // 获取当前 span
if (currentSpan != null) {
    String traceId = currentSpan.context().traceIdString();
    String spanId = currentSpan.context().spanIdString();
}

自定义 tag

可将业务相关的信息写入 tag 中,方便在查看调用链信息时关联查看业务相关信息。

Span currentSpan = Tracing.currentTracer().currentSpan(); // 获取当前 span
if (currentSpan != null) {
    currentSpan.tag("biz.k1", "v1").tag("biz.k2", "v2");
}

创建新 span

如果使用了某些组件访问外部服务,找不到官方或开源的 instrumentation,或者有一个本地的耗时任务,也想通过创建一个 span 来记录任务的运行时间和结果,可以自己创建一个新的 span。

ScopedSpan span = Tracing.currentTracer().startScopedSpan("span name");
try {
    // 访问外部服务 或 本地耗时任务
} catch (Exception e) {
    span.error(e); // 任务出错
    throw e;
} finally {
    span.finish(); // 必须记得结束 span
}

下面是另外一种方式,这种方式提供了更多的特性:

Tracer tracer = Tracing.currentTracer();
Span span = tracer.nextSpan().name("span name").start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) { // SpanInScope 对象需要关闭
    // 访问外部服务 或 本地耗时任务
} catch (Exception e) {
    span.error(e); // 任务出错
    throw e;
} finally {
    span.finish(); // 必须记得结束 span
}

跨线程追踪

使用包装过的 Runnable 和 Callable 对象

Runnable runnable = ...; // 原始的 Runnable 对象
Runnable tracingRunnable = Tracing.current().currentTraceContext().wrap(runnable); // 包装过的 Runnable 对象

同样的方式也可以使用于 Callable 对象。

使用包装过的线程池

ExecutorService service = ....;
ExecutorService proxiedService = tracing.currentTraceContext().executorService(service); 

对接其他分布式追踪系统

除 Zipkin 之外,还有很多优秀的开源或商业的分布式链路追踪系统。其中一部分对 Zipkin 协议做了兼容,如果不想使用 Zipkin 也是可以尝试一下其他的分布式链路追踪系统。

关注我的公众号

扫码关注


叉叉哥
3.8k 声望60 粉丝