分布式调用链Sleuth服务应用及解析

1. 调用链初探

1.1 调用链说明

微服务结构从广义上来说属于分布式架构,在划分微服务节点时,我们通常按业务来区分。每一个逻辑上的业务对应一个服务单元,每个服务单元包含一个至多个服务节点。但是随着业务的复杂度越来越高,服务单元部署的越来越多,服务单元之间的耦合性变得不可控,问题定位也就越来越难。假如一个服务单元A,需要调用多个服务单元,多个服务单元又调用了其他服务单元,那么就产生了一系列的技术痛点:

  • 如何记录一条请求链路的调用流程、调用顺序、每个服务单元的处理时间;
  • 异常日志需要搜索线上机器位置,定位哪里出错的时间长;
  • 一些异常错误被大事化小、小事化了,最后不了了之,最后甩锅给了客户端、网络问题、机器问题等。

分布式调用链路追踪就是为了解决上述问题所提出的,目前,市面上比较热门的分布式调用链解决方案主要有如下几种:

解决方案名 贡献者 简介说明
Zipkin Twitter 开源,Spring Cloud Sleuth依赖其实现,无代码入侵
Dapper Google 定义OpenTracing标准,未开源
CAT 美团点评 开源,完全国产
EagleEye 阿里中间件 未开源
Pinpoint Naver 成熟度很高的APM调用链监控项目,开源
SkyWalking SkyWalking管理委员会/Apache基金会 开源

1.2 方案选择考量

如何选择一款分布式调用链解决方案,笔者总结应考虑如下几点:

  • 代码入侵性,作为一个与业务无关的组件,应对具体的业务透明,尽可能减少入侵业务系统,降低开发者的负担;
  • 低损耗性,记录调用链本身会带来系统的性能损耗,在生产环境中,需要组件支持可配置的采样率来应对调用链组件可能产生的损耗问题;
  • 可扩展性,一个优秀的调用链系统必须支持分布式部署,具备良好的可扩展性;
  • 与当前已有组件的可兼容性,调用链系统,应当适合当前已有的诸如MQ、数据库等存储组件,减少后续运维开发的负担,不能为了只上一个调用链系统,而要部署一大堆新的环境;
  • 计算和展示,汇总各个应用程序的调用链日志后,需要完善的查询和展示工具,来对调用链进行分析;

综合以上几点因素,加上笔者本身在生产环境下使用Spring Boot构建项目居多,最终选择了Zipkin作为我们的分布式链路追踪的解决方案。Sleuth是Spring Cloud提供的,包装了Zipkin的组件包。本文接下来的内容,主要介绍了Sleuth实现Zipkin的细节,以及Zipkin的应用案例。如果你的项目也是基于Spring Boot构建,那直接推荐你使用Sleuth做链路追踪,这样无需改动目前代码,可直接上手。

2. Sleuth与Zipkin

2.1 Sleuth与Zipkin关系说明

Spring Cloud提供了Sleuth作为日志收集工具包,封装了Dapper的标准和Zipkin Client等组件,统一为Spring Cloud应用提供了分布式链路追踪解决方案。本博文之后的代码内容全部依赖于:Spring Cloud + Sleuth + Zipkin + openjdk1.8。

2.2 OpenTracing术语介绍

Zipkin的术语代码级别的定义在这里查看

2.2.1 Annotation

Annotation用来记录一次调用产生的事件:

  • cs,Client Send,客户端发起一个请求;
  • sr,Server Received,服务端获得请求并准备开始处理它;
  • ss,Server Send,服务端处理完请求并返回;
  • cr,Client Receive,客户端收到服务端的返回内容;

有了上面这四个Annotation,我们就可以提取一次请求链路的关键事件、时间戳、网络延迟等信息。

2.2.2 Span

工作单元,你可以理解为对应你的服务单元,调用链每经过一个服务单元,都会产生一个唯一的64位SpanID。

2.2.3 Trace

调用链形成的路径,一系列Span组成的树状结构。

2.2.4 流程分析

一次典型的调用链产生过程大致为:
image

  • 用户新请求到Service1,本次请求没有trace和span信息,Service1初始化生成自己的traceId=X,spanId=A;
  • Service1继续向Service2发请求,Service2根据发来请求中header信息,提取traceId=X,并初始化spanId=B;
  • Service2继续向Service3发请求,Service3根据请求中的header,提取traceId=X,并初始化spanId=C;

这样,我们就可以提取出一个完整的调用链路,路径记录为traceId=X,它经过了三个服务单元A->B->C,且调用深度为3。链路数据做持久化之后,X作为链路的唯一性ID,可以用来查询链路数据。

3. Sleuth源码分析

Sleuth提供了如下一些组件,和目前一些应用做融合:
image

如果你的项目中,用到了图中的组件,都可以无缝支持Sleuth。

3.1 自动配置

Sleuth使用起来比较方便,只需要引入相应的包,即可以在应用程序启动的时候自动配置Sleuth,Spring Boot在启动时,会扫描sleuth-core下的spring.factories文件,这里面配置了需要自动装配的bean。Sleuth的自动配置代码在org.springframework.cloud.sleuth.autoconfig包下。我们首先看TraceAutoConfiguration.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true)
@EnableConfigurationProperties(SleuthProperties.class)
public class TraceAutoConfiguration {
/**
* 定义Tracing
*/
@Bean
@ConditionalOnMissingBean
// NOTE: stable bean name as might be used outside sleuth
Tracing tracing(@LocalServiceName String serviceName, Propagation.Factory factory,
CurrentTraceContext currentTraceContext, Sampler sampler,
ErrorParser errorParser, SleuthProperties sleuthProperties,
@Nullable List<Reporter<zipkin2.Span>> spanReporters) {
Tracing.Builder builder = Tracing.newBuilder().sampler(sampler)
.errorParser(errorParser)
.localServiceName(StringUtils.isEmpty(serviceName) ? DEFAULT_SERVICE_NAME
: serviceName)
.propagationFactory(factory).currentTraceContext(currentTraceContext)
.spanReporter(new CompositeReporter(this.spanAdjusters,
spanReporters != null ? spanReporters : Collections.emptyList()))
.traceId128Bit(sleuthProperties.isTraceId128())
.supportsJoin(sleuthProperties.isSupportsJoin());
for (FinishedSpanHandler finishedSpanHandlerFactory : this.finishedSpanHandlers) {
builder.addFinishedSpanHandler(finishedSpanHandlerFactory);
}
for (TracingCustomizer customizer : this.tracingCustomizers) {
customizer.customize(builder);
}
return builder.build();
}
@Bean(name = TRACER_BEAN_NAME)
@ConditionalOnMissingBean
Tracer tracer(Tracing tracing) {
return tracing.tracer();
}
/**
* 生成一个默认的trace采集器bean,默认永远不采集
*/
@Bean
@ConditionalOnMissingBean
Sampler sleuthTraceSampler() {
return Sampler.NEVER_SAMPLE;
}
/**
* 生成一个span名称生成器
*/
@Bean
@ConditionalOnMissingBean
SpanNamer sleuthSpanNamer() {
return new DefaultSpanNamer();
}
}

3.2 Web配置

当启动Web服务时,TraceWebAutoConfiguration.javaTraceWebFluxAutoConfiguration.java会进行一些与Web相关的自动配置:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.web.enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnBean(Tracing.class)
@AutoConfigureAfter(TraceWebAutoConfiguration.class)
public class TraceWebFluxAutoConfiguration {
@Bean
public TraceWebFilter traceFilter(BeanFactory beanFactory) {
return new TraceWebFilter(beanFactory);
}
}

其中,TraceWebFilter.java是trace过滤器,对每个http请求过滤,在Spring MVC触发之前,从Request Header中读取trace信息,生成本服务单元的span,没有没有找到相应的header,则初始化新生成一个span。其中初始化Span的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private Span findOrCreateSpan(Context c) {
Span span;
if (c.hasKey(TraceContext.class)) {
TraceContext parent = c.get(TraceContext.class);
span = this.tracer.newChild(parent).start();
if (log.isDebugEnabled()) {
log.debug("Found span in reactor context" + span);
}
}
else {
if (this.attrSpan != null) {
span = this.attrSpan;
if (log.isDebugEnabled()) {
log.debug("Found span in attribute " + span);
}
}
else {
span = this.handler.handleReceive(
new WrappedRequest(this.exchange.getRequest()));
if (log.isDebugEnabled()) {
log.debug("Handled receive of span " + span);
}
}
this.exchange.getAttributes().put(TRACE_REQUEST_ATTR, span);
}
return span;
}

3.3 Zuul网关配置

和网关组件有关的配置,在org.springframework.cloud.sleuth.instrument.zuul包下,在TracePostZuulFilter.java中,配置了网关的后置过滤器,用来传递trace信息给下游。Sleuth通过Request Header传送trace信息,主要包装以下几个参数到header:

  • x-b3-traceid
  • x-b3-spanid
  • x-b3-parentspanid
  • x-b3-sampled

3.4 RPC自动配置

Sleuth支持RPC的自动配置代码,在TraceRpcAutoConfiguration.javaTraceGrpcAutoConfiguration.java
其中,TraceRpcAutoConfiguration中定义了普通了RpcTracing,TraceGrpcAutoConfiguration中定义了GrpcTracing。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@ConditionalOnClass({ GrpcTracing.class, GRpcGlobalInterceptor.class })
@ConditionalOnProperty(value = "spring.sleuth.grpc.enabled", matchIfMissing = true)
@ConditionalOnBean(RpcTracing.class)
@AutoConfigureAfter(TraceRpcAutoConfiguration.class)
public class TraceGrpcAutoConfiguration {
@Bean
public GrpcTracing grpcTracing(RpcTracing rpcTracing) {
return GrpcTracing.create(rpcTracing);
}
// Register a global interceptor for both the server
@Bean
@GRpcGlobalInterceptor
ServerInterceptor grpcServerBraveInterceptor(GrpcTracing grpcTracing) {
return grpcTracing.newServerInterceptor();
}
// This is wrapper around gRPC's managed channel builder that is spring-aware
@Bean
@ConditionalOnMissingBean(SpringAwareManagedChannelBuilder.class)
public SpringAwareManagedChannelBuilder managedChannelBuilder(
Optional<List<GrpcManagedChannelBuilderCustomizer>> customizers) {
return new SpringAwareManagedChannelBuilder(customizers);
}
@Bean
GrpcManagedChannelBuilderCustomizer tracingManagedChannelBuilderCustomizer(
GrpcTracing grpcTracing) {
return new TracingManagedChannelBuilderCustomizer(grpcTracing);
}
}

3.5 Log自动配置

Sleuth默认支持Slf4j日志组件,Slf4j中定义的MDC(Mapped Diagnostic Contexts),通过MDC的put方法,可以实现用户自定义的日志输出。我们看一下org.springframework.cloud.sleuth.log.SleuthLogAutoConfiguration类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true)
@AutoConfigureBefore(TraceAutoConfiguration.class)
public class SleuthLogAutoConfiguration {
/**
* Configuration for Slfj4.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MDC.class)
@EnableConfigurationProperties(SleuthSlf4jProperties.class)
protected static class Slf4jConfiguration {
@Bean
@ConditionalOnProperty(value = "spring.sleuth.log.slf4j.enabled",
matchIfMissing = true)
static CurrentTraceContext.ScopeDecorator slf4jSpanDecorator(
SleuthProperties sleuthProperties,
SleuthSlf4jProperties sleuthSlf4jProperties) {
return new Slf4jScopeDecorator(sleuthProperties, sleuthSlf4jProperties);
}
}
}

Slf4jScopeDecorator中,MDC的配置方法为:

1
2
3
4
5
6
7
8
9
10
11
12
String traceIdString = currentSpan.traceIdString();
MDC.put("traceId", traceIdString);
String parentId = currentSpan.parentId() != null
? HexCodec.toLowerHex(currentSpan.parentId()) : null;
replace("parentId", parentId);
replace(LEGACY_PARENT_ID_NAME, parentId);
String spanId = HexCodec.toLowerHex(currentSpan.spanId());
MDC.put("spanId", spanId);
MDC.put(LEGACY_SPAN_ID_NAME, spanId);
String sampled = String.valueOf(currentSpan.sampled());
MDC.put("spanExportable", sampled);
MDC.put(LEGACY_EXPORTABLE_NAME, sampled);

在logback.xml 文件中通过配置pattern,就可以取出对应的属性值并打印(其实不用单独配置,logback已经帮你默认配置好了,在这里只是给大家传递log打印配置的概念):

1
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%X{parentId}][%X{spanId}][%X{spanExportable}][%thread]%-5level - %logger - %msg%n</pattern>

如果你没有做自定义log配置,默认的Sleuth+Slf4j的日志打印案例为:

1
2020-05-12 17:19:17.079 INFO [test-client-name,f9cf11d6048dce73,35b0b61343412bc6,true] 83396 --- [io-12111-exec-5] com.zhaoyh.controller.MainController : your logs...
  • test-client-name,即配置文件中的applicationName;
  • f9cf11d6048dce73,即traceId;
  • 35b0b61343412bc6,即spanId;
  • true/false,是否输出到Zipkin Server;

4. 使用总结

4.1 Sleuth使用的优势

  • 对Web、RPC、Log做了较好的封装,在很小的代码入侵条件下,实现了对调用链的拦截和保存;
  • 上手容易,Spring Cloud提供了Sleuth来封装Zipkin,对开发人员及其友好,对于基本的日志需求,只需要引入相关的pom即可;
  • 链路数据存储支持ES、MySQL、Cassandra、内存等,这些基本覆盖了我们的需求;
  • 社区活跃度较高,得益于Spring Cloud的广泛应用,Sleuth的用户也相应的较多;

4.2 Sleuth的不足

  • Sleuth没有提供全局的调用统计,即某个接口的吞吐量、平均时延等信息无法提供;
  • 没有提供接口报警机制,出现了有问题的接口调用,只能开发人员主动查询;

5. 参考文档

  1. Spring Cloud Sleuth
  2. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure
  3. Spring Cloud之分布式链路跟踪服务Sleuth
  4. Zipkin

以上内容就是关于分布式调用链Sleuth服务应用及解析的全部内容了,谢谢你阅读到了这里!

Author:zhaoyh