.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px}.markdown-body h2{padding-bottom:12px;font-size:24px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}
在单体服务的架构中,所有的服务,组件都在一台机器上,如果需要监控服务的异常与耗时,往往是比较简单的。我们可以使用 AOP 在调用具体的业务逻辑前后分别打印一下时间即可计算出整体的调用时间。在问题追踪的时候,也可以在关键节点打印日志。
但是在微服务架构里就不同了,一次请求会涉及到多个模块与系统,往往需要多台机器的相互协作才能完成。而一系列的请求,不仅会涉及到串联并联、还有同步异步之分。这个时候,如果依然采取单体架构中服务监控的方式,那么确定这个请求背后调用了哪些服务,哪些模块,哪些节点及调用的先后顺序,调用的耗时,或者追踪调用中出现的问题甚至会让开发人员当场去世。于是乎,我们马上就会想到链路追踪。
链路追踪:通过在程序内打点记录日志的方式,记录每次请求的调用链路信息。特点是数据精准、细致,适合查看某一次请求的调用链路,一般用于查看某些响应较慢的接口瓶颈。
要实现服务追踪,我们有三点问题需要解决:
通过分布式追踪系统能很好地定位如下请求的每条具体请求链路,从而轻易地实现请求链路追踪:
那么,有没有一种规范能让大家都很好的进行链路追踪系统的开发呢?
为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范,OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。
Opentracing是分布式链路追踪的一种规范标准,具有平台无关的特性,开发者只需修改少量的配置代码,就可以在符合Opentracing标准的链路追踪系统之间自由切换。
被跟踪的服务只需要调用Opentracing接口,就可以被任何实现这套接口的跟踪后台(比如Zipkin, Jaeger等等)支持,而作为一个跟踪后台,只要实现了个这套接口,就可以跟踪到任何调用这套接口的服务。同时,Opentracing对进程间跟踪数据传递与追踪的最小独立单位Span进行了标准化。
Span是一条追踪链路中的基本组成要素,也是最小独立单位。一个span表示一个独立的工作单元,比如可以表示一次函数调用,一次http请求等等。
span中包含以下信息:
一个完整请求链路,每个调用链由多个 Span 组成。
Trace 的全局上下文信息, 如里面有包含着traceId。
大家可以看我这张图来了解数据结构,一次请求的完整请求完整就是一个Trace, 显然对于这个请求来说,必须要有一个全局标识来标识这一个请求TraceId,每一次调用就称为一个 Span每一次调用都要带上全局的TraceId和自身的SpanId, 这样才可把全局 TraceId 与每个调用关联起来。
了解到OpenTracing的数据结构之后。我们首先要收集服务Tacre的数据。
与单体服务中采用埋点的方式不同,现在主流的zipkin和SkyWalking采用了不同的方法收集服务链路的数据:
收集到的数据的内容如下图所示:
如果对每个请求调用都采集,那毫无疑问数据量会非常大。对于已经上线的服务而言,每一次请求的情况都是差不多的(如果出现异常报错当然另说)。其实没有必要对每个请求都采集,我们可以设置采样频率,只采样部分数据,SkyWalking中就默认设置了3秒只采样三次。这样的频率其实足够我们分析性能了(小声哔哔:其实大多数业务根本没有那么多的请求)。同时,为了保证全局一致性,上游服务采样了(SpanContext有说明),那么下游服务一定也需要采样,不然很有可能出现不一致的情况。
得到这些数据以后,对链路数据进行可视化展示就能实现服务追踪了。
Zipkin是一款开源的分布式实时数据追踪系统,基于Google Dapper的论文设计而来,由Twitter公司开发贡献,它的架构图如下:
但是光有Zipkin还不够,Spring Cloud为我们提供了Sleuth这个组件,它可以为服务之间调用提供错误补货,耗时分析和链路追踪。
Spring Cloud Sleuth可以结合Zipkin,将信息发送到Zipkin,利用Zipkin的存储来存储信息,利用Zipkin Ui来展示数据(Sleuth 默认采用 Http 方式将 span 传输给 Zipkin)。
基于springboot来搭建zipkin还是比较简单的事情,在生成模块的时候已经有了完整的脚手架,只需要在maven(或者gradle等任何仓库)放上对应的依赖就行。
在zipkin项目中引入依赖
org.springframework.cloud
spring-cloud-starter-sleuth
org.springframework.cloud
spring-cloud-starter-zipkin
org.springframework.amqp
spring-rabbit
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
在service与client项目中也需要引入zipkin依赖
org.springframework.cloud
spring-cloud-starter-zipkin
为什么选择 RabbitMQ 消息中间件发送 span 信息
sleuth 默认采用 http 通信方式,将数据传给 zipkin 作页面渲染,但是 http 传输过程中如果由于不可抗因素导致 http 通信中断,那么此次通信的数据将会丢失。但是使用RabbitMQ的话,消息队列可以积压千万级别的消息,下次重连之后依然可以可以继续消费。随着线程增多,并发量提升之后,RabbitMQ 异步发送数据明显更具有优势。同时,RabbitMQ 支持消息、队列持久化,可以通过消息状态落库、重回队列、镜像队列等技术手段保证其高可用。具体可以参考RabbitMQ原理。
除了依赖方面,在yaml中,zipkin这样配置
server:
port: 9411
spring:
application:
name: zipkin-server
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
management:
metrics:
web:
server:
auto-time-requests: false
其它服务端中加入zipkin的配置
spring:
sleuth:
sampler:
probability: 1.0
zipkin:
base-url: http://localhost:9411
在使用 Spring Boot 2.x 版本后,官方就不推荐自行定制编译了,让我们直接使用编译好的 jar 包。所以我们在使用@EnableZipkinServer或@EnableZipkinStreamServer注解的时候要注意搭配好springboot的版本。
@SpringBootApplication
@EnableEurekaClient
@EnableZipkinServer
public class ZipkinServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinServerApplication.class, args);
}
}
搭建完毕以后,我们可以登录 http://localhost:9411/zipkin/ 地址进入zipkin的主页:
点击Find Traces就能看到较为简洁的显示Span的个数以及调用总时延。
我们进入一个完整的调用链后访问其中的一个节点得到以下数据:
然后在Dependencies的选项中能看到调用链路的图,当然,因为测试的原因,并没有建立很复杂的调用链路。
从这一块我们能够感知到,springcloud对zipkin的集成非常友好,不用另外启动什么jar包,可以直接集成在springcloud的环境中,这也是我选择用zipkin作为展示的原因。
要保证TraceId的全局唯一就可以了,这里我们可以参考 SOFATrace 中的设计,通过 8 位的 IP 地址和 13 位的时间戳,以及四位的自增序列,加上本身进程的 PID 号,这样组成的字符串就可以保证全局唯一了。
IP地址 | 生产ID的时间 | 自增序列 | 进程PID |
---|---|---|---|
8位 | 13位 | 4位 | 可变序列 |
0ad1348f | 1403169275002 | 1003 | 56696 |
举个例子,可以使用消息中间件优化:安装rabbitmaq,修改zipkin服务器的启动方式,从rabbit拉取消息。
修改客户端以rabbitmq的形式向mq发送消息(在需要收集日志的微服务中添加依赖,修改配置)
实际上 Trace 的采样率一直是个头疼的问题,设置太低可能有些接口采集不到有效的信息,设置得太高又会造成成本过高的问题。在 Jaeger 中提供了动态采样率的功能,这样可以保证同一个服务中,低 QPS 的接口可以被有效采集,而高 QPS 的接口有较低的采样率。
Zipkin是Twitter开源的调用链分析工具,目前基于springcloud sleuth得到了广泛的使用,特点是轻量,使用部署简单。
除此之外还有:
类别 | Zipkin | SkyWalking | Pinpoint | CAT |
---|---|---|---|---|
实现方式 | 拦截请求,发送(HTTP,mq)数据至zipkin服务 | java探针,字节码增强 | java探针,字节码增强 | 代码埋点(拦截器,注解,过滤器等) |
接入方式 | 基于linkerd或者sleuth方式,引入配置即可 | javaagent字节码 | javaagent字节码 | 代码埋点(拦截器,注解,过滤器等) |
代码侵入 | http,MQ | gRPC | thrift | http/tcp |
数据存储 | ES,mysql,Cassandra,内存 | ES,H2 | Hbase | mysql,hdfs |
颗粒度 | 接口级 | 方法级 | 方法级 | 代码级 |
社区活跃度 | 高 | 低 | 中 | 中 |
至于开发选型的问题,大家可以根据自己的产品实际选择适合的链路追踪系统,可以参考这篇知乎文章监控系统比较 Skywalking Pinpoint Cat zipkin