随着业务的发展, 系统规模也会变 得越来越大, 各微服务间的调用关系也变得越来越错综复杂。 通常一个由客户端发起的请 求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果, 在复杂的微服 务架构系统中, 几乎每一个前端请求都会形成一条复杂的分布式服务调用链路, 在每条链 路中任何一个依赖服务出现延迟过高或错误的时候都有可能引起请求最后的失败。这时候, 对于每个请求, 全链路调用的跟踪就变得越来越重要, 通过实现对请求调用的跟踪可以帮 助我们快速发现错误根源以及监控分析每条请求链路上的性能瓶颈等。
针对上面所述的分布式服务跟踪问题, Spring Cloud Sleuth 提供了 一 套完整的解决方 案。
只需在服务的 pom.xrnl 依赖管理中增加 spring-cloud-starter-sleuth 依赖
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-sleuth</artifactid>
</dependency>
假设我们现在有trace-1、trace-2和eureka-server三个微服务应用,并且trace-1、trace-2都关联eureka-server。
并且trace-1写一个接口,在接口中,调用trace-2,当我们请求trace-1的接口时,我们看到日志信息如下:
-- trace-1
INFO [trace-1, f410ab57afd5cl45, a9f2118fa2019684, false) 25028 --- [nio-9101-exec-l] ication$$EnhancerBySpringCGLIB$$d8228493 : ===<call trace-1>===
-- trace-2
INFO [trace-2, f 410ab57afd5cl45, e9a377dc2268bc29, false J 23112 --- [nio-9102-exec-l] ication$$EnhancerBySpringCGLIB$$e6cb4078 : ===<call trace-2>===
从上面的控制台输出内 容 中 , 我 们可 以看到多了 一 些形如[trace-1, f410ab57afd5c145, a9f2118fa2019684, false]的日志信息, 而这些元素正是实现分布式服务跟踪的重要组成部分, 每个值的含义如下所述。
• 第一个值: trace-1, 它记录了应用的名称,也就是 application.properties中 spring.application.name参数配置的属性。
• 第二个值: f410ab57afd5c145, Spring Cloud Sleuth生成的一个ID,称为Trace ID,它用来标识一条请求链路。 一条请求链路中包含一个TraceID, 多个SpanID。
• 第三个值: a9f2118fa2019684, Spring Cloud Sleuth生成的另外一个 ID, 称为Span ID, 它表示一个基本的工作单元, 比如发送一个HTTP请求。
• 第四个值: false, 表示是否要将该信息输出到Zipkin等服务中来收集和展示 。
上面四个值中的Trace ID和SpanID是Spring Cloud Sleuth实现分布式服务跟踪的核心。 在一次服务请求链路的调用过程中, 会保待并传递同一个Trace ID, 从而将整个分布于不同微服务进程中的请求跟踪 信息串联起来。 以上面输出内容为例, trace-1 和trace-2同属于一个前端服务请求来源,所以它们的TraceID是相同的,处于同一条请求链路中。
跟踪原理
分布式系统中的服务跟踪在理论上并不复杂, 它主要包括下面两个关键点:
为了实现请求跟踪, 当请求发送到分布式系统的入口端点时, 只需要服务跟踪框架为该请求创建一个唯一的跟踪标识, 同时在分布式系统内部流转的时候,框架始终保待传递 该唯一标识, 直到返回给请求方为止, 这个唯一标识就是前文中提到的Trace ID。 通过TraceID的记录, 我们就能将所有请求过程的日志关联起来。
为了统计各处理单元的时间延迟, 当请求到达各个服务组件时, 或是处理逻辑到达某个状态时, 也通过一个唯一标识来标记它的开始、 具体过程以及结束, 该标识就是前文中提到的SpanID。 对于每个Span来说, 它必须有开始和结束 两个节点, 通过记录开始 Span和结束Span的时间戳,就能统计出该Span的时间延迟,除了时间戳记录之外, 它还可以包含一些其他元数据, 比如事件名称、 请求信息等。
通过 在工程中 引入 spring-cloud-starter-sleuth 依赖之后, 它会自动为当前应用构建起各通信通道的跟踪机制, 比如:
通过诸如 RabbitMQ、 Kafka C 或者其他任何 Spring Cloud Stream 绑定器实现的消息中间件) 传递的请求。
通过 Zuul 代理传递的请求。
通过 RestTemplate 发起的请求。
在快速入门示例中, 由于 trace -1对 trace-2 发起的请求是通过 Res t Template 实现的, 所以 spring-cloud-starter-sleuth 组件会对该请求进行处理 。 在发送到 trace-2 之前, Sleuth 会 在该请求的 Header 中增加实现 跟踪需要的重要信息,主要有下面 这几个(更多关千头信息的定义可以通过查看 org.springframework.cloud. sleuth.Span 的源码获取)。
• X-B3-Traceld: 一 条请求链路 (Trace) 的唯 一 标识, 必需的值。
• X-B3-Spanld: 一 个工作单元 (Span) 的唯 一 标识, 必需的值。
• X-B3-ParentSpanld: 标识当前工作单元所属的上 一 个工作单元 , Root Span C 请求链 路的第 一 个工作单元) 的该值为空。
• X-B3-Sampled: 是否被抽样输出的标志, 1 表示需要被输出 , 0 表示不需要被输出 。
• X-Span-Name: 工作单元的名称。
可以通过对 trace-2 的实现做 一 些修改来输出这些头部信息, 具体如下:
@RequestMapping(value = "/trace-2", method = RequestMethod.GET)
public String七race(HttpServletRequest request) {
logger.info("===<call trace-2, Traceid={}, Spanid={}>===", request.getHeader("X-B3-Traceid"), request.getHeader("X-B3-Spanid"));
return "Trace";
}
}
抽样收集
通过Trace ID和Span ID已经实现了对分布式系统中的请求跟踪, 而记录的跟踪信息 最终会被分析系统收集起来, 并用来实现对分布式系统的监控和分析功能, 比如, 预警延 迟过长的请求链路、 查询请求链路的调用明细等。 此时, 我们在对接分析系统时就会碰到 一个问题:
分析系统在收集跟踪信息的时候, 需要收集多少跟踪信息才合适呢?
理论上来说, 我们收集的跟踪信息越多就可以越好地反映出系统的实际运行情况, 并给出更精准的预警和分析。 但是在高并发的分布式系统运行时, 大量的请求调用会产生海 量的跟踪日志信息, 如果收集过多的跟踪信息将会对整个分布式系统的性能造成 一 定的影 响, 同时保存大量的日志信息也需要不少的存储开销。 所以, 在 Sleuth 中采用了抽象收集 的方式来为跟踪信息打上收集标记, 也就是我们之前在日志信息中看到的第4个布尔类型 的值, 它代表了该信息是否要被后续的跟踪信息收集器获取和存储。
Sleuth 中的抽样收集策略是通过 Sampler 接口实现的, 它的定义如下:
public interface Sampler {
/**
* @return true if the span is not null and should be exported to the tracing system
*/
boolean isSarnpled(Span span);
}
通过实现 isSarnpled 方法, Spring Cloud Sleuth 会在产生跟踪信息的时候调用它来为 跟踪信息生成是否要被收集的标志。 需要注意的是, 即使 isSampled 返回了 false, 它 仅代表该跟踪信息不被输出到后续对接的远程分析系统(比如 Zipkin), 对于请求的跟踪活 动依然会进行, 所以我们在日志中还是能看到收集标识为 false 的记录。
默认情况下, Sleuth 会使用 PercentageBasedSarnpler 实现的抽样策略,以请求百 分比的方式配置和收集跟踪信息。 我们可以通过在 appli cati on.properti es 中配置 下面的参数对其百分比值进行设置, 它的默认值为0 .1, 代表收集10%的请求跟踪信息。
spring.sleuth.sampler.percentage=0.1
在开发调试期间, 通常会收集全部跟踪信息并输出到远程仓库, 我们可以将其值设置 为1, 或者也可以通过创建 AlwaysSarnpler 的 Bean (它实现的 isSarnpled 方法始终返 回 true) 来覆盖默认的 PercentageBasedSarnpler 策略, 比如:
@Bean
public AlwaysSampler defaultSampler () (
return new AlwaysSampler();
}
在实际使用时,通过与 Span 对象中存储信息的配合,我们可以根据实际情况做出更贴 近需求的抽样策略, 比如实现 一 个仅包含指定Tag的抽样策略:
public class TagSampler implements Sampler {
private String tag;
public TagSampler(String tag) {
this.tag= tag;
}
@Override
public boolean isSampled(Span span) (
return span.tags().get(tag) != null;
}
}
由于跟踪日志信息数据的价值往往仅在最近的 一 段时间内非常有用, 比如 一 周。 那么 我们在设计抽样策略时, 主要考虑在不对系统造成明显性能影响的情况下, 以在日志保留 时间窗内充分利用存储空间的原则来实现抽样策略。
与Logstash整合
通过之前的准备与整合,我们已经为trace-1和trace-2引入了Spring Cloud Sleuth的基础模块spring-cloud-s七arter-sleuth, 实现了在各个微服务的日志信息中添加跟踪信息的功能。 但是, 由于日志文件都离散地存储在各个服务实例的文件系统之上, 仅仅通过查看日志文件来分析我们的请求链路依然是一件相当麻烦的事, 所以我们还需要一些工具来帮助集中收集、 存储和搜索这些跟踪信息。 引入基于日志的分析系统是一个不错的选择, 比如ELK平台, 它可以轻松地帮助我们收集和存储这些跟踪日志, 同时在需要的时候我们也 可以根据Trace ID来轻松地搜索出对应请求链路 相关的明细日志。
ELK 平台主要由 E l asticSearch 、 Logstash和Kibana 三 个 开 源 工 具 组成 。
- ElasticSearch是一个开源分布式搜索引擎, 它的特点有: 分布式, 零配置, 自动发现, 索引自动分片, 索引副本机制,RESTful风格接口, 多数据源, 自动搜索负载等。
- Logstash是一个完全开源的工具, 它可以对日志进行收集、 过滤, 并将其存储供以后使用。
- Kibana 也是一个开源和免费的工具, 它可以为Logstash 和ElasticSearch提供日志分析友好的Web界面, 可以帮助汇总、 分析和搜索重要数据日志。
Spring Cloud Sleuth 在与ELK平台整合使用时, 实际上只要实现与负责日志收集的 Logstash完成数据对接即可, 所以我们需要为Logstash准备JSON格式的日志输出。 由于 Spring Boot 应用默认使用logback来记录日志,而Logstash自身也有对logback日志工具的 支持工具, 所以我们可以直接通过在logback的配置中增加对Logstash的Appender, 就能 非常方便地将日志转换成以JSON的格式存储和输出了。
下面我们来详细介绍 一 下在快速入门示例的基础上, 如何实现面向Logstash的日志输 出配置。
在pom.xml依赖中引入logstash丑ogback-encoder依赖, 具体如下:
net.logstash.logback logstash-logback-encoder 4.6 在工程/resource 目录下创建 bootstrap.properties 配置文件,将 spring. application.name= trace-1 配置移动到该文件中去。由于 logback-spring.xml 的加载在 application.properties 之前, 所以之前的配置 logback-spring.xml 无法获取 spring.application.name 属性, 因此这里将该属性移动到最先加载的 bootstrap.properties 配置文件中。
在工程 /resource 目录下创建 logback 配置文件 logback-spring.xml, 具体内 容如下:
<configura已on>
<'-- 日志在工程中的桧出位置 --> <!--控制台的日志捡出样式--> <! -- 控制台Appender --> INFO ${CONS0LE_LOG—PATTERN} utf8 <file>${LOG_ FILE}. j son file> ${LOG—FILE}. json. %d{yyyy-MM-dd}. gz 7 UTC "severity": "%level", "service" : "$ { springAppName: -} ", "trace": "%X{X-B3-Traceid:-} ", "span": "%X{X-B3-Spanid:-} ", "exportable": "%X{X-Span-Export: 一} ” , "pid": "${PIO:-}", "thread": "%thread", "class": "%logger{40}", "rest": "%message"
对 Logstash 的支持主要通过名为 logstash 的 Appender 实现, 内容并不复杂, 主要是 对日志信息的格式化处理, 上面为了方便调试和查看, 我们先将 JSON 格式的日志输出到文 件中。
完成上面的改造之后, 我们再将快速入门的示例运行起来, 并发起对 trace-1 的接 口访间。 此时可以在 trace-1 和 trace-2 的工程目录下发现有 一 个 build 目录, 下面 分别创建了以各自应用名称命名的 JSON 文件, 该文件就是在 logback-spring.xml 中配置的名为 logs七ash 的 Appender 输出的日志文件,其中记录了类似下面格式的 JSON日志:
{" @timestamp":"2016-12-04T06:57:58.970+00:00","severity":"INFO","service":"trac
e-l","trace":"589ee5f7b860132f","span":"a9e891273affb7fc","exportable":"false","pid
":"19756","thread":"http-nio-9101-exec-1","class":"c.d.TraceApplication$$EnhancerBy
SpringCGLIB$$a9604da6","rest":"===<call trace-1>===" )
{"@timestamp":"2016-12-04T06:57:59.061+00:00","severity":"INFO","service":"trace-1","trace":"589ee5f7b860132f","span":"2df8511ddf3d79a2","exportable":"false","pid
":"19756","thread":"http-nio-9101-exec-l","class":"o.s.c.a.AnnotationConfigApplicat
ionContext","rest":"Refreshing org. springframework. context. annotation. AnnotationConfigApplicationContext@64951f38: startup date [Sun Dec 04 14: 57: 59 CST 2016]; parent:
org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationCo.n
text@4b8c8f15"}
除了可以通过上面的方式生成 JSON 文件外, 还可以使用 LogstashTcpSocket Appender 将日志内容直接通过 Tep Socket 输出到 Logstash 服务端, 比如
<appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>127.0.0.1:9250</destination>
</appender>
与Zipkin整合
虽然通过 ELK 平台提供的收集、存储、 搜索等强大功能, 我们对跟踪信息的管理和使 用已经变得非常便利。但是,在 ELK 平台中的数据分析维度缺少对请求链路中各阶段时间 延迟的关注, 很多时候我们追溯请求链路的 一 个原因是为了找出整个调用链路中出现延迟 过高的瓶颈源, 或为了实现对分布式系统做延迟监控等与时间消耗相关的需求, 这时候类 似 ELK 这样的日志分析系统就显得有些乏力了。 对千这样的问题, 我们就可以引入 Zipkin 来得以轻松解决。
Zipkin 是 Twitter 的 一 个开源项目, 它基于 Google Dapper 实现。我们可以使用它来收 集各个服务器上请求链路的跟踪数据, 并通过它提供的 REST API 接口来辅助查询跟踪数 据以实现对分布式系统的监控程序, 从而及时发现系统中出现的延迟升高问题并找出系统 性能瓶颈的根源。 除了面向开发的 API 接口之外, 它还提供了方便的 UI 组件来帮助我们 直观地搜索跟踪信息和分析请求链路明细, 比如可以查询某段时间内各用户请求的处理时 间等。
下图展示了 Zipkin 的基础架构, 它主要由 4 个核心组件构成。
- Collector:收集器组件, 它主要处理从外部系统发送过来的跟踪信息, 将这些信息转换为Zip幻n内部处理的Span格式, 以支待后续的存储、 分析、 展示等功能。
- storage:存储组件,它主要处理收集器接收到的跟踪信息, 默认会将这些信息存储在内存中,我们也可以修改此存储策略, 通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTfull:API组件, 它主要用来提供外部访问接口。 比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- WEB UI:UI组件, 基于API组件实现的上层应用。 通过UI组件, 用户可以方便而又直观地查询和分析跟踪信息。