1.什么是sprng cloud sleuth
目的是对一个请求所经过的整个服务链的跟踪,帮助我们快速发现错误根源以及监控分析每条请求链路上的性能瓶颈。
2.快速入门
首先前提是启动服务消费者和服务提供者并且分别服务注册中心注册
在消费者和提供者的依赖中添加sleuth依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
在消费者消费之后,可以看到打印了一些信息
- 第一个值:llg-consumer 代表的是服务名称
- 第二个值:38364868ab851c1e是Sleuth生成的ID,称为TraceID,它用来标识一条请求链路。一条请求链路中包含一个TraceID,多个SpanID
- 第三个值:9d2df7f95505492f是Sleuth生成的ID,称为SpanID,它表示一个基本的工作单元,比如发送一个HTTP请求
- 第四个值:false,表示是否要将该信息输出到ZipKin等服务中来收集和展示
3.跟踪原理
引用依赖之后会自动为当前应用构建起各通信通道的跟踪机制,比如
-
通过rabbitmq、kafka(或者其他任何stream绑定器实现的消息中间件)传递的请求
-
通过Zuul代理传递的请求
-
通过RestTemplate 发起的请求
原理是通过在请求头设置标记段标记请求
- X-B3-TraceID:一套请求链路(Trace)的唯一标识,必须的值。
- X-B3-SpanID:一个工作单元(Span)的唯一标识,必须的值
- X-B3-ParentSpanID: 标识当前工作单元所属的上一个工作单元,所以第一个请求的工作单元肯定是为空的
- X-Span-Name:工作单元的名称
4.抽样收集
由于系统一般会有高并发的场景,如果收集信息过大则会影响性能。所以在上方的跟踪消息处的第四个值是个boolean类型,代表是否需要被收集器收集
通过实现接口Sample的isSmampled()方法,如果返回false则不收集
public interface Sampler {
boolean isSampled(Span var1);
}
默认情况下使用PercentageBasedSampler实现收集的策略
public class PercentageBasedSampler implements Sampler {
public boolean isSampled(Span currentSpan) {
if (this.configuration.getPercentage() != 0.0F && currentSpan != null) {
if (this.configuration.getPercentage() == 1.0F) {
return true;
} else {
synchronized(this) {
int i = this.counter.getAndIncrement();
boolean result = this.sampleDecisions.get(i);
if (i == 99) {
this.counter.set(0);
}
return result;
}
}
} else {
return false;
}
}
}
配置方法比较简单,通过配置文件即可,默认为0.1,也就是收集10%的数据
spring.sleuth.sampler.probability=0.1
5.与Logstash整合
由于跟踪信息都在每个服务实例上,看起来非常麻烦,所以我们需要一个工具来帮忙收集存储和搜索这些跟踪信息。比如基于日志的分析系统ELK平台
ELK平台由ElasticSerach、Logstash和Kibana三个开源工具组成。
ElasticSerach :是一个开源分布式搜索引擎,它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,RESTful风格接口,多数据源,自动搜索负载等。
Logstash :是一个完全开源的工具,它可以对日志进行收集、过滤,并将其存储供以后使用
Kibana:也是一个开源和免费的工具,它可以为Logstash和ElasticSearch提供日志分析友好的Web界面,可以帮助汇总、分析和搜索重要数据日志。
入门实战
由于ELK平台主要的就是将Logstash与服务对接,而spring boot使用的日志是logback,而Logstash也有与logback对接的工具类
下面引入这个工具依赖
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.2</version>
</dependency>
然后在resource目录下写上配置文件logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="springAppName" source="spring.application.name"/>
<!-- Example for logging into the build folder of your project -->
<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow} %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
<!-- Appender to log to console -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- Minimum logging level to be presented in the console logs-->
<level>INFO</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<!-- Appender to log to file -->
<!--<appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">-->
<!--<file>${LOG_FILE}</file>-->
<!--<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">-->
<!--<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>-->
<!--<maxHistory>7</maxHistory>-->
<!--</rollingPolicy>-->
<!--<encoder>-->
<!--<pattern>${CONSOLE_LOG_PATTERN}</pattern>-->
<!--<charset>utf8</charset>-->
<!--</encoder>-->
<!--</appender>-->
<!-- Appender to log to file in a JSON format -->
<appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "${springAppName:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"exportable": "%X{X-Span-Export:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="logstash"/>
<!--<appender-ref ref="flatfile"/>-->
</root>
</configuration>
6.与ZipKin整合
由于ELK缺乏对时间延迟的关注,所以我们需要使用ZipKIn
Zipkin主要包括四部分组成
- Collercto;收集器组件,它主要处理从外部系统发送过来的跟踪消息,将这些信息转化为ZipKin内部处理的Span格式,以支持后续的存储、分析、展示等功能。
- Storge: 存储组件,它主要处理收集器接收到的跟踪消息,默认会将这些信息存储在内存中。我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库
- RESTFUL API : API组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等
- Web UI : 基于API组件的上层应用,通过UI组件,用户可以方便又直观地查询和分析跟踪消息
HTTP收集
下面详细介绍Slueth与ZipKin的基础整合过程
第一步:搭建ZipKin Server(2.0已经过时)
创建一个zipKin-server服务,引入相关依赖
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-server</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>
开启ZipKIn server注解
@EnableZipkinServer
@SpringBootApplication
public class ZipkinApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplication.class, args);
}
}
完成后通过访问http://localhost:9411可以看到管理首页
2.0搭建Zipkin Server新方法
官方提供了一键脚本
| |
如果用 Docker 的话,直接
| |
第二步:为应用引入和配置ZipKin配置
需要被收集信息的服务添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
接着配置ZipKin Server 的地址
spring.zipkin.base-url=http://localhost:9411
消息中间件收集(2.0过时)
通过消息中间件来跟踪信息进行异步收集的封装,通过结合stream,可以非常轻松地让应用客户端将跟踪信息输出到消息中间件上,同时ZipKin服务端从消息中间件上异步地消费这些跟踪信息
相比于HTTP的方式来说,使用消息中间件有以下优点
- 微服务与ZipKin Server 解耦,微服务无需知道ZIpKin Server的网络地址
- 有些时候网络不通,HTTP可能无法工作
第一步,对客户端添加依赖。需要sleuth、stream、sleuth-stream依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
设置配置信息
这里已经去掉ZioKin Server 地址的配置,实现解耦
spring.application.name=trace-2
server.port=9102
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#spring.sleuth.sampler.percentage=0.1
# log trace detail
logging.level.org.springframework.web.servlet.DispatcherServlet=DEBUG
logging.file=${spring.application.name}.log
第二步:修改zipKIn-server服务端
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>
配置信息
spring.application.name=zipkin-server-stream
server.port=9411
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
logging.file=${spring.application.name}.log
开启服务
@EnableZipkinStreamServer
@SpringBootApplication
public class ZipkinApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinApplication.class, args);
}
}
消息中间件收集(2.0)
方式二:消息总线 RabbitMQ
因为之前说的 Zipkin 不再推荐我们来自定义 Server 端了,所以在最新版本的 Spring Cloud 依赖管理里已经找不到 zipkin-server 了。
那么如果直接用官方提供的 jar 包怎么从 RabbitMQ 中获取 trace 信息呢?
我们可以通过环境变量让 Zipkin 从 RabbitMQ 中读取信息,就像这样:
复制
| |
可配置的环境变量如下表所示:
属性 | 环境变量 | 描述 |
---|---|---|
zipkin.collector.rabbitmq.concurrency | RABBIT_CONCURRENCY | 并发消费者数量,默认为1 |
zipkin.collector.rabbitmq.connection-timeout | RABBIT_CONNECTION_TIMEOUT | 建立连接时的超时时间,默认为 60000 毫秒,即 1 分钟 |
zipkin.collector.rabbitmq.queue | RABBIT_QUEUE | 从中获取 span 信息的队列,默认为 zipkin |
zipkin.collector.rabbitmq.uri | RABBIT_URI | 符合 RabbitMQ URI 规范 的 URI,例如amqp://user:pass@host:10000/vhost |
如果设置了 URI,则以下属性将被忽略。
属性 | 环境变量 | 描述 |
---|---|---|
zipkin.collector.rabbitmq.addresses | RABBIT_ADDRESSES | 用逗号分隔的 RabbitMQ 地址列表,例如localhost:5672,localhost:5673 |
zipkin.collector.rabbitmq.password | RABBIT_PASSWORD | 连接到 RabbitMQ 时使用的密码,默认为 guest |
zipkin.collector.rabbitmq.username | RABBIT_USER | 连接到 RabbitMQ 时使用的用户名,默认为guest |
zipkin.collector.rabbitmq.virtual-host | RABBIT_VIRTUAL_HOST | 使用的 RabbitMQ virtual host,默认为 / |
zipkin.collector.rabbitmq.use-ssl | RABBIT_USE_SSL | 设置为true 则用 SSL 的方式与 RabbitMQ 建立链接 |
关于 Zipkin 的 Client 端,也就是微服务应用,我们就在之前 trace-a、trace-b 的基础上修改,只要在他们的依赖里都引入spring-cloud-stream-binder-rabbit
就好了,别的不用改。
| |
不过为了说明是通过 RabbitMQ 传输的信息,我将spring.zipkin.base-url
均改为http://localhost:9412/
,即指向一个错误的地址。
分别重启 trace-a、trace-b 工程,并启动 Zipkin Server
| |
然后访问 http://localhost:8080/trace-a 并刷新 Zipkin UI,看到如下内容,就说明 Sleuth+Zipkin+RabbitMQ 整合成功了。
此时看 RabbitMQ Admin,会看到多了一个名为 zipkin 的 Queue
7.收集原理
数据模型
- Span:代表了一个基础单元。对于同一个请求来说一些Annotation所标识的不同阶段属于同一个单元,这个工作单元用64位的ID来唯一标识。这个工作单元包括了Trace ID用来之后的串联、描述信息、事件时间戳、Annotation的键值对属性、上一级工作单元的Span ID 等。
- Trace: 它是由一些列具有相同Trace ID 的Span串联形成的一个树状结构。在复杂的分布式系统中,每一个外部请求通常都会产生一个复杂的树状结构的Trace
- Annotation: 一个包含有时间戳的事件标签,在Sleuth中定义了下面4个核心Annotation来标识一个请求的开始和结束
- cs(client send) : 该Annotation用来记录客户端发起一个请求,标识了请求的开始
- sr(server receive): 接受请求,准备开始处理。通过计算cs和sr的时间戳差值,得出请求的网络延迟
- ss(server send): 服务器处理完准备发送请求响应信息。通过计算ss与sr两个Annotation的时间戳之差,我们可以得到当前服务端处理请求的时间消耗
- cr(client receive) : 该annotation用来记录客户端接收到服务端的恢复,同时标识了请求的结束。通过计算cr与cs两个Annotation的时间戳之差,我们可以得到该Http请求从客户端发起到接受服务端响应的总时间消耗。
- BInaryAnnoation: 它用来对跟踪信息添加一些额外的补充说明,一般以键值对的方式出现
收集机制
(略)
8.数据存储
由于Zipin Server 只会将跟踪信息存储咋内存中,一般情况下,我们都需要将这个记录保存到mysql数据库中
第一步,为ZipKIn-server 添加依赖
org.jooq是为了防止依赖版本错误而配置的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.8.0</version>
</dependency>
第二步:在MySQL中创建用于ZipKin存储的Schema
可以直接扫描依赖的sql文件创建表
spring.datasource.schema=classpath:/mysql.sql
spring.datasource.url=jdbc:mysql://localhost:3306/zipkin
spring.datasource.password=root
spring.datasource.username=root
spring.datasource.continue-on-error=true
spring.datasource.initialize=true
zipkin.storage.type=mysql
9.API接口
ZipKin提供了RESTFul API接口供用户在第三方系统中调用来定制自己的跟踪信息展示或监控
- /dependencies get 获取依赖关系
- /services get 获取服务列表
- /spans get 根据服务名来获取所有的Span名
- /spans post 向ZipKin Server 上传Span
- /trace/{traceId} get 根据Trace ID 获取指定跟踪信息的Span列表
- /trace/ get 根据指定条件查询并返回符合条件的trace清单
参考文章地址 :https://windmt.com/2018/04/24/spring-cloud-12-sleuth-zipkin/
- 本文作者: Yibo
- 本文链接: https://windmt.com/2018/05/07/spring-cloud-13-spring-cloud-gateway-router/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!