目录
在springcloud技术栈构建的微服务架构体系中,一旦微服务数量越来越多,服务之间的调用链路也必然越来越复杂,遇到问题时,排查难度也会相应增加。
如下图所示,为模拟一个微服务架构的系统在真实线上部署的场景,客户端发出的一个请求,通过网关之后,在内部处理请求时,可能经历了非常复杂的互相调用,试想,现在某个链路突然发生异常,对于开发人员来说,是不是有点摸不着头脑。
以上只是众多的微服务调用链路中相对比较简单的一种,通常来说,在分布式调用中,一个由客户端发起的请求,在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果。
在复杂的微服务架构系统中,几乎每一个请求都会形成一条复杂的分布式服务调用链路。在每条链路中,任何一个依赖服务出现延迟过高,或错误时都有可能造成请求最后的失败。这时对于每个请求全链路调用的跟踪就变得非常重要。
通过实现对请求调用的跟踪,可以帮助开发人员快速的定位错误根源,以及监控分析每条请求链路上的性能瓶颈等。
在开源的分布式链路追踪解决方案中,以springcloud生态来说,针对微服务链路追踪这个问题,Spring Cloud Sleuth提供了一套完整的解决方案。
链路追踪一词最早在2010年提出,由谷歌发表的一篇关于大规模分布式系统跟踪的论述,介绍了谷歌自研的分布式链路追踪的实现原理。
侠义上理解链路追踪,就是指一次任务从开始到结束,期间调用的所有系统以及耗时(时间跨度)都可以完整的记录下来。
广义上讲,链路追踪是指在分布式系统中,将一次请求的处理过程进行记录并聚合展示的一种方法。目的是将一次分布式请求的调用情况集中在一处展示,如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等。这样就可以轻松了解一个请求在系统中的完整生命周期,包括经过的服务、调用的操作以及每个操作的延迟等。
通过链路追踪,可以更好地理解系统的性能瓶颈、找出问题的根源以及优化系统的性能。
目前,链路追踪方案像Google的Dapper,阿里的鹰眼,大众点评的CAT,Twitter的Zipkin,LINE的pinpoint,国产的skywalking等。市面上的全链路监控理论模型大多都是借鉴Google Dapper论文,这里列举下面几种:
下面结合两种使用较多的链路追踪工具zipkin和Skywalking 进行对比说明
类型 | zipkin | SKYwalking |
基本原理 | 拦截请求,发送(HTTP,mq)数据至zipkin服务 | java探针,字节码增强 |
接入方式 | 基于linkerd或者sleuth方式,引入配置即可 | Java agent字节码 |
支持OpenTracing | 是 | 是 |
颗粒度 | 接口级(类级别) | 方法级 |
存储 | ES,mysql,Cassandra,内存 | ES,H2,TIDB |
agent到collector的协议 | http,MQ | http,gRPC |
在链路追踪的解决方案中,不管是哪一种,基本上各个组件底层都使用了相同的规范,其中涉及到下面几个非常重要的术语。
span代表一组基本的工作单元,一次单独的调用链可称为一个span。通俗理解,span就是一次请求信息,当发送一个远程调用时就会产生一个Span,Span 由一个64位ID唯一标识的。
举例来说,为统计一个请求中各处理单元的延迟,当请求到达服务的各个组件时,通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等元数据。
如下,图中一个矩形框就是一个 Span,前端从发出请求到收到回复就是一个 Span。
由一组TraceId相同的Span串联形成的一个树状结构。一个trace可认为是一次完整的链路,内部包含多个span,trace和span之间是一对多的关系。而span与span之间存在父子级关系。
为实现请求跟踪,当请求到达分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯一标识将所有的请求串联起来,形成一条完整的请求链路。
举个例子:客户端调用服务 A 、服务 B 、服务 C 、服务 F,而每个服务例如 C 就是一个 Span,如果在服务 C 中另起线程调用了 D,那么 D 就是 C 的子 Span,如果在服务 D 中另起线程调用了 E,那么 E 就是 D 的子 Span,这个 C -> D -> E 的链路就是一条 Trace。如果链路追踪系统做好了,链路数据有了,借助前端解析和渲染工具,可以达到下图中的效果:
Annotation 用于记录一段时间内的事件,内部使用的重要注释:
Spring Cloud 微服务治理框架中的一个组件,专门用于记录链路数据的开源组件,官网地址
sluth提供的核心功能主要如下:
下面这张图详细说明了sleuth在工作过程中其内部的运行原理,理解了这张图的原理,不仅明白了sleuth是如何运行的,也对链路追踪中的几个概念也会有更深刻的理解。
关于这张图的调用过程做如下说明:
其实 span 内除了记录这几个参数之外,还可以记录一些其他信息,比如发起调用服务名称、被调服务名称、返回结果、IP、调用服务的名称等,最后,我们再把相同 parentid 的 span 信息合成一个大的 span 块,就完成了一个完整的调用链。
zipkin是一款开源的分布式实时数据追踪系统(Distributed Tracking System),基于 Google Dapper的论文设计而来,由 Twitter 公司开发贡献。它有助于收集对服务架构中的延迟问题进行故障排除所需的计时数据。功能包括收集和查找这些数据。
zipkin是一个开放源代码分布式的跟踪系统,它可以帮助收集服务调用中的时间数据,以解决微服务架构中的延迟问题。具体来说,包括数据收集、存储、查找和展现。
当微服务接入zipkin之后,每个服务向zipkin报告计时数据,zipkin会根据调用关系通过Zipkin UI生成依赖关系图,展示多少跟踪请求经过了哪些服务,该系统让开发者可通过一个web前端轻松地收集和分析数据,可非常方便的监测系统中存在的瓶颈。
下面是一张关于zipkin的原理图,从图中不难发现,在真实的环境中,zipkin主要是承担收集各个服务上报过来的信息,比如日志、服务链路等数据,经过内部的处理之后,通过ui界面进行展现。
下面是zipkin的工作原理图
或者通过下面这张运行原理图可以以全局的视角来了解zipkin
从上面的图中,可以发现zipkin在运行过程中,其核心组件主要包括下面几种:
Collector:信息收集器
主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin内部处理的Span格式,以支持后续的存储、分析、展示等功能。
Storage:数据存储组件
主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到 数据库或es 中。
RESTful API:API组件
主要用于提供外部访问接口,比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
Web UI:UI组件 ?
基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分析跟踪信息。
小结:
1)Zipkin 分两端,一个是 Zipkin 服务端,一个是 Zipkin 客户端,客户端也就是微服务应用;
2)客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端;
3)发送的方式有两种,一种是消息总线的方式如 RabbitMQ 发送,还有一种是 HTTP 报文的方式发送;
通过上面的介绍我们了解到,Sleuth是将每个请求从开始调用到结束过程中的每一步都进行记录, 但这些信息都是分散存在的,通过以日志格式输出出来,真正进行问题分析定位时并不方便,此时就需要有一个工具可以将这些分散的信息进行收集和汇总,并且能显示可视化结果,便于分析和定位。
有了zipkin之后,sleuth的只需要把链路追踪的元数据信息上报给zipkin,由zipkin进行存储并将链路信息以可视化的方式进行呈现。
sleuth可以单独在springboot项目中使用,也可以在springcloud、dubbo中进行集成,下面介绍sleuth最简单的集成方式。
文档地址:官方文档地址
git地址:sleuth git地址
maven中导入sleuth依赖
<!-- spring cloud sleuth 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
以之前整合的一个springcloud微服务为例进行说明,最简单的集成方式是,只需要在需要进行链路追踪的类上面添加 @Slf4j注解即可,如下,服务的调用链路为:order -> user,我们分别在需要调用的类上面添加注解;
用户微服务中添加注解,在方法的关键入口添加日志输出,默认sleuth追踪info级别的日志
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
//http:localhost:9002/user/getById?id=001
@GetMapping("/getById")
public User getById(@RequestParam String id) {
log.info("根据ID获取用户信息");
return userService.getById(id);
}
}
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
@Override
public User getById(String id) {
log.info("[用户服务] 基于 id 查询用户信息:{}", id);
String key = "sw:users:" + id;
Object json = redisTemplate.opsForValue().get(key);
if (json != null) {
log.info("[用户服务] redis 中查询到用户信息:key={}, json={}", key, json);
return JSON.parseObject(json.toString(), User.class);
}
User user = userMapper.getById(id);
if (user != null) {
log.info("[用户服务] redis 中不存在,从数据库查到数据并缓存:{}", user);
redisTemplate.opsForValue().set(key, user, 2, TimeUnit.HOURS);
return user;
}
log.warn("[用户服务] 基于 id 查询用户失败,用户不存在:{}", id);
return null;
}
}
order微服务中添加注解
@RequestMapping("/order")
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
//localhost:9003/order/getById?id=001
@GetMapping("/getById")
public Object getById(@RequestParam String id) {
log.info("根据ID获取订单");
return orderService.getById(id);
}
}
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserFeignService userFeignService;
@Override
public Map getById(String id) {
log.info("[订单服务] 基于 id 查询订单详情:{}", id);
Map map = new HashMap();
Order order = new Order();
order.setOrderId("0002");
order.setProductId("0001");
order.setProductName("小米手机");
map.put("order",order);
User user = userFeignService.getById("001");
map.put("user",user);
return map;
}
}
重启两个微服务模块,然后调用一下接口
接口调用成功后,我们观察控制台输出日志信息,order模块输出日志信息如下:
user模块输出的日志信息如下:
关于日志信息,做如下几点说明:
通过上文了解到,zipkin是一款用于做实时链路数据追踪的应用,通过可视化的方式展现从服务中上报的日志等链路追踪数据,便于问题的排查定位和分析,更加的人性化。
通常,以springcloud技术栈的微服务中,通常是将sleuth 搭配zipkin一起使用。当然也可以单独在springboo工程中接入zipkin。
zipkin服务端是一个独立的可执行的 jar 包,jar包下载地址:
1、官网下载,OpenZipkin · A distributed tracing system
2、ftp下载,zipkin各版本下载地址
这里我下载 2.22.1的版本的jar包
下载之后,不管是本地还是在linux服务器上,最简单的方式,直接使用下面的命令启动即可
java -jar zipkin-server-2.20.1-exec.jar
服务启动之后,通过9411端口可以直接访问zipkin的web-ui界面,如下图所示,后面当微服务接入zipkin之后,一旦发生服务间的调用将会展示出调用链路相关信息
Zipkin 默认将监控数据存储在内存中,很显然,如果 Zipkin 挂掉或重启的话,监控数据就会丢失。所以生产中,肯定是不能直接这样使用,需要对Zipkin的监控数据进行持久化存储。zipkin提供了多种数据持久化存储的方式:
下面以mysql为例进行说明
脚本下载地址在 github zipkin项目里的 zipkin-storage/mysql-v1/src/main/resounce 目录下
为例方便使用,下面贴出完整的sql
--
-- Copyright 2015-2019 The OpenZipkin Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
-- in compliance with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software distributed under the License
-- is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
-- or implied. See the License for the specific language governing permissions and limitations under
-- the License.
--
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;
创建一个数据库,名为zipkin,然后执行上面的sql创建表
使用下面的命令重新启动zipkin服务
java -jar zipkin-server-2.22.1-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=IP地址 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=密码
启动之后再次访问,仍然可以正常访问ui页面
上面的环境准备好之后,相当于是搭建了zikpkin的服务端,接下来就可以在springcloud微服务工程中(客户端)集成zpikin,将客户端的服务调用链路信息上报zipkin了,参考下面的操作步骤
sleuth的依赖仍然保留,同时添加zipkin的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
在工程的配置文件中添加如下关于zipkin和sleuth的配置信息,如果是云服务器,需要提前开通9411的防火墙端口
server:
port: 9002
spring:
application:
name: user-service
zipkin:
base-url: http://127.0.0.1:9411
sleuth:
sampler:
probability: 1
cloud:
nacos:
discovery:
server-addr: nacos地址:8848
确保zipkin服务已经启动,然后启动user和order两个服务,并调用下面的获取订单接口
调用成功后,在zipkin的ui界面上就能看到调用的链路信息了,如下图所示:
也可以点击进去,进一步查看,通过其他维度检查这个调用链路的详细信息,比如中间经历了哪些步骤,各个链路的耗费时间
也可以切换到依赖视图,以拓扑图的形式展示服务的调用链路情况
本文详细介绍了sleuth和zipkin的使用,在springcloud的微服务生态中,这两个组件的搭配可以快速实现服务链路的追踪,接入简单,对服务的可观测性来说是一个很好的补充,而zipkin的思想,也影响甚至促成了应用可观测领域很多解决方案的诞生,有必要深入学习和掌握其适用场景,本篇到此结束感谢观看。