微服务架构中完成一项功能常常会在多个服务之间远程调用(RPC),造成调用链。每一个服务节点可能在不一样的机器上甚至是不一样的集群上,须要能追踪整个调用链,以便在服务调用出错或延时较高时准肯定位问题。
如下内容引用 Dapper,大规模分布式系统的跟踪系统 译文 ,介绍了分布式服务追踪的重要性以及设计原则:java
当代的互联网的服务,一般都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不一样的软件模块集上,这些软件模块,有多是由不一样的团队开发、可能使用不一样的编程语言来实现、有可能布在了几千台服务器,横跨多个不一样的数据中心。所以,就须要一些能够帮助理解系统行为、用于分析性能问题的工具。
举一个跟搜索相关的例子,这个例子阐述了Dapper能够应对哪些挑战。好比一个前段服务可能对上百台查询服务器发起了一个Web查询,每个查询都有本身的Index。这个查询可能会被发送到多个的子系统,这些子系统分别用来处理广告、进行拼写检查或是查找一些像图片、视频或新闻这样的特殊结果。根据每一个子系统的查询结果进行筛选,获得最终结果,最后汇总到页面上。咱们把这种搜索模型称为“全局搜索”(universal search)。总的来讲,这一次全局搜索有可能调用上千台服务器,涉及各类服务。并且,用户对搜索的耗时是很敏感的,而任何一个子系统的低效都致使致使最终的搜索耗时。若是一个工程师只能知道这个查询耗时不正常,可是他无从知晓这个问题究竟是由哪一个服务调用形成的,或者为何这个调用性能差强人意。首先,这个工程师可能没法准确的定位到此次全局搜索是调用了哪些服务,由于新的服务、乃至服务上的某个片断,都有可能在任什么时候间上过线或修改过,有多是面向用户功能,也有多是一些例如针对性能或安全认证方面的功能改进。其次,你不能苛求这个工程师对全部参与此次全局搜索的服务都了如指掌,每个服务都有多是由不一样的团队开发或维护的。再次,这些暴露出来的服务或服务器有可能同时还被其余客户端使用着,因此此次全局搜索的性能问题甚至有多是由其余应用形成的。举个例子,一个后台服务可能要应付各类各样的请求类型,而一个使用效率很高的存储系统,好比Bigtable,有可能正被反复读写着,由于上面跑着各类各样的应用。
上面这个案例中咱们能够看到,对Dapper咱们只有两点要求:无所不在的部署,持续的监控。无所不在的重要性不言而喻,由于在使用跟踪系统的进行监控时,即使只有一小部分没被监控到,那么人们对这个系统是否是值得信任都会产生巨大的质疑。另外,监控应该是7x24小时的,毕竟,系统异常或是那些重要的系统行为有可能出现过一次,就很难甚至不太可能重现。那么,根据这两个明确的需求,咱们能够直接推出三个具体的设计目标:mysql
在 spring cloud 技术栈中, spring cloud Sleuth 借鉴了 Google Dapper 的实现, 提供了分布式服务追踪的解决方案。git
Spring Cloud Sleuth 提供了两种追踪信息收集的方式,一种是经过 http 的方式,一种是经过 异步消息 的方式,这里以生产环境经常使用的 异步消息 的收集方式为例。
在以前建立的项目上作修改,增长 Spring Cloud Sleuth 分布式服务追踪功能。
修改 add-service-demo
的 pom.xml
文件,增长相关依赖:程序员
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency>
spring-cloud-starter-sleuth
引入 sleuth 基础jar包, spring-cloud-sleuth-stream
和 spring-cloud-stream-binder-rabbit
引入经过 异步消息 收集追踪信息的相关jar包,spring-cloud-starter-feign
引入了 feign,用来远程调用别的服务(在 基于docker部署的微服务架构(二): 服务提供者和调用者 中有介绍),稍后会建立一个提供随机数的服务,用来展现服务调用链。
而后修改 log4j2.xml
配置文件, 修改日志格式为:github
<Property name="PID">????</Property> <Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property> <Property name="LOG_LEVEL_PATTERN">%5p</Property> <Property name="logFormat"> %d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN} [@project.artifactId@,%X{X-B3-TraceId},%X{X-B3-SpanId},%X{X-Span-Export}] ${sys:PID} --- [%15.15t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} </Property>
在日志信息中增长用来追踪的 TraceId、SpanId ,Export 表示是否导出到 zipkin 。
以前在 基于docker部署的微服务架构(四): 配置中心 的内容中已经配置了 rabbitmq,用于 spring cloud bus,因此这里就不用再配消息队列了,用以前配置的 rabbitmq 就能够了。
这时候启动 add-service-demo 工程,能够看到控制台输出的日志信息增长了 TraceId、SpanId 的相关信息,INFO [add-service-demo,,,] 18668
,可是如今尚未具体的内容,由于没有发生服务调用。web
建立一个新的工程 random-service-demo
,用来生成一个随机整数。新建 maven 项目,修改 pom.xml 文件,引入相关依赖:spring
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.10.0.1</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <properties> <!-- 指定java版本 --> <java.version>1.8</java.version> <!-- 镜像前缀,推送镜像到远程库时须要,这里配置了一个阿里云的私有库 --> <docker.image.prefix> registry.cn-hangzhou.aliyuncs.com/ztecs </docker.image.prefix> <!-- docker镜像的tag --> <docker.tag>demo</docker.tag> <!-- 激活的profile --> <activatedProperties></activatedProperties> <kafka.bootstrap.servers>10.47.160.238:9092</kafka.bootstrap.servers> </properties>
这里一样引入了 Sleuth 相关内容。
建立启动入口类 RandomServiceApplication.java
:sql
@EnableDiscoveryClient @SpringBootApplication public class RandomServiceApplication { public static void main(String[] args) { SpringApplication.run(RandomServiceApplication.class, args); } }
resources 中的配置文件能够彻底复用 add-service-demo 中的配置,由于最终的配置是从 配置中心 中拉取的, resources 只须要配置 config-server 的相关内容便可。
在 git 仓库中增长 random-service-demo-dev.yml
配置文件,内容:docker
server: port: 8200 spring: rabbitmq: host: 10.47.160.238 port: 5673 username: guest password: guest
配置了端口和消息队列。apache
建立一个 RandomController.java
对外提供随机数服务:
@RestController @RefreshScope public class RandomController { private static final Logger logger = LoggerFactory.getLogger(RandomController.class); @RequestMapping(value = "/random", method = RequestMethod.GET) public Integer random() { logger.info(" >>> random"); Random random = new Random(); return random.nextInt(10); } }
业务逻辑很简单,生成一个 0 ~ 10 的随机整数并返回。
接下来在 add-service-demo 工程中增长一个随机数相加的接口,调用 random-service-demo 生成随机数,并把随机数相加做为结果返回。
在 AddServiceApplication.java
中增长 @EnableFeignClients
注解,开启 feign 客户端远程调用。
增长 RandomService.java
用来远程调用 random-service-demo 中的接口:
@FeignClient("RANDOM-SERVICE-DEMO") public interface RandomService { @RequestMapping(method = RequestMethod.GET, value = "/random") Integer random(); }
在 AddController.java
中增长 randomAdd 方法,并对外暴露接口。在方法中两次调用 random-service-demo 生成随机数的接口,把随机数相加做为结果返回:
@Autowired private RandomService randomService; private static Logger logger = LoggerFactory.getLogger(AddController.class); @RequestMapping(value = "/randomAdd", method = RequestMethod.GET) public Map<String, Object> randomAdd() { logger.info(">>> randomAdd"); Integer random1 = randomService.random(); Integer random2 = randomService.random(); Map<String, Object> returnMap = new HashMap<>(); returnMap.put("code", 200); returnMap.put("msg", "操做成功"); returnMap.put("result", random1 + random2); return returnMap; }
修改服务网关 service-gateway-demo 引入 sleuth, 修改 pom.xml 引入依赖(参照 add-service-demo ),修改 log4j2.xml 中的日志格式(参照 add-service-demo )。
启动 add-service-demo 、 random-service-demo 、 service-gateway-demo ,经过网关调用接口 http://localhost/add-service/randomAdd。查看日志能够发现 从 service-gateway-demo 到 add-service-demo 再到 random-service-demo 中输出的日志信息,包含相同的 TraceId ,代表处于一个调用链。
经过上边的配置,在服务调用的过程当中 spring cloud sleuth 自动帮咱们添加了 TraceId 、 SpanId 等服务追踪须要的内容。如今还须要集中收集这些信息,并提供可视化界面把这些信息展现出来。
Zipkin 是 Twitter 的一个开源项目,容许开发者收集各个服务上的监控数据,并提供查询接口。spring cloud sleuth 对 zipkin 作了封装,提供了两种数据保存方式:内存和 mysql ,这里以生产环境中使用的 mysql 持久化方式为例。
建立一个 maven 工程 zipkin-server-demo,修改 pom.xml 文件增长相关依赖:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.10.0.1</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <properties> <!-- 指定java版本 --> <java.version>1.8</java.version> <!-- 镜像前缀,推送镜像到远程库时须要,这里配置了一个阿里云的私有库 --> <docker.image.prefix> registry.cn-hangzhou.aliyuncs.com/ztecs </docker.image.prefix> <!-- docker镜像的tag --> <docker.tag>demo</docker.tag> <!-- 激活的profile --> <activatedProperties></activatedProperties> <kafka.bootstrap.servers>10.47.160.238:9092</kafka.bootstrap.servers> </properties>
简单说明下引入的依赖:spring-cloud-sleuth-zipkin-stream
引入了经过消息驱动的方式收集追踪信息所须要的 zipkin 依赖, spring-cloud-starter-sleuth
、spring-cloud-stream-binder-rabbit
,这两个和以前项目中引入的同样,都是消息驱动的 sleuth 相关依赖。zipkin-autoconfigure-ui
引入了 zipkin 相关依赖,最后引入了 mysql 和 jdbc 的依赖,用于保存追踪数据。
在 resources 目录中新建配置文件 application.yml :
server: port: 9411 spring: profiles: active: @activatedProperties@ rabbitmq: host: 10.47.160.114 port: 5673 username: guest password: guest datasource: schema: classpath:/mysql.sql url: jdbc:mysql://10.47.160.114:3306/sleuth_log username: soa password: 123456 initialize: true continueOnError: true sleuth: enabled: false output: ansi: enabled: ALWAYS zipkin: storage: type: mysql
配置了 zipkin web页面的端口 9411
,配置 mysql 和初始化脚本, 并指定 zipkin.storage.type
为 mysql。
在 resources 目录中建立 mysql 初始化脚本 mysql.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'; 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 保存追踪数据须要的表。
新建 log4j2.xml 配置文件,能够把其余项目中的复制过来( add-service-demo 等),内容都是同样的。
建立启动入口类 ZipkinServerApplication.java
:
@SpringBootApplication @EnableZipkinStreamServer public class ZipkinServerApplication { public static void main(String[] args) { SpringApplication.run(ZipkinServerApplication.class, args); } }
运行 main 方法启动 zipkin,访问 http://localhost:9411 打开页面。
有可能在 zipkin 中查询不到数据,这是由于 sleuth 有一个采样率的概念,并不会发送全部的数据,能够经过配置 spring.sleuth.sampler.percentage
指定数据采样的百分比。
重复屡次访问 http://localhost/add-service/randomAdd 调用接口,就能在 zipkin 中查询到数据了。
还能够查看服务间的调用链:
这部份内容和前面几篇文章基本相同,都是把容器间的访问地址和 --link
参数对应,再也不赘述。
若是使用 ELK 进行日志分析的话,可使用 grok 插件解析 spring cloud sleuth 追踪系统的日志信息(关于 ELK 系统的部署,能够参阅 基于docker部署的微服务架构(七): 部署ELK日志统计分析系统 )。
修改 logstash 的配置文件,增长 grok filter:
filter { grok { match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span},%{DATA:exportable}\]\s+%{DATA:pid}---\s+\[%{DA TA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } } }
这样就能够解析日志信息了。
分布式服务追踪在微服务架构中是很是重要的一部分,在发生异常时须要经过追踪系统来定位问题。Spring Cloud Sleuth
基于 Google Dapper 提供了一个简单易用的分布式追踪系统。
在生产环境中,只有追踪系统还不够,在服务调用发生错误时,好比:网络延时、资源繁忙等,这种错误每每会形成阻塞,形成后续访问困难,在高并发状况下,调用服务失败时若是没有隔离措施,会波及到整个服务端,进而使整个服务端崩溃。
因此还须要一个熔断系统,对服务依赖作隔离和容错。下一篇将会介绍 hystrix 熔断系统。