基于SpringBoot 2.x+Sleuth+Zipkin(R
背景
无论你的系统是庞大的单应用架构,还是采用的微服务架构,只要是调用过程非常复杂,都会存在如何追踪各个方法或者服务间调用异常如何被开发和运维人员快速定位的问题,为了解决这个问题,你势必需要记录大量的日志。
举个例子,对于一个大流量的Web应用通常以Stateless方式设计,这样可以更方便的进行水平扩容。但是随着应用实例数量越来越多,我们查询日志就越来越困难。在没有日志系统的情况下,首先我们需要定位到请求的服务器地址,如果每台服务器都部署了多个应用实例,我们则需要去每个应用实例的日志目录下去找日志文件。每个服务可能还会设置日志滚动策略(如:每200M一个文件),还有日志压缩归档策略。
如此,我们查询一条出错信息就要在茫茫多的日志文件里去找到它,于是使出我们的十八般武艺head less tail grep wc awk count cut,但是如果需要统计最近3天的某个接口的异常次数,或者超时次数。。。。除了上面出现的状况我们还需要考虑:日志量太大如何归档、文本搜索太慢怎么办?
但是对于故障排查肯定是希望能够快速的进行日志查询、定位、解决问题,对于实时性要求非常高。为了解决这个问题,日志追踪系统应运而生。
概念
先介绍一个概念:分布式跟踪,或分布式追踪。
电商平台由数以百计的分布式服务构成,每一个请求路由过来后,会经过多个业务系统并留下足迹,并产生对各种Cache或DB的访问,但是这些分散的数据对于问题排查,或是流程优化都帮助有限。对于这么一个跨进程/跨线程的场景,汇总收集并分析海量日志就显得尤为重要。要能做到追踪每个请求的完整调用链路,收集调用链路上每个服务的性能数据,计算性能数据和比对性能指标(SLA),甚至在更远的未来能够再反馈到服务治理中,那么这就是分布式跟踪的目标了。在业界,Twitter 的 zipkin 和淘宝的鹰眼就是类似的系统,它们都起源于 Google Dapper 论文。
整理一下,Google叫Dapper,淘宝叫鹰眼,Twitter叫ZipKin,京东商城叫Hydra,eBay叫Centralized Activity Logging (CAL),大众点评网叫CAT,其底层实现的追踪逻辑,几乎都是一样的。
分布式追踪系统的设计理念
(1)低侵入性——作为非业务组件,应当尽可能少侵入或者无侵入其他业务系统,对于使用方透明,减少开发人员的负担;
(2)灵活的应用策略——可以(最好随时)决定所收集数据的范围和粒度;
(3)时效性——从数据的收集和产生,到数据计算和处理,再到最终展现,都要求尽可能快;
(4)决策支持——这些数据是否能在决策支持层面发挥作用,特别是从 DevOps 的角度;
(5)可视化才是王道。
Zipkin出场
在众多的追踪系统中,本文只是对Zipkin的应用做说明。
最初接触Zipkin,是在《Spring Cloud 微服务实战》这本书中的 Spring Cloud Sleuth章节,按照这个章节写的例子学习下来,踩了无数个坑,弄的我是云里雾里。索性放弃书上的例子,直接看 Zipkin官网(https://zipkin.io)和Spinrg官网中关于Cloud Sleuth章节(https://cloud.spring.io/spring-cloud-static/spring-cloud-sleuth/2.1.2.RELEASE/multi/multi_spring-cloud-sleuth.html),同时查阅了多个网络上的文章,才发现随着Spring boot 2.X的问世,Spring Cloud Sleuth关于Zipkin的实现方式发生了非常大的变化。(在写这篇文章时,Zipkin的版本是V2.14.0)
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 就可以了。你可能会问:spring-cloud-starter-sleuth不需要引入了吗? 我的回答是:需要,但它会被spring-cloud-starter-zipkin间接依赖而自动引入的。
Sleuth with Zipkin over RabbitMQ or Kafka :最后一种使用方式是,如果你不想通过HTPP方式集成ZipkinServer,而是通过RabbitMQ或者Kafka这些消息中间件做异步消息处理的话(笔者推荐这种用法),你除了需要引入spring-cloud-starter-zipkin,还需要引入spring-rabbit(本文以Rabbit为例)。还有一点就是:如果使用了消息中间件传递消息,那么Zipkin的服务端也需要做相应的配置,才能监听处理消息。
RabbitMQ相关参数配置
spring:
rabbitmq:
host: 10.10.10.10
port: 5672
username: guest
password: guest
同时,在启动Zipkin服务端的时候,添加以上相同的Rabiit配置
D:\>java -jar zipkin.jar --zipkin.collector.rabbitmq.addresses=10.10.10.10
端口如果不写默认是5672,用户名密码也是采用的默认值,如果你的配置不是采用默认值,需要维护相应参数。
启动服务
启动集成了Sleuth的服务和Zipkin 服务端,进行访问带多级调用的服务(省略具体操作),打开Zipkin的UI界面http://localhost:9411/zipkin/ ,如下图:
可以看到,刚才的调用链已经可以查询到了。点击某条记录,进入调用详细页面。
可以看到每个服务的调用时间,时长,是否异常等信息。这样,你就可以快速定位那些响应时间超长的请求或者发生异常的请求。
存储
Zipkin最初是为在Cassandra上存储数据而构建的,因为Cassandra是可扩展的,有一个灵活的模式,并且在Twitter中广泛使用。然而,我们使这个组件可插拔。除了Cassandra之外,Zipkin的存储还支持ElasticSearch和MySQL。
要想把记录的信息存储到Mysqls,你需要在启动Zipkin Server的时候添加如下的参数:
D:\>java -jar zipkin.jar --zipkin.collector.rabbitmq.addresses=10.10.10.10 --STORAGE_TYPE=mysql --MYSQL_DB=zipkin --MYSQL_HOST=10.10.1.10 --MYSQL_TCP_PORT=3306 --MYSQL_USER=root --MYSQL_PASS=123456
并且,要确保在zipkin库下,已经存在了需要用到的表。建表语句需要与Zipkin Server的版本相对应,避免未知错误:
CREATETABLEIFNOTEXISTSzipkin_spans (
`trace_id_high`BIGINTNOTNULLDEFAULT0COMMENT'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id`BIGINTNOTNULL,
`id`BIGINTNOTNULL,
`name`VARCHAR(255)NOTNULL,
`remote_service_name`VARCHAR(255),
`parent_id`BIGINT,
`debug`BIT(1),
`start_ts`BIGINTCOMMENT'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration`BIGINTCOMMENT'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARYKEY(`trace_id_high`,`trace_id`,`id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSEDCHARACTERSET=utf8 COLLATE utf8_general_ci;
ALTERTABLEzipkin_spans ADD INDEX(`trace_id_high`,`trace_id`) COMMENT'for getTracesByIds';
ALTERTABLEzipkin_spans ADD INDEX(`name`) COMMENT'for getTraces and getSpanNames';
ALTERTABLEzipkin_spans ADD INDEX(`remote_service_name`) COMMENT'for getTraces and getRemoteServiceNames';
ALTERTABLEzipkin_spans ADD INDEX(`start_ts`) COMMENT'for getTraces ordering and range';
CREATETABLEIFNOTEXISTSzipkin_annotations (
`trace_id_high`BIGINTNOTNULLDEFAULT0COMMENT'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id`BIGINTNOTNULLCOMMENT'coincides with zipkin_spans.trace_id',
`span_id`BIGINTNOTNULLCOMMENT'coincides with zipkin_spans.id',
`a_key`VARCHAR(255)NOTNULLCOMMENT'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value`BLOB COMMENT'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type`INTNOTNULLCOMMENT'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp`BIGINTCOMMENT'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4`INTCOMMENT'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`SMALLINTCOMMENT'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name`VARCHAR(255) COMMENT'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSEDCHARACTERSET=utf8 COLLATE utf8_general_ci;
ALTERTABLEzipkin_annotations ADD UNIQUEKEY(`trace_id_high`,`trace_id`,`span_id`,`a_key`,`a_timestamp`) COMMENT'Ignore insert on duplicate';
ALTERTABLEzipkin_annotations ADD INDEX(`trace_id_high`,`trace_id`,`span_id`) COMMENT'for joining with zipkin_spans';
ALTERTABLEzipkin_annotations ADD INDEX(`trace_id_high`,`trace_id`) COMMENT'for getTraces/ByIds';
ALTERTABLEzipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT'for getTraces and getServiceNames';
ALTERTABLEzipkin_annotations ADD INDEX(`a_type`) COMMENT'for getTraces and autocomplete values';
ALTERTABLEzipkin_annotations ADD INDEX(`a_key`) COMMENT'for getTraces and autocomplete values';
ALTERTABLEzipkin_annotations ADD INDEX(`trace_id`,`span_id`,`a_key`) COMMENT'for dependencies job';
CREATETABLEIFNOTEXISTSzipkin_dependencies (
`day`DATENOTNULL,
`parent`VARCHAR(255)NOTNULL,
`child`VARCHAR(255)NOTNULL,
`call_count`BIGINT,
`error_count`BIGINT,
PRIMARYKEY(`day`,`parent`,`child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSEDCHARACTERSET=utf8 COLLATE utf8_general_ci;