《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

一、为什么需要 Spring Cloud Sleuth

微服务架构是一个分布式架构,微服务系统按业务划分服务单元,一个微服务系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性较高,如果出现了错误和异常,很难去定位。主要体现在一个请求可能需要调用很多个服务,而内部服务的调用复杂性决定了问题难以定位。所以在微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出现问题能够快速定位的目的

Google 开源了 Dapper 链路追踪组件,并在 2010 年发表了论文《Dapper,a Large-Scale Distributed Systems Tracing Infrastructure》,这篇论文是业内实现链路追踪的标杆和理论基础,具有很高的参考价值

目前,常见的链路追踪组件有 Google 的 Dapper、Twitter 的 Zipkin,以及阿里的 Eagleeye(鹰眼)等,它们都是非常优秀的链路追踪开源组件

本章主要讲述如何在 Spring Cloud Sleuth 中集成 Zipkin。在 Spring Cloud Sleuth 中集成 Zipkin 非常简单,只需要引入相应的依赖并做相关的配置即可

二、基本术语

Spring Cloud Sleuth 采用了 Google 的开源项目 Dapper 的专业术语

Span
基本工作单元,发送一个远程调度任务就会产生一个 Span,Span 是用一个 64 位 ID 唯一标识的,Trace 是用另一个 64 位 ID 唯一标识的。Span 还包含了其它的信息,例如摘要、时间戳时间、Span 的 ID 以及进程 ID

Trace
由一系列 Span 组成的,呈树状结构。请求一个微服务系统的 API 接口,这个API 接口需要调用多个微服务单元,调用每个微服务单元都会产生一个新的 Span,所有由这个请求产生的 Span 组成了这个 Trace

Annotation
用于记录一个事件,一些核心注解用于定义一个请求的开始和结束,这些注解如下:

  • cs-Client Sent:客户端发送一个请求,这个注解描述了 Span 的开始
  • sr-Server Received:服务端获得请求并准备开始处理它,如果将其 sr 减去 cs 时间戳,便可得到网络传输的时间
  • ss-Server Sent:服务端发送响应,该注解表明请求处理的完成(当请求返回客户端),用 ss 的时间戳减去 sr 时间戳,便可以得到服务器请求的时间
  • cr-Client Received:客户端接收响应,此时 Span 结束,用 cr 的时间戳减去 cs 时间戳,便可以得到整个请求所消耗的时间

Spring Cloud Sleuth 提供了一套完整的链路解决方案,它可以结合 Zipkin,将链路数据发送到 Zipkin,并利用 Zipkin 来存储链路信息,也可以利用 Zipkin UI 来展示数据

那么什么是 Zipkin 呢?

Zipkin 是 Twitter 的一个开源项目,它基于 Google 的 Dapper 实现,被业界广泛使用。Zipkin 致力于收集分布式系统的链路数据,提供了数据持久化策略,也提供面向开发者的 API 接口,用于查询数据,还提供了 UI 组件帮助我们查看具体的链路信息

Zipkin 提供了可插拔式的数据存储方式,目前支持的数据存储由 In-Memory、MySQL、Cassandra 和 ElasticSearch

Zipkin 的架构如图所示:

在这里插入图片描述

它主要由 4 个核心组件构成:

  • Collector:链路数据收集器,主要用于处理从链路客户端发送过来的链路数据,将这些数据转换为 Zipkin 内部处理的 Span 格式,以支持后续的存储、分析和展示等功能
  • Storage:存储组件,用来存储接收到的链路数据,默认会将这些数据存储在内存中,同时支持多种存储策略,比如将链路数据存储在 M有SQL、Cassandra 和 ElasticSearch 中
  • RESTful API:API 组件,它是面向开发者的,提供外部访问 API 接口,可以通过这些 API 接口来自定义展示界面
  • WEB UI:UI 组件,基于 API 接口实现的上层应用,用户利用 UI 组件可以很方便地查询和分析链路数据

三、案例讲解

本章的案例一共有 4 个工程,基本信息如表所示:

应用名端口作用
eureka-server8761注册中心
eureka-client8763服务提供者、链路追踪客户端
eureka-client-feign8765服务消费者、链路追踪客户端
Zipkin(以 Jar 包的形式启动)9411链路追踪服务端

和之前的工程一样,采用 Maven 工程的多 Module 形式。新建一个主 Maven 工程,在主 Maven 工程的 pom 文件里指定 Spring Boot 的版本为 2.1.0,Spring Cloud 版本为 Greenwich。RELEASE。eureka-server 工程作为服务注册中心,zipkin-server 作为链路追踪服务中心,负责存储来南路数据,Spring Cloud 从 Edgware 版本开始,使用 Jar 包的形式启动,Jar 包需要从官方下载,用户不需要额外创建工程。eureka-client 工程是一个服务提供者,对外暴露 API 接口,同时它也作为链路追踪客户端,产生链路数据,并将链路数据传递给服务消费者。eureka-feign-client 作为服务消费者,调用 eureka-client 提供的服务,同时它也作为链路追踪客户端,产生链路数据,并将链路数据上传给链路追踪服务中心 zipkin-server

1.启动 Zipkin Server

eureka-server 的构建流程就不再一一演示,直接启动 Zipkin Server

在 Spring Cloud Dalston 版本中,zipkin-server 可以通过引入相关依赖的方式构建工程。从 Edgware 版本之后,这一方式改变为强制采用官方提供的 Jar 包的形式启动。因此用户需要下载官方提供的 Jar 包,使用 Git Bash 通过以下命令一键启动

curl -sSl https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar

上述第一行命令会从 Zipkin 官网下载官方的 Jar 包,第二行命令用来运行 zipkin.jar 文件

通过 java -jar zipkin.jar 的方式启动之后,在浏览器上访问 localhost:9411,显示的界面如图所示:

在这里插入图片描述

2.构建服务提供者

在主 Maven 工程下建一个 Module 工程,取名为 eureka-client,作为服务提供者,对外暴露 API 接口。user-service 工程的 pom 文件继承了主 Maven 工程的 pom 文件,并引入了 Eureka 的起步依赖 spring-cloud-starter-netflix-eureka-client、WEB 起步依赖 spring-boot-starter-web,以及 Zipkin 的起步依赖 spring-cloud-starter-zipkin 和 sleuth 的起步依赖 spring-cloud-starter-sleuth,代码如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
</dependencies>

在程序的配置文件 application.yml 中,指定程序名为 eureka-client,端口号为 8763,服务注册地址 http://localhost:8761/eureka/,Zipkin Server 地址为 http://localhost:9411。spring-sleuth.sample.percentage 为 1.0,即以 100% 的概率将链路的数据上传给 Zipkin Server,在默认情况下,该值为 0.1。另外,需要通过配置 spring.sleuth.web.client.enable 为 “true” 来开启 Sleuth 的功能,具体配置文件代码如下:

server:
  port: 8763
  
spring:
  application:
    name: eureka-client
  sleuth:
    web:
      client:
        enabled: true
    sampler:
      probability: 1.0  #设置采样比例,1.0 即全部都需要,默认为 0.1
  zipkin:
    base-url: http://localhost:9411/  #指定 zipkin 服务器地址
    
eureka:
  client:
    service-url: 
      defaultZone: http://lcoalhost:8761/eureka/

在 HiController 类建一个 “/hi” 的 API 接口,对外提供服务,代码如下:

package com.sisyphus.controller;

import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HiController {
    
    @Autowired
    Tracer tracer;
    
    @Value("${server.port}")
    String port;
    
    public String home(@RequestParam String name){
        tracer.currentSpan().tag("name","sisyphus");
        return "hi " + name + ",i am from port:" + port;
    }
}

最后作为 Eureka Client,需要在程序的启动类 EurekaClientApplication 加上 @EnableEurekaClient 注解,开启 Eureka Client 的功能

3.构建服务消费者

新建一个名为 eureka-feign-client 的工程,这个工程作为服务消费者,使用 FeignClient 来消费服务;同时作为 Zipkin 客户端,将链路数据上传给 Zipkin Server。在工程的 pom 文件中除了需要继承主 Maven 工程的 pom 文件以外,还需引入如下的依赖,代码如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
</dependencies>

在工程的配置文件 application.yml 中,配置程序名为 eureka-feign-client,端口号为 8765,服务注册地址为 http://localhost:8761/eureka/。另外,设置 spring.sleuth.web.client.enable 为 “true” 来使 WEB 开启 Sleuth 功能;spring.sleuth.sampler.probability 可以被设置为小数;最大值为 1.0,标识链路数据 100% 收集到 zipkin-server;当设置为 0.1 时,表示以 10% 的概率收集链路数据;spring.zipkin.base-url 设置 zipkin-server 的地址。配置代码如下:

server:
  port: 8765
  
spring:
  application:
    name: eureka-feign-client
  sleuth:
    web:
      client:
        enabled: true
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://localhost:9411/  #指定 Zipkin 服务器的地址
    
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

在程序的启动类 EurekaFeignClientApplication 上加 @EnableEurekaClient 和 @EnableFeignClients 注解,代码如下:

package com.sisyphus;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaFeignClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaFeignClientApplication.class, args);
    }
}

服务消费者通过 FeignClient 消费服务提供者提供的服务,FeignClient 的代码如下:

package com.sisyphus.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient("eureka-client")
@Component
public interface EurekaClientFeign {
    @GetMapping("/hi")
    String sayHiFromClientEureka(@RequestParam("name") String name);
}

在 Service 层,通过调用 EurekaClientFeign 来消费服务,代码如下:

package com.sisyphus.service;

import com.sisyphus.feign.EurekaClientFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class HiService {

    @Autowired
    EurekaClientFeign eurekaClientFeign;

    public String sayHi(String name){
        return eurekaClientFeign.sayHiFromClientEureka(name);
    }
}

在 Controller 层,对外暴露一个 API 接口,通过 HiService 来调用 eureka-client 的服务,代码如下:

package com.sisyphus.controller;

import com.sisyphus.service.HiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HiController {
    @Autowired
    HiService hiService;
    
    @GetMapping("/hi")
    public String sayHi(@RequestParam(defaultValue = "sisyphus", required = false) String name){
        return hiService.sayHi(name);
    }
}

4.项目演示

完整的项目搭建完毕,依次启动 eureka-server、eureka-client 和 eureka-feign-client,并启动 zipkin.jar。在浏览器上访问 http://localhost:8765/hi(如果报错,是因为服务与发现需要一定的时间,需要耐心等待几十秒),浏览器显示如下:

hi sisyphus,i am from port:8763

访问 http://localhost:9411,即访问 Zipkin 的展示界面,如图所示:

在这里插入图片描述

这个界面用于展示 Zipkin Server 收集的链路数据,可以根据服务名、开始时间、结束时间、请求消耗的时间等条件来查找:

在这里插入图片描述

从图中可知请求的调用情况,例如请求的调用时间、消耗时间,以及请求调用的链路情况等

单击导航栏的 “依赖” 按钮,可以查看服务的依赖关系。在本案例中,eureka-feign-client 消费了 eureka-client 提供的 API 服务,这两个服务的依赖关系如图所示:

在这里插入图片描述

四、在链路数据中添加自定义数据

现在需要实现这样一个功能:在链路数据中加上请求的操作人。本案例在 eureka-client 服务中实现。在 API 接口逻辑方法中,通过 Tracer 的 currentSpan 方法获取当前的链路数据的 Span,通过 tag 方法加上自定义的数据。在本案例中加上链路的操作人的代码如下:

package com.sisyphus.controller;

import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HiController {

    @Autowired
    Tracer tracer;

    @Value("${server.port}")
    String port;

    @GetMapping("/hi")
    public String home(@RequestParam String name){
        tracer.currentSpan().tag("name","sisyphus");
        return "hi " + name + ",i am from port:" + port;
    }
}

五、使用 RabbitMQ 传输链路数据

在上述案例中,最终 eureka-feign-client 收集的数据是通过 HTTP 上传给 zipkin-server 的。在 Spring Cloud Sleuth 中支持消息组件来传输链路数据,本节使用 RabbitMQ 来传输链路数据。使用 RabbitMQ 前需要安装 RaabitMQ 程序

链路数据是通过 RabbitMQ 来传输的,那么 zipkin-server 是如何直到 RabbitMQ 的地址,又如何监听收到的链路数据呢?zipkin-server 是通过读取环境变量来获取 RabbitMQ 的配置信息的,这需要在程序启动时通过环境变量的形式将配置信息注入环境中, 然后 zipkin-server 从环境变量中读取,可配置的属性如下表所示

属性环境变量描述
zipkin.collector.rabbitmq.addressRABBIT_ADDRESSES用逗号分隔的 RabbitMQ 地址列表,例如 loaclhost:5672,localhost:5673
zipkin.collector.rabbitmq.passwordRABBIT_PASSWORD连接到 RabbitMQ 时使用的密码,默认为 guest
zipkin.collector.rabbitmq.usernameRABBIT_USER连接到 RabbitMQ 时使用的用户名,默认为 guest
zipkin.collector.rabbitmq.virtual-hostRABBIT_VIRTUAL_HOST使用的 RabbitMQ virtual host,默认为 /
zipkin.collector.rabbitmq.use-sslRABBIT_USE_SSL设置为 “true”,则用 SSL 的方式与 RabbitMQ 建立链接
zipkin.collector.rabbitmq.concurrencyRABBIT_CONCURRENCY并发消费者数量,默认为 1
zipkin.collector.rabbitmq.connection-timeoutRABBIT_CONNECTION_TIMEOUT建立连接时的超时时间,默认为 60000 毫秒,即 1 分钟
zipkin.collector.rabbitmq.queueRABBIT_QUEUE从中获取 span 信息的队列,默认为 zipkin

比如用以下命令启动 Zipkin 的服务:

RABBIT_ADDRESSES=localhost java -jar zipkin.jar

上述命令等同于以下命令

java -jar zipkin.jar --zipkin.collector.rabbitmq.addressed=localhost

用上述两条命令中的任何一条重新启动 Zipkin 服务。接着改造 Zipkin Client(包括 gateway-service 工程和 user-service 工程),在它们的 pom 文件中加上 spring-cloud-stream-binder-rabbit 的依赖,代码如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>

同时在配置文件 application.yml 中加上 RabbitMQ 的配置,并去掉 spring.zipkin.base-url 的配置,代码如下;

spring:
  rabbitmq:
    host: loaclhost
    username: guest
    password: guest
    port: 5672

这样就可以将链路的上传数据方式从 HTTP 改为消息代理组件 RabbitMQ

六、在 MySQL 数据库中存储链路数据

在上面的例子中,Zipkin Server 将数据存储在内存中,一旦程序重启,之前的链路数据会全部丢失,那么怎么将链路数据存储起来呢?Zipkin 支持将链路数据存储在 MySQL、ElasticSearch 和 Cassandra 数据库中

首先需要初始化 Zipkin 存储在 MySQL 数据的脚本,具体可到 GitHub 中搜索 Zipkin 进行查看。本案例中的源码资源文件夹中也包含该数据库脚本

在数据库中初始化上述脚本后,需要让 Zipkin 连接上数据库。Zipkin 连接数据库同连接 RabbitMQ 一样,都是从环境变量中读取的。Zipkin 连接数据库的属性所对应的环境变量如下表所示:

属性环境变量描述
zipkin.torage.typeSTORAGE_TYPE默认为 mem,即内存,其它可支持的为 Cassandra、Cassandra3、ElasticSearch、MySQL
zipkin.torage.mysql.hostMYSQL_HOST数据库的 host,默认为 localhost
zipkin.torage.mysql.portMYSQL_TCP_PORT数据库的端口,默认为 3006
zipkin.torage.mysql.usernameMYSQL_USER连接数据库的用户名,默认为空
zipkin.torage.mysql.passwordMYSQL_PASS连接数据库的密码,默认为空
zipkin.torage.mysql.dbMYSQL_DBZipkin 使用的数据库名,默认为 zipkin
zipkin.torage.mysql.max-activeMYSQL_MAX_CONNECTIONS最大连接数,默认为 10

使用以下命令启动,Zipkin 就可以启动时连接数据库,并将链路数据存储在数据库中,命令如下:

STORAGE_TYPE=mysql MYSQL_HOST=localhost MYSQL_TCP_PORT=3306 MYSQL_USER=root MYSQL_PASS=123456 MYSQL_DB=zipkin java -jar zipkin.jar

上述命令等同于以下命令:

java -jar zipkin.jar --zipkin.torage.type=mysql --zipkin.torage.mysql.host=localhost --zipkin.torage.mysql.port=3306 --zipkin.torage.mysql.username=root -- zipkin.torage.mysql.password=123456

使用上面的命令启动 zipkin.jar 工程,然后在浏览器上访问 http://localhost:8765/hi,再访问 http://localhost:9411/zipkin/,就可以看到链路数据。这时去数据库查看数据,也可以看到存储再数据库的链路数据

这时重启应用 zipkin.jar,再次在浏览器上访问 httpL//localhost:9411/zipkin/,仍然可以得到之前的结果,证明链路数据存储在数据库中,而不是内存中

七、在 ElasticSearch 中存储链路数据

在高并发的情况下,使用 MySQL 存储链路数据显然不合理,这时可以选择使用 ElasticSearch 存储。读者需要自行安装 ElasticSearch 和 Kibana,下载地址为 http://www.elastic.co/products/elasticsearch。安装完成后启动,其中 ElasticSearch 的默认端口号为 9200,Kibana 的默认端口号为 5601

同理,Zipkin 连接 ElasticSearch 也是从环境变量中读取的连接属性,ElasticSearch 香瓜的呢环境变量和对应的属性如下表所示:

属性环境变量描述
zipkin.torage.elasticsearch.hostsES_HOSTS默认为空
zipkin.torage.elasticsearch.pipelineES_PIPELINE默认为空
zipkin.torage.elasticsearch.max-requestsES_MAX_REQUESTS默认为 64
zipkin.torage.elasticsearch.timeoutES_TIMEOUT默认为 10s
zipkin.torage.elasticsearch.indexES_INDEX默认为 zipkin
zipkin.torage.elasticsearch.date-separatorES_DATE_SEPARATOR默认为 zipkin
zipkin.torage.elasticsearch.index-shardsES_INDEX_SHARD默认为 5
zipkin.torage.elasticsearch.index-replicasES_INDEX_REPLICAS|ES_INDEX_REPLICAS默认为 1
zipkin.torage.elasticsearch.usernameES_USERNAME默认为空
zipkin.torage.elasticsearch.passwordES_PASSWORD默认为空

使用以下命令启动 Zipkin,Zipkin 就将链路数据存储在 ElasticSearch 中,启动命令如下:

STORAGE_TYPE=elasticsearch ES_HOSTS=http://localhost:9200 ES_INDEX=zipkin java -jar zipkin.jar

启动完成后,在浏览器上访问 httpL//localhost:8765/hi,再访问 http://localhost:9411/zipkin/,可以看到链路数据,这时链路数据存储在 ElasticSearch 中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

313YPHU3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值