微服务架构下,服务之间的关系错综复杂。从调用一个 HTTP API 到最终返回结果,中间可能发生了多个服务间的调用。而这些被调用的服务,可能部署在不一样的服务器上,由不一样的团队开发,甚至可能使用了不一样的编程语言。在这样的环境中,排查性能问题或者定位故障就很麻烦。html
Zipkin 是一个分布式链路追踪系统(distributed tracing system)。它能够收集并展现一个 HTTP 请求从开始到最终返回结果之间完整的调用链。java
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:mysql
cs
Client Send,客户端发起请求的,这是一个 span 的开始sr
Server Receive,服务端收到请求开始处理ss
Server Send,服务端处理请求完成并响应cr
Client Receive,客户端收到响应,这个 span 到此结束记录了以上的时间点,就能够很容易分析出一个 span 每一个阶段的耗时:git
cr - cs
是整个流程的耗时sr - cs
以及 cr - ss
是网络耗时ss - sr
是被调用服务处理业务逻辑的耗时然而,sr
和 ss
两个 annotation 依赖被调用方,若是被调用方没有相应的记录,例以下游服务没有对接 instrumentation 库,或者像执行一条 SQL 这样的场景,被调用方是一个数据库服务,不会记录 sr
和 ss
,那么这个 span 就只有 cs
和 cr
。github
相关文档:web
当上游服务经过 HTTP 调用下游服务,如何将两个服务中的全部 span 串联起来,造成一个 trace,这就须要上游服务将 traceId 等信息传递给下游服务,而不能让下游从新生成一个 traceId。spring
Zipkin 经过 B3 传播规范(B3 Propagation),将相关信息(如 traceId、spanId 等)经过 HTTP 请求 Header 传递给下游服务:sql
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 │ │ │ └───────────────────┘ │ └───────────────────┘ │ └───────────────────┘ │ │ │ │ │ └───────────────────────┘ └───────────────────────┘
相关文档:数据库
GitHub 仓库: https://github.com/openzipkin...apache
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 。
本文以 Web 服务为例,不涉及像 Dubbo 这样的 RPC 服务。
假设现有一个 Spring MVC 项目想要对接 Zipkin,须要使用 Brave 埋点,并将相关数据提交到 Zipkin 服务上。
首先加入一个 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。
添加依赖:
<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 对象,须要根据项目的实际状况选择其中一种。
若是现有的项目是 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 配置的 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) 机制关联一些日志框架:
以 Logback 为例(本文中案例使用的方式),能够配置下面的 pattern 在日志中输出 traceId 和 spanId:
<pattern>%d [%X{traceId}/%X{spanId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
添加依赖:
<dependency> <groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-spring-webmvc</artifactId> </dependency>
首先建立 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
用于处理外部调用的 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; }
若是有兴趣的话能够看下 DelegatingTracingFilter
的源码,它本质上是一个 TracingFilter
的代理。TracingFilter
来源于 brave-instrumentation-servlet 模块。DelegatingTracingFilter
经过 Spring 容器中的 HttpTracing
对象建立了一个 TracingFilter
。相关代码在 DelegatingTracingFilter.java
54 行。
到此,Spring MVC 项目已经完成了最基本的 Brave 埋点和提交 Zipkin 的配置。若是有现有的 Zipkin 服务,将建立 OkHttpSender
提供的接口地址换成实际地址,启动服务后经过 HTTP 请求一下服务,就会在 Zipkin 上找到一个对应的 trace。
因为每一个服务内部还会调用其余服务,例如经过 HTTP 调用外部服务的 Api、链接远程数据库执行 SQL,此时还须要用到其余 instrumentation。
因为篇幅有限,下面仅介绍几个经常使用的 instrumentation。
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 用于 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。
Span currentSpan = Tracing.currentTracer().currentSpan(); // 获取当前 span if (currentSpan != null) { String traceId = currentSpan.context().traceIdString(); String spanId = currentSpan.context().spanIdString(); }
可将业务相关的信息写入 tag 中,方便在查看调用链信息时关联查看业务相关信息。
Span currentSpan = Tracing.currentTracer().currentSpan(); // 获取当前 span if (currentSpan != null) { currentSpan.tag("biz.k1", "v1").tag("biz.k2", "v2"); }
若是使用了某些组件访问外部服务,找不到官方或开源的 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 runnable = ...; // 原始的 Runnable 对象 Runnable tracingRunnable = Tracing.current().currentTraceContext().wrap(runnable); // 包装过的 Runnable 对象
一样的方式也可使用于 Callable
对象。
ExecutorService service = ....; ExecutorService proxiedService = tracing.currentTraceContext().executorService(service);
除 Zipkin 以外,还有不少优秀的开源或商业的分布式链路追踪系统。其中一部分对 Zipkin 协议作了兼容,若是不想使用 Zipkin 也是能够尝试一下其余的分布式链路追踪系统。