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

原创
2016/11/28 11:08
阅读数 3K

前言

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

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

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

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

引入 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 配置文件, 修改日志格式为:

    <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,但是现在还没有具体的内容,因为没有发生服务调用。

创建一个新的工程 random-service-demo,用来生成一个随机整数。新建 maven 项目,修改 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-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

  @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 配置文件,内容:

  server:
    port: 8200

  spring:
    rabbitmq:
    host: 10.47.160.238
    port: 5673
    username: guest
    password: guest

配置了端口和消息队列。

创建一个 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 熔断系统。

展开阅读全文
加载中

作者的其它热门文章

打赏
2
51 收藏
分享
打赏
3 评论
51 收藏
2
分享
返回顶部
顶部
返回顶部
顶部