Spring Cloud Sleuth 在Spring Boot 1.x时代,是对Zipkin做一个完全整合,不仅实现了以 HTTP 的方式收集跟踪信息,还实现了通过消息中间件来对跟踪信息进行异步收集的封装。就连Zipkin服务端也做了一层封装。而到了 Spring Boot 2.0 之后 Zipkin 不再推荐我们再自定义 Server 端了,Sleuth专注于对Dapper 中的算法进行封装,spring-cloud-starter-zipkin 只是对Zipkin客户端的封装,对于Sleuth在工程中如何使用,Spring官网上是这样写的:
Only Sleuth (log correlation):如果只想使用Sleuth功能,而不想与Zipkin做集成的话,那么你只需要引入spring-cloud-starter-sleuth
就可以了,普通开发人员可能没有机会用到这种方式。
Sleuth with Zipkin via HTTP :使用Sleuth并通过HTTP方式集成ZipkinServer,你只需要引入spring-cloud-starter-zipkin
就可以了。starter中本身就已经依赖了sleuth和sleuth+zipkin。
Sleuth with Zipkin over RabbitMQ or Kafka :通过RabbitMQ或者Kafka这些消息中间件进行异步的消息处理,此时需要引入spring-cloud-starter-zipkin
,还需要引入spring-rabbit
(以Rabbit为例),且Zipkin的服务端也需要做rabbitMq的相应配置。
使用 RabbitMQ 异步发送 span 信息#
为什么选择 RabbitMQ 消息中间件发送 span 信息
1.sleuth 默认采用 http 通信方式,将数据传给 zipkin 作页面渲染,但是 http 本身存在握手链接等延时损耗,且传输过程中如果由于不可抗因素导致通信中断,那么此次通信的数据将会丢失。
2.而使用中间件的话,RabbitMQ 消息队列可以积压千万级别的消息,下次重连之后可以继续消费。且随着线程增多,并发量提升之后,RabbitMQ 异步发送数据明显更具有优势。
3.RabbitMQ 支持消息、队列持久化,可以通过消息状态落库、重回队列、镜像队列等技术手段保证其高可用。
rabbitmq:
host: 192.168.200.128
port: 5672
username: guest
password: guest
本文本次仅实现
**Sleuth with Zipkin via HTTP**
方式。
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
server:
port: 6868
spring:
application:
name: eureka # 服务名称
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 不要注册自己到eureka服务中
fetch-registry: false # 是否从eureka中获取信息
service-url: # 配置注册中心访问地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-netflix-eureka-clientartifactId>
<version>2.1.1.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-serverartifactId>
<version>2.12.3version>
<exclusions>
<exclusion>
<artifactId>log4j-slf4j-implartifactId>
<groupId>org.apache.logging.log4jgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-autoconfigure-uiartifactId>
<version>2.12.3version>
dependency>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-autoconfigure-storage-mysqlartifactId>
<version>2.12.3version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
dependency>
dependencies>
server:
port: 8082
management:
metrics:
web:
server:
auto-time-requests: false
spring:
zipkin:
# 关闭本工程的推送到zipkin服务的功能
enabled: false
discovery-client-enabled: false
storage:
type: mysql
rabbitmq:
host: 192.168.200.128
port: 5672
username: guest
password: guest
# 配置mysql
datasource:
schema: classpath:/mysql.sql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/atest?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
# Switch this on to create the schema on startup:
# initialize: true
# continueOnError: true
sleuth:
enabled: false
# sampler:
# probability: 1.0
##数据库脚本创建地址,当有多个是可使用[x]表示集合第几个元素
#spring.datasource.schema[0]=classpath:/mysql.sql
# 关闭自动配置启用所有请求得检测
# 加入下面的配置可以防止报错:java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys.
management:
metrics:
web:
server:
# 避免访问 zipkin 页面报 java.lang.IllegalArgumentException
auto-time-requests: false
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import zipkin2.server.internal.EnableZipkinServer;
@SpringBootApplication
@EnableEurekaClient
@EnableZipkinServer
public class QddFreightZipkinApplication {
public static void main(String[] args) {
SpringApplication.run(QddFreightZipkinApplication.class, args);
}
}
手动建数据库表,sql如下:
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
运行Zipkin服务端,并访问http:..laocalhost:8082
j就可以看到zipkin的界面。如下:
添加zipkin依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
spring:
zipkin:
# 不使用rabbitmq需要配置Zipkin服务器地址
base-url: http://localhost:8082
sleuth:
sampler:
# 配置采样比例,生产环境应调小,否则影响系统执行效率,或者改为rabbitMq方式
probability: 1.0
但在测试的过程中我们会发现,有时候,程序刚刚启动后,刷新几次,并不能看到任何数据,原因就是我们的spring-cloud-sleuth收集信息是有一定的比率的,默认的采样率是0.1,该值的大小与系统响应速度成反比。建议生产环境下不要设置过大。
可能是版本更新问题,一些文章中设置的方式是
spring.sleuth.sampler.percentage=1
,但我查看了源码后发现,现在已经改为probability了。如果按照原来的写应该会报黄。是否生效,我没再进行确认。但如果已经没有这个字段属性了,应该不会生效。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
//import org.springframework.context.annotation.Bean;
import zipkin2.server.internal.EnableZipkinServer;
//import zipkin2.storage.mysql.v1.MySQLStorage;
//import javax.sql.DataSource;
@SpringBootApplication
@EnableEurekaClient
@EnableZipkinServer
public class QddFreightZipkinApplication {
public static void main(String[] args) {
SpringApplication.run(QddFreightZipkinApplication.class, args);
}
// 不使用该bean同样可以完成数据库写操作
// @Bean
// public MySQLStorage mySQLStorage(DataSource datasource) {
// return MySQLStorage.newBuilder().datasource(datasource).executor(Runnable::run).build();
// }
}
有些文章上说需要在引导类中注册MySQLStorage的Bean,但是本人测试过按照我文章中所写的方法,并不需要该Bean。而且,大部分文章里给的Bean的注册方式都是
MySQLStorage.builder()....;
。但我用的版本中方法为MySQLStorage.newBuilder()....
。因为没有用到这个Bean,所以我不知道是否正确,但至少不报错。
注意DataSource的包名是javax.sql.DataSource。
因为是测试,所以没有搭建服务consumer,只是使用postman做了请求测试。
如下请求,响应成功。
查看zipkin界面上显示的追踪到的数据:
可以点击查看详细的请求数据。
数据库中相应的数据也已经写入成功。
缺陷1:zipkin客户端向zipkin-server程序发送数据使用的是http的方式通信,每次发送的时候涉及到连接和发送过程。如果设置采样率过高,会拉低系统性能,所以不建议生产环境设置过高采样率。
缺陷2:当我们的zipkin-server程序关闭或者重启过程中,因为客户端收集信息的发送采用http的方式会被丢失。
1、通信采用socket或者其他效率更高的通信方式。
2、客户端数据的发送尽量减少业务线程的时间消耗,采用异步等方式发送收集信息。
3、客户端与zipkin-server之间增加缓存类的中间件,例如redis、MQ等,在zipkin-server程序挂掉或重启过程中,客户端依旧可以正常的发送自己收集的信息。
采用以上三种方式会提高系统的执行和响应效率和可靠性。而且spring-cloud也已经为我们提供了采用redis或Es等内存数据存储的方式,以及利用消息中间件进行数据分阶段存储的实现方式。