基于docker部署的微服务架构(九): 分布式服务追踪 Spring Cloud Sleuth

前言

微服务架构中完成一项功能常常会在多个服务之间远程调用(RPC),造成调用链。每一个服务节点可能在不一样的机器上甚至是不一样的集群上,须要能追踪整个调用链,以便在服务调用出错或延时较高时准肯定位问题。
如下内容引用 Dapper,大规模分布式系统的跟踪系统 译文 ,介绍了分布式服务追踪的重要性以及设计原则:java

当代的互联网的服务,一般都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不一样的软件模块集上,这些软件模块,有多是由不一样的团队开发、可能使用不一样的编程语言来实现、有可能布在了几千台服务器,横跨多个不一样的数据中心。所以,就须要一些能够帮助理解系统行为、用于分析性能问题的工具。
举一个跟搜索相关的例子,这个例子阐述了Dapper能够应对哪些挑战。好比一个前段服务可能对上百台查询服务器发起了一个Web查询,每个查询都有本身的Index。这个查询可能会被发送到多个的子系统,这些子系统分别用来处理广告、进行拼写检查或是查找一些像图片、视频或新闻这样的特殊结果。根据每一个子系统的查询结果进行筛选,获得最终结果,最后汇总到页面上。咱们把这种搜索模型称为“全局搜索”(universal search)。总的来讲,这一次全局搜索有可能调用上千台服务器,涉及各类服务。并且,用户对搜索的耗时是很敏感的,而任何一个子系统的低效都致使致使最终的搜索耗时。若是一个工程师只能知道这个查询耗时不正常,可是他无从知晓这个问题究竟是由哪一个服务调用形成的,或者为何这个调用性能差强人意。首先,这个工程师可能没法准确的定位到此次全局搜索是调用了哪些服务,由于新的服务、乃至服务上的某个片断,都有可能在任什么时候间上过线或修改过,有多是面向用户功能,也有多是一些例如针对性能或安全认证方面的功能改进。其次,你不能苛求这个工程师对全部参与此次全局搜索的服务都了如指掌,每个服务都有多是由不一样的团队开发或维护的。再次,这些暴露出来的服务或服务器有可能同时还被其余客户端使用着,因此此次全局搜索的性能问题甚至有多是由其余应用形成的。举个例子,一个后台服务可能要应付各类各样的请求类型,而一个使用效率很高的存储系统,好比Bigtable,有可能正被反复读写着,由于上面跑着各类各样的应用。
上面这个案例中咱们能够看到,对Dapper咱们只有两点要求:无所不在的部署,持续的监控。无所不在的重要性不言而喻,由于在使用跟踪系统的进行监控时,即使只有一小部分没被监控到,那么人们对这个系统是否是值得信任都会产生巨大的质疑。另外,监控应该是7x24小时的,毕竟,系统异常或是那些重要的系统行为有可能出现过一次,就很难甚至不太可能重现。那么,根据这两个明确的需求,咱们能够直接推出三个具体的设计目标:mysql

  1. 低消耗:跟踪系统对在线服务的影响应该作到足够小。在一些高度优化过的服务,即便一点点损耗也会很容易察觉到,并且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
  2. 应用级的透明:对于应用的程序员来讲,是不须要知道有跟踪系统这回事的。若是一个跟踪系统想生效,就必须须要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,每每因为跟踪系统在应用中植入代码的bug或疏忽致使应用出问题,这样才是没法知足对跟踪系统“无所不在的部署”这个需求。面对当下想Google这样的快节奏的开发环境来讲,尤为重要。
  3. 延展性:Google至少在将来几年的服务和集群的规模,监控系统都应该能彻底把控住。
  4. 一个额外的设计目标是为跟踪数据产生以后,进行分析的速度要快,理想状况是数据存入跟踪仓库后一分钟内就能统计出来。尽管跟踪系统对一小时前的旧数据进行统计也是至关有价值的,但若是跟踪系统能提供足够快的信息反馈,就能够对生产环境下的异常情况作出快速反应。

spring cloud 技术栈中, spring cloud Sleuth 借鉴了 Google Dapper 的实现, 提供了分布式服务追踪的解决方案。git

引入 Spring Cloud Sleuth 追踪系统

Spring Cloud Sleuth 提供了两种追踪信息收集的方式,一种是经过 http 的方式,一种是经过 异步消息 的方式,这里以生产环境经常使用的 异步消息 的收集方式为例。
在以前建立的项目上作修改,增长 Spring Cloud Sleuth 分布式服务追踪功能。
修改 add-service-demopom.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-streamspring-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>

在日志信息中增长用来追踪的 TraceIdSpanIdExport 表示是否导出到 zipkin
以前在 基于docker部署的微服务架构(四): 配置中心 的内容中已经配置了 rabbitmq,用于 spring cloud bus,因此这里就不用再配消息队列了,用以前配置的 rabbitmq 就能够了。
这时候启动 add-service-demo 工程,能够看到控制台输出的日志信息增长了 TraceIdSpanId 的相关信息,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.javasql

@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-demorandom-service-demoservice-gateway-demo ,经过网关调用接口 http://localhost/add-service/randomAdd。查看日志能够发现 从 service-gateway-demoadd-service-demo 再到 random-service-demo 中输出的日志信息,包含相同的 TraceId ,代表处于一个调用链。

使用zipkin收集追踪信息并展示

经过上边的配置,在服务调用的过程当中 spring cloud sleuth 自动帮咱们添加了 TraceIdSpanId 等服务追踪须要的内容。如今还须要集中收集这些信息,并提供可视化界面把这些信息展现出来。
ZipkinTwitter 的一个开源项目,容许开发者收集各个服务上的监控数据,并提供查询接口。spring cloud sleuthzipkin 作了封装,提供了两种数据保存方式:内存和 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-sleuthspring-cloud-stream-binder-rabbit,这两个和以前项目中引入的同样,都是消息驱动的 sleuth 相关依赖。zipkin-autoconfigure-ui 引入了 zipkin 相关依赖,最后引入了 mysqljdbc 的依赖,用于保存追踪数据。

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.typemysql
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

有可能在 zipkin 中查询不到数据,这是由于 sleuth 有一个采样率的概念,并不会发送全部的数据,能够经过配置 spring.sleuth.sampler.percentage 指定数据采样的百分比。
重复屡次访问 http://localhost/add-service/randomAdd 调用接口,就能在 zipkin 中查询到数据了。
zipkin data

zipkin data 2

还能够查看服务间的调用链: zipkin dependency

使用docker-maven-plugin打包并生成docker镜像

这部份内容和前面几篇文章基本相同,都是把容器间的访问地址和 --link 参数对应,再也不赘述。

demo源码 spring-cloud-4.0目录

grok插件解析日志内容

若是使用 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 熔断系统。

相关文章
相关标签/搜索