微服务架构下,服务之间的关系错综复杂。从调用一个 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
是被调用服务处理业务逻辑的耗时
然而,
sr
和ss
两个 annotation 依赖被调用方,如果被调用方没有相应的记录,例如下游服务没有对接 instrumentation 库,或者像执行一条 SQL 这样的场景,被调用方是一个数据库服务,不会记录sr
和ss
,那么这个 span 就只有cs
和cr
。
相关文档:
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
来存放TraceContext
。TraceContext
包含了一个 trace 的相关信息,例如 traceId。由于在 Spring MVC 应用中,一个请求的业务逻辑通常在同一个线程中(暂不考虑异步 Servlet)。一个请求内部的所有业务逻辑应该共用一个 traceId,自然是把
TraceContext
放在ThreadLocal
中比较合理。这也意味着,默认情况下 traceId 只在当前线程有效,跨线程会失效。当然,跨线程也有对应的方案,本文后续会有详细介绍。在
CurrentTraceContext
中可以添加ScopeDecorator
,通过 MDC (Mapped Diagnostic Contexts) 机制关联一些日志框架:- brave-context-slf4j SLF4J
- brave-context-log4j2 Log4J 2
- brave-context-log4j12 Log4J v1.2
以 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-mysql6 和 brave-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 也是可以尝试一下其他的分布式链路追踪系统。
- Jaeger 是 Uber 开源的一套分布式追踪系统,可以通过它的 9411 端口对接: https://www.jaegertracing.io/...
- SkyWalking 提供了 Zipkin receiver 来接收 Zipkin 格式的数据: https://github.com/apache/sky...
- 阿里云链路追踪 是阿里云提供的商业链路追踪系统,对接文档: https://help.aliyun.com/docum...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。