什么是链路跟踪系统?
在微服务中,多个服务分布在不同物理机器上,各个服务之间相互调用。如何清晰地记录服务调用过程,并在出现问题的时候能够通过查看日志和服务之间的调用关系来定位问题,这样的系统就叫做链路跟踪系统。
分布式服务跟踪系统主要由五部分组成:
数据采集数据传输数据存储数据分析数据可视化
为什么需要链路跟踪系统?
在微服务出现服务调用过程慢、服务无法调用成功等问题,都需要用到链路跟踪系统来进一步排查问题,否则就是盲人摸象,无从下手。
Spring Cloud Sleuth
Spring Cloud Sleuth 是为了对微服务之间的调用链路进行跟踪的一个组件,提供了非常多的整合方案,比如可以整合zipkin
在了解sleth前,必须要掌握如下几个概念:
Span:采集的基本工作单元,有一个64位ID唯一标识,还包含摘要、时间戳事件、关键值注释(tags)、进度ID(通常是IP地址),span在不断的启动和停止,同时记录了时间信息。Trace:由一系列的span组成一个树状结构Annotation:用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束,如下:cs(Client Sent):客户端发起一个请求,这个annotation描述了这个span的开始sr(Server Received):服务端收到请求并准备开始处理它。sr-cr=网络延迟ss(Server Sent):服务端处理完成,准备返回客户端。ss-sr=服务端处理请求花费的时间cr(Client Received):标识span结束,客户端接收到服务端的回复。cr-cs=整个请求所花费的所有时间采样率
在网络流量很大的情况下,如果全部采集对传输、存储、性能压力都会比较大。这时可以通过设置采样率来采样部分信息,sleuth可以通过配置spring.sleuth.sampler.probability=1.0
,则表示采样率为100%(采集服务的全部追踪数据),默认值为0.1(10%)。
也可以通过Bean的方式设置采样率为全部采样(AlwaysSampler
)或不采样(NeverSampler
)
@Beanpublic Sampler defaultSampler() {return new AlwaysSampler();}
了解了以上几个概念后,我们来看下具体的链路跟踪过程来加深下印象:
链路跟踪基本过程:
server1开启一个trace100和span1,然后发送请求到server2,span1包含在trace100中server2收到请求后,开启一个span2,父级为span1,向server3请求并返回,span2结束server2再开启一个span3,父级为span1,向server4请求并返回,span3结束在server2中的span2、span3都结束后,server2返回请求到server1,span1结束,trace100结束
Zipkin
sleuth是spring cloud提供的平台无关性的链路跟踪组件,可以整合不同的实现,这里整合的就是zipkin。
zipkin是Twitter的一个开源项目,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和发现。
可以借助zipkin来收集各个服务器上请求链路的跟踪数据,并通过REST API接口来查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高、请求丢失等问题并找出系统的性能瓶颈。
除了API的方式,也提供了可视化组件来帮助我们直观的搜索跟踪信息和分析请求链路与花费时间。比如需要查询某段时间内各个用户请求的处理花费时间。
如图可知:
Zipkin提供了多种存储方式:In-Memory、MySQL、Cassandra以及Elasticsearch(生产环境推荐ES)Zipkin提供了多种数据展现方式:REST API查询接口和可视化的界面展示
手把手教你实现微服务链路跟踪系统
接下来我们通过示例来演示如何实现并使用链路跟踪系统。
此示例中,会搭建两个模块,一个是sale模块,一个是user模块,通过sale模块调用user模块,看看zipkin能不能跟踪到
Zipkin 环境搭建
安装jdk8、mysql
下载zipkin包:/remote_content?g=io.zipkin&a=zipkin-server&v=LATEST&c=exec
建立一个名为zipkin数据库,并初始化zipkin默认的几个表:
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,`remote_service_name` VARCHAR(255),`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',PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;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(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';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 and autocomplete values';ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';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,`error_count` BIGINT,PRIMARY KEY (`day`, `parent`, `child`)) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
然后以mysql为存储方式启动zipkin 在zipkin.jar同级目录下创建zipkin-server.properties
文件,内容为
zipkin.storage.type=mysqlzipkin.storage.mysql.host=localhostzipkin.storage.mysql.port=3306zipkin.storage.mysql.username=rootzipkin.storage.mysql.password=123456
运行zipkin:java -jar zipkin.jar
sleuth 示例
开始之前,我们先新建一个user表,一起放在zipkin数据库中
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for tb_user-- ----------------------------DROP TABLE IF EXISTS `tb_user`;CREATE TABLE `tb_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID ',`name` varchar(255) NOT NULL COMMENT '用户名',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ------------------------------ Records of tb_user-- ----------------------------BEGIN;INSERT INTO `tb_user` VALUES (1, 'Kitty');INSERT INTO `tb_user` VALUES (2, 'Mon');COMMIT;SET FOREIGN_KEY_CHECKS = 1;
创建一个名为sleuth-demo的maven项目,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.siqi.sleuth</groupId><artifactId>sleuth-demo</artifactId><packaging>pom</packaging><version>1.0-SNAPSHOT</version><modules><module>sleuth-sale</module><module>sleuth-user</module></modules><properties><piler.source>1.8</piler.source><piler.target>1.8</piler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.3.4.RELEASE</spring-boot.version><sleuth.version>2.2.6.RELEASE</sleuth.version></properties><dependencyManagement><!--maven的多继承情况下需要这么配置,否则无法多继承--><dependencies><!--分布式链路追踪父工程--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-sleuth-dependencies</artifactId><version>${sleuth.version}</version><type>pom</type><scope>import</scope></dependency><!--spring boot 父工程--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>
继承两个父工程:spring boot和sleuth
创建sale子模块
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><parent><artifactId>sleuth-demo</artifactId><groupId>org.siqi.sleuth</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>sale</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- 通过http发送跟踪数据到zipkin(默认为 http://localhost:9411/api/v2/spans) --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zipkin</artifactId></dependency></dependencies></project>
新建一个Sale实体
public class Sale {/*** ID*/private Long id;/*** 名称*/private String name;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
新建一个SaleController
@RestControllerpublic class SaleController {@AutowiredRestTemplate restTemplate;/*** 通过sale服务向user服务请求数据* http://localhost:8081/sales/1** @param id* @return*/@RequestMapping(value = "/sales/{id}", method = GET, produces = {APPLICATION_JSON_VALUE})public Sale describeSale(@PathVariable(value = "id") Long id) {// 向user服务请求数据return restTemplate.getForObject("http://localhost:8091/users/{1}", Sale.class, id);}@BeanRestTemplate restTemplate() {return new RestTemplate();}}
模块配置为:
spring.application.name=saleserver.port=8081##.springframework.web=DEBUGspring.sleuth.traceId128=truespring.sleuth.sampler.probability=1.0# Adds trace and span IDs to logs (when a trace is in progress)logging.pattern.level=[%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n# Propagates a field named 'user_name' downstream# Note: In sleuth 3.x it is spring.sleuth.baggage.remote-fields=user_namespring.sleuth.propagation-keys=user_name
新建一个user模块,供sale模块进行服务调用
pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><parent><artifactId>sleuth-demo</artifactId><groupId>org.siqi.sleuth</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>user</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!-- 通过http发送跟踪数据到zipkin(默认为 http://localhost:9411/api/v2/spans) --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zipkin</artifactId></dependency><dependency><groupId>io.zipkin.brave</groupId><artifactId>brave-instrumentation-mysql8</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency></dependencies></project>
新建一个User实体:
public class User {/*** ID*/private Long id;/*** 名称*/private String name;public User() {}public User(Long id, String name) {this.id = id;this.name = name;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}}
新建一个UserRepository,来访问数据库获取用户数据:
@Repositorypublic class UserRepository {private static final Logger logger = LoggerFactory.getLogger(UserRepository.class);@Resourceprivate JdbcTemplate jdbcTemplate;/*** 根据用户ID查询用户信息** @param id ID* @return 用户信息*/public User queryById(Long id) {logger.info("query user by id ");// 由于是演示,所以使用最简单的方式查询数据库数据return (User) jdbcTemplate.queryForObject("select id, name from tb_user where id = ?",new Object[]{id},new BeanPropertyRowMapper(User.class));}}
新建一个UserController
@RestControllerpublic class UserController {@Resourceprivate UserRepository userRepository;/*** 查看用户信息* http://localhost:8091/users/1** @param id* @return json格式*/@ResponseBody@RequestMapping(value = "/users/{id}", method = GET, produces = {APPLICATION_JSON_VALUE})public User describeUser(@PathVariable(value = "id") Long id) {if (id == null) {throw new IllegalArgumentException("id can't be null");}return userRepository.queryById(id);}}
配置文件为:
spring.application.name=userserver.port=8091# 日志等级.springframework.web=DEBUGlogging.pattern.level=[%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n## sleuth配置spring.sleuth.traceId128=truespring.sleuth.sampler.probability=1.0# Adds trace and span IDs to logs (when a trace is in progress)# Propagates a field named 'user_name' downstream# Note: In sleuth 3.x it is spring.sleuth.baggage.remote-fields=user_namespring.sleuth.propagation-keys=user_name## 数据库配置spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/zipkin?\useSSL=false\&queryInterceptors=brave.mysql8.TracingQueryInterceptor\&exceptionInterceptors=brave.mysql8.TracingExceptionInterceptor\&zipkinServiceName=userDB\&useUnicode=true&characterEncoding=utf8\&allowMultiQueries=true\&serverTimezone=UTCspring.datasource.username=rootspring.datasource.password=123456
链路跟踪验证
启动sale、user模块,并访问地址:http://localhost:8081/sales/1
这时我们发出了一个请求,通过浏览器-》sale-》user,然后返回数据
访问成功说明项目搭建没问题
我们来看看zipkin的跟踪情况,打开zipkin控制台:http://localhost:9411/
通过依赖,并选择指定的时间,可以查询出我们刚才发除的请求,从sale->user->userDB
通过点击TRACES,可以查看指定请求花费时间
至此,我们完成了从示例构建到zipkin链路跟踪演示
常见的链路追踪技术
cat
由大众点评开源,基于java开发的实时应用监控平台,包括实时应用监控,业务监控。继承方案是通过代码埋点的方式来实现监控,比如:拦截器,过滤器等。对代码的入侵性很大,集成成本较高,风险较大。
zipkin
由Twitter公司开源,开放源代码分布式的跟踪系统,用于手机服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使用较为简单,集成很方便,但功能较简单。
pinpoint
由韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具,特点是支持多种插件,UI功能较强,接入端无代码入侵。
skywalking(推荐使用)
是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点和pinpoint差不多。
扩展
sleuth采样算法Reservoir sampling(水塘抽样)。实现类是PercentageBasedSampler
。
附水塘抽样算法:/krcys/p/9121487.html