概述
链路追踪的基本功能
- 分布式环境下链路追踪
- Timing 信息
- 定位链路
- 信息收集和展示
Sleuth
Sleuth 的功能
Sleuth 的最核心功能就是提供链路追踪,在一个用户请求发起到结束的整个过程中,这个Request经过的所有服务都会被梳理出来:
上图是一个由用户 X 请求发起的,穿过多个服务的分布式系统,A、B、C、D、E 表示不同的子系统或处理过程。在这个图中, A 是前端,B、C 是中间层、D、E 是 C 的后端。这些子系统通过 rpc 协议连接,例如 gRPC。
一个简单实用的分布式链路追踪系统的实现,就是对服务器上每一次请求以及响应收集跟踪标识符(message identifiers)和时间戳(timestamped events)。
分布式服务的跟踪系统需要记录在一次特定的请求后系统中完成的所有工作的信息。用户请求可以是并行的,同一时间可能有大量的动作要处理,一个请求也会经过系统中的多个服务,系统中时时刻刻都在产生各种跟踪信息,必须将一个请求在不同服务中产生的追踪信息关联起来。
借助 Sleuth 的链路追踪能力,我们还可以完成一些其他的任务,比如说:
- 线上故障定位:结合Tracking ID寻找上下游链路中所有的日志信息(这一步还需要借助一些其他开源组件,后面会有这部分的Demo)
- 依赖分析梳理:梳理上下游依赖关系,理清整个系统中所有微服务之间的依赖关系
- 链路优化:比如说目前我们有三种途径可以导流到下单接口,通过对链路调用情况的统计分析,我们可以识别出转化率最高的业务场景,从而为以后的产品设计提供指导意见。
- 性能分析:梳理各个环节的时间消耗,找到性能瓶颈,为性能优化、软硬件资源调配指明方向
Sleuth 的设计理念
从上图我们可以看出,Sleuth 采用底层 Log 系统的方式实现业务埋点。
哪些数据需要埋点?
每一个微服务都有自己的Log组件(slf4j,lockback等各不相同),当我们集成了Sleuth之后,它便会将链路信息传递给底层Log组件,同时Log组件会在每行Log的头部输出这些数据,这个埋点动作主要会记录两个关键信息:
- 链路ID: 当前调用链的唯一ID,在这次调用请求开始到结束的过程中,所有经过的节点都拥有一个相同的链路ID
- 单元ID: 在一次链路调用中会访问不同服务器节点上的服务,每一次服务调用都相当于一个独立单元,也就是说会有一个独立的单元ID。同时每一个独立单元都要知道调用请求来自哪里(就是对当前服务发起直接调用的那一方的单元ID,我们记为Parent ID)
比如这里服务A是起始节点,所以它的Event ID(单元ID)和Trace ID(链路ID)相同,而服务B的前置节点就是A节点,所以B的Parent Event就指向A的Event ID。而C在B的下游,所以C的Parent就指向B。A、B和C三个服务都有同一个链路ID,但是各自有不同的单元ID。
数据埋点之前要解决的问题
看起来创建埋点数据是件很容易的事儿,但是想让这套方案在微服务集群环境下生效,我们还需要先解决两个核心问题:
- Log系统集成:如何让埋点信息加入到业务Log中?
- **埋点信息的传递:**我们知道SpringCloud中的调用都是通过HTTP请求来传递的,那么上游调用方是如何将链路ID等信息传入到下游的呢?
Log 系统集成
我们需要把链路追踪信息加入到业务Log中,这些业务Log是我们研发人员写在具体服务里的,而不是Sleuth单独打印的log,因此Sleuth需要找到一个合适的切入点,让底层Log组件可以获取链路信息,并且我们的业务代码还不需要做任何改动。
如果有对Log框架做过深度定制的同学可能一下就能想到实现方式,就是使用MDC + Format Pattern的方式输出信息,我们先来看一下Log组件打印信息到文件的过程:
当我们使用log.info() 打印日志的时候,Log组件会将“写入”动作封装成一个LogEvent 事件,而这个事件的具体表现形式由 Log Format 和 MDC 共同控制,Format决定了Log的输出格式,而MDC决定了输出什么内容。
Log Format Pattern
Log组件定义了日志输出格式,这和我们平时使用“String.format”的方式差不多,集成了Sleuth后的Log输出格式是下面这个样子:
"%5p [sleuth-traceA,%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]"
我们可以发现上面有几个X 开头的占位符,这就是我们需要写入Log的链路追踪信息了。至于这几个符号分别对应链路信息的哪部分,下文会详细介绍。
MDC
MDC是通过InheritableThreadLocal 来实现的,它可以携带当前线程的上下文信息。它的底层是一个Map结构,存储了一系列Key-Value的值。Sleuth就是借助Spring的AOP机制,在方法调用的时候配置了切面,将链路追踪数据加入到了MDC中,这样在打印Log的时候,就能从MDC中获取这些值,填入到Log Format中的占位符里。
由于MDC基于InheritableThreadLocal而不是ThreadLocal实现,因此假如在当前线程中又开启了新的子线程,那么子线程依然会保留父线程的上下文信息。
源代码可以参考logback组件中LogEvent 类的prepareForDeferredProcessing 方法,了解MDC和Log Format是如何工作的。建议在打印log的地方打一个断点,本地启动项目后发起一次调用,然后一路跟进去一看便知。
调用链路数据模型 Trace,Span,Annotation
Span
它标识了 Sleuth 下面一个基本工作单元,每个单元都有一个独一无二的ID。比如服务A发起对服务B的调用,这个事件就可以看作一个独立单元,它生成了一个独立的ID。
Span 不单单只是一个ID,它还包含一些其他信息,比如事件戳,它标识了一个事件从开始到结束的时间,我们可以用这个信息来统计接口的执行时间。每个 Span 还有一系列特殊的“标记”,也就是接下来要介绍的 “Annotation”,它标识了这个 Span 在执行过程中发起的一些特殊事件。
Trace
实际场景中,我们需要知道某次请求调用的情况,所以只有spanid还不够,得为每次请求做个唯一标识,这样才能根据标识查出本次请求调用的所有服务,它就是从头到尾贯穿整个调用链的ID,我们叫它 Trace ID,不管调用链路中途访问了多少个服务节点,在每个节点的 log 中都会打印同一个 Trace ID。
Annotation
一个 Span 可以包含多个 Annotation,每个 Annotation 表示一个特殊事件,比如:
- Client Sent简称cs,客户端发起调用请求到服务端。
- Server Received简称sr,指服务端接收到了客户端的调用请求。
- Server Sent简称ss,指服务端完成了处理,准备将信息返给客户端。
- Client Received简称cr,指客户端接收到了服务端的返回信息。
每个 Annotation 同样有一个时间戳字段,这样我们就能计算一个 Span 内部每个事件的起始和结束时间。
用一张图表示 Trace、Span 和 Annotation 的关系:
上面的图中调用了两个接口Server 1和Service 2,整个调用过程的所有Span都有相同的Trace ID,但每一个Span都有独立的Span ID。其中Service 1对Service 2的调用分为两个Span,蓝色Span的时间跨度从调用发起直到调用结束,分别记录了4个特殊事件(对应客户端和服务端对Request和Response的传输)。绿色Span主要针对Service 2内部业务的处理,因此我们在Service 2中打印的日志将会带上绿色Span的ID。
服务节点间的ID传递
我们知道了Trace ID和Span ID,眼下的问题就是如何在不同服务节点之间传递这些ID。我想这一步大家很容易猜到是怎么做的,因为在Eureka的服务治理下所有调用请求都是基于HTTP的,那我们的链路追踪ID也一定是HTTP请求中的一部分。可是把ID加在HTTP哪里好呢?Body里可以吗?NoNoNo,一来GET请求压根就没有Body,二来加入Body还有可能影响后台服务的反序列化。那加在URL后面呢?似乎也不妥,因为某些服务组件对URL的长度可能做了限制(比如Nginx可以设置最大URL长度)。
那剩下的只有Header了!Sleuth正是通过Filter向Header中添加追踪信息,我们来看下面表格中Header Name和Trace Data的对应关系:
HTTP Header Name | Trace Data | 说明 |
---|
X-B3-TraceId | Trace ID | 链路全局唯一ID | X-B3-SpanId | Span ID | 当前Span的ID | X-B3-ParentSpanId | Parent Span ID | 前一个Span的ID | X-Span-Export | Can be exported for sampling or not | 是否可以被采样 |
Zipkin
Zipkin 能干什么?
上文讲了 Sleuth 的最核心功能就是提供链路追踪,数据采样、日志埋点和Log系统集成,但是没有什么页面可以展示出来,没有信息汇聚的能力,不能够直观的对整个集群的调用链路进行分析。
Zipkin是一套分布式实时数据追踪系统,它主要关注的是时间维度的监控数据,比如某个调用链路下各个阶段所花费的时间,同时还可以从可视化的角度帮我们梳理上下游系统之间的依赖关系。
Zipkin 的核心功能
Zipkin的主要作用是收集Timing维度的数据,以供查找调用延迟等线上问题。所谓Timing其实就是开始时间+结束时间的标记,有了这两个时间信息,我们就能计算得出调用链路每个步骤的耗时。Zipkin的核心功能有以下两点:
- 数据收集: 聚合客户端数据
- 数据查找: 通过不同维度对调用链路进行查找
Zipkin分为服务端和客户端,服务端是一个专门负责收集数据、查找数据的中心Portal,而每个客户端负责把结构化的Timing数据发送到服务端,供服务端做索引和分析。这里我们重点关注一下“Timing数据”到底用来做什么,前面我们说过Zipkin主要解决调用延迟情况的线上排查,它通过收集一个调用链上下游所有工作单元的独立用时,Zipkin就能知道每个环节在服务总用时中所占的比重,再通过图形化界面的形式,让开发人员知道性能瓶颈出在哪里。
Zipkin提供了多种维度的查找功能用来检索Span的耗时,最直观的是通过Trace ID查找整个Trace链路上所有Span的前后调用关系和每阶段的用时,还可以根据Service Name或者访问路径等维度进行查找。
Zipkin 的组件
-
Collector:很多人以为Collector是一个客户端组件,其实它是Zipkin Server的守护进程,用来验证客户端发送来的链路数据,并在存储结构中建立索引。守护进程就是指一类用于执行特定任务的后台进程,它独立于Zipkin Server的控制终端,一直等待接收客户端数据。 -
Storage:Zipkin支持ElasticSearch和MySQL等存储介质用来保存链路信息 -
Search Engine:提供基于JSON API的接口来查找信息 -
Dashboard:一个大盘监控页面,后台调用Search Engine来获取展示信息。大家如果本地启动Zipkin会每次刷新主页后系统日志会打印Error信息,这个是Zipkin的一个小问题,直接跳过即可。
|