微服务架构实战第九节 微服务可视化监控和链路追踪

29 监控原理:如何理解服务监控和 Spring Cloud Sleuth 的基本原理?

从本课时开始,我们将讨论一个在微服务架构中非常重要的话题,即服务监控。今天我们将简要分析服务监控的基本原理,这是理解服务监控相关工具和框架的基础。同时,作为 Spring Cloud 中用于实现服务监控的专用工具,Spring Cloud Sleuth 为实现这些基本原理提供了完整而强大的解决方案。

服务监控基本原理

在微服务架构中,我们基于业务划分服务并对外暴露服务访问接口。试想这样一个场景,如果我们发现某一个业务接口在访问过程中发生了错误,一般的处理过程就是快速定位到问题所发生的服务并进行解决。但在如下所示中大型系统中,一个业务接口背后可能会调用一批其他业务体系中的业务接口或基础设施类的底层接口,这时候我们如何能够做到快速定位问题呢?

微服务调用链路示意图

传统的做法是通过查阅服务器的日志来定位问题,但在中大型系统中,这种做法可操作性并不强,主要原因是我们很难找到包含错误日志的那台服务器。一方面,开发人员可能都不知道整个服务调用链路中具体有几个服务,也就无法找到是哪个服务发生了错误。就算找到了目标服务,在分布式集群的环境下,我们也不建议直接通过访问某台服务器来定位问题。服务监控的需求就应运而生。

分布式服务跟踪和监控的运行原理上实际上并不复杂,我们首先需要引入两个基本概念,即 SpanId 和 TraceId。

SpanId

SpanId 一般被称为跨度 Id。在上图中,针对服务 A 的访问请求,通过 SpanId 来标识该请求的到达并返回的具体过程。显然,对于这个 Span 而言,势必需要明确 Span 的开始时间和结束时间,这两个时间之间的差值就是服务 A 对这个请求的处理时间。

TraceId

除了 SpanId 外,我们还需要 TraceId,也就是跟踪 Id。同样是在上图中,要想监控整个链路,我们不光需要关注服务 A 中的 Span,而是需要把请求通过所有服务的 Span 都串联起来。这时候就需要为这个请求生成一个全局的唯一性 Id,通过这个 Id 可以串联起如上图所示,从服务 A 到服务 F 的整个调用,这个唯一性 Id 就是 TraceId。

同时,我们也应该关注于各个 Span 之间的顺序关系。显然,在上图中,服务 A 位于服务 B 的上游,所以访问服务 A 所生成的 SpanId 应该是访问服务 B 所生成的 SpanId 的父 SpanId,服务 B 和服务 C 的调用关系以此类推。这样,我们通过获取请求的唯一性 TraceId,并通过各个父 SpanId 与子 SpanId 之间的关联关系就可以构建出一条完整的服务调用链路。

关于 Span,业界一般使用四种关键事件记录每个服务的客户端请求和服务器响应过程。我们可以基于这四种关键事件来剖析一个 Span 中的时间表示方式,如下所示:

Span 中的四种关键事件示意图

在上图中,cs 表示 Client Send,也就是客户端向服务 A 发起了一个请求,代表了一个 Span 的开始。sr 代表 Server Receive,表示服务端接收客户端的请求并开始处理它。一旦请求到达了服务器端,服务器端对请求进行处理,并返回结果给客户端,这时候就会 ss 事件,也就是 Server Send。最后的 cr 表示 Client Receive,表示客户端接收到了服务器端返回的结果,代表着一个 Span 的完成。

我们可以通过计算这四个关键时间之前的差值来获取 Span 中的时间信息。显然,sr-cs 值等于请求的网络延迟,ss-sr 值表示服务端处理请求的时间,而 cr-sr 值则代表客户端接收服务端数据的时间。

通过这些关键事件我们就可以发现服务调用链路中存在的问题,目前主流的服务监控实现工具都对这些关键事件做了支持和封装。

通过前面所介绍的服务监控基本原理,我们明确了分布式环境下服务跟踪的载体,即 TraceId 和 SpanId。但要实现服务跟踪,我们还需要围绕这两个载体做进一步的分析和挖掘。首先,我们需要对整个调用过程的所有服务进行埋点并生成事件,并对这些事件数据进行采集。同时,由于事件数据量一般都很大,不仅要能实现存储,还需要能提供快速查询。然后,在此基础上,我们还需要对采集到的事件数据进行各种指标运算,并将运算结果保存起来,并提供各种排序、阈值设置和警告等功能。

这些工作不是简单一个工具和框架能全部完成的,我们也不想自己从无到有实现这样一整套解决方案。幸好在 Spring Cloud 中存在一个组件能够帮忙我们简化实现,这个组件就是 Spring Cloud Sleuth。在 Spring Cloud Sleuth 中,也把这些事件称为注解(Annotation),请你注意名称上的不同叫法。

使用 Spring Cloud Sleuth 实现服务监控

Spring Cloud Sleuth 是 Spring Cloud 的组成部分之一,对于分布式环境下的服务调用链路,我们可以通过该框架来完成服务监控和跟踪方面的各种需求。

当我们将 Spring Cloud Sleuth 添加到系统的类路径,该框架便会自动建立日志收集渠道,不仅包括常见的使用 RestTemplate 发出的请求,同时也能无缝支持通过 API 网关 Zuul 以及 Spring Cloud Stream 所发送的请求。本课程前面的课时已经对这些组件做了详细的介绍。

针对监控数据的管理,Spring Cloud Sleuth 可以设置常见的日志格式来输出 TraceId 和 SpanId。我们也可以利用诸如 Logstash 等日志发布组件将日志发布到 ELK 等日志分析工具中进行处理。同时,Spring Cloud Sleuth 也兼容了 Zipkin、HTrace 等第三方工具的应用和集成。在下一课时中,我们就将集成 Spring Cloud Sleuth 与 Zipkin 来提供可视化的链路跟踪系统。

接下来,就让我们引入 Spring Cloud Sleuth 框架。借助于 Spring Cloud Sleuth 中即插即用的服务调用链路构建过程,我们想要在某个微服务中添加服务监控功能,要做的事情只有一件,即把 spring-cloud-starter-sleuth 组件添加到 Maven 依赖中即可,如下所示。

<dependency>
     <groupId>org.springframework.cloudgroupId>
     <artifactId>spring-cloud-starter-sleuthartifactId>
dependency>

初始化工作完成之后,接下来就来看一下引入 Spring Cloud Sleuth 之后为我们带来的变化,首当其冲的切入点是控制台日志分析。在 SpringHealth 案例系统中,我们通过发起一次请求操作来观察 Spring Cloud Sleuth 的运行时效果。

我们知道 intervention-service 分别调用了 user-service 和 device-service。如果我们调用http://localhost:5555/springhealth/intervention/interventions/springhealth_admin/device1 端点。那么,在 user-service 中,在控制台上可以看到如下日志信息:

INFO [userservice,81d66b6e43e71faa,6df220755223fb6e,true] 18100 --- [nio-8082-exec-8] c.s.user.controller.UserController       : Get user by userName from 8082 port of userservice instance

我们关注于上述日志信息中的斜体部分内容,包括了四段内容,即服务名称、TraceId、SpanId 和 Zipkin 标志位,它是格式如下所示:

[服务名称, TraceId, SpanId, Zipkin 标志位]

显然,第一段中的 userservice 代表着该服务的名称,使用的就是在 bootstrap.yml 中 spring.application.name 指定的服务名称。考虑到服务跟踪的需求,为服务指定一个统一而友好的名称是一项最佳实践。

第二段中的 TraceId 代表一次完整请求的唯一编号,上例中的 81d66b6e43e71faa 就是该次请求的唯一编号。在诸如 Zipkin 等可视化工具中,可以通过 TraceId 查看完整的服务调用链路。

在一个完整的服务调用链路中,每一个服务之间的调用过程都可以通过 SpanId 进行唯一标识,例如上例中位于第三段的 6df220755223fb6e。所以 TraceId 和 SpanId 是一对多的关系,即一个 TraceId 一般都会包含多个 SpanId,每一个 SpanId 都从属于特定的 TraceId。当然,也可以通过 SpanId 查看某一个服务调用过程的详细信息。

最后的第四段代表 Zipkin 标志位,该标志位用于识别是否将服务跟踪信息同步到 Zipkin, Zipkin 是一个可视化工具,可以将服务跟踪信息通过一定的图形化形式展示出来。因为在请求运行过程中,我们已经启动了 Zipkin 服务器,所以上例中的标志位值为 true,意味着所有跟踪信息将被同步到 Zipkin 中。关于 Zipkin 的详细介绍请参考下一课时的内容。

如果你执行过前面课时中的代码,你会发现以上四段内容的日志显示效果为 [userservice,,,],也就说默认请求下 TraceId、SpanId 和 Zipkin 标志位都为空,这些内容都是在引入 Spring Cloud Sleuth 之后被自动添加到了每一次服务调用中。

同样,我们再来看一下 device-service 和 intervention-service 中的日志信息。device-service 中的控制台输出日志如下,同样可以看到用斜体表示的完整四段内容,其中 TraceId 为81d66b6e43e71faa,SpanId 为 e1dffdb86c81cc3c,Zipkin 标志位为 true:

INFO [deviceservice,81d66b6e43e71faa,e1dffdb86c81cc3c,true] 18656 --- [nio-8081-exec-2] c.s.device.controller.DeviceController   : Get device by code: device1 from port: 8081

同样,在 intervention-service 中的日志信息中,我们也发现了类似的一条记录,其中 TraceId 为 81d66b6e43e71faa,SpanId 为 992aec60c399ece2,Zipkin 标志位也为 true:

INFO [interventionservice,81d66b6e43e71faa,992aec60c399ece2,true] 28648 --- [nio-8081-exec-2] c.s.intervention.controller.InterventionController   : Generate intervention for userName: springhealth_admin and deviceCode: device1.

请注意,以上三段日志中的 TraceId 都是 81d66b6e43e71faa,也就是它们属于同一个服务调用链路,而不同的 SpanId 代表着整个链路中的具体某一个服务调用。我们从日志中的时间上也不难看出三者之间的调用时序。基于这三个服务以及 TraceId、SpanId 所生成的服务调用时序链路效果如下所示:

三个服务调用链路效果图

关于该链路的可视化效果和更详细的数据信息我们在下一课时中还会有具体展开。

小结与预告

构建服务监控和链路跟踪在微服务系统开发过程中是一项基础设施类工作,而我们可以借助于 Spring Cloud Sleuth 来轻松完成这项工作。Spring Cloud Sleuth 内置了日志采集和分析机制,能够帮忙我们自动化建立 TraceId 和 SpanId 之间的关联关系。本课时对如何在业务开发过程中引入 Spring Cloud Sleuth 做了全面介绍。

这里给你留一道思考题:你能描述服务监控过程中的 TraceId、SpanId、四大关键事件的概念和作用吗?

Spring Cloud Sleuth 是一个集成化的框架,可以与其他第三方组件进行无缝集成从而提供更加强大的链路跟踪功能。在下一课时中,我们就将通过集成 Zipkin 来实现可视化的链路跟踪效果。


30 监控可视:如何整合 Spring Cloud Sleuth 与 Zipkin 实现可视化监控?

上一课时,我们引入了 Spring Cloud Sleuth 框架来为微服务系统访问链路自动添加 TraceId 和 SpanId。而 Spring Cloud Sleuth 的强大之处实际上并不是体现在独立的服务跟踪和日志处理能力上,而是体现在框架整合能力上。业界关于日志聚合的主流实现方案包括 ELK 等,Spring Cloud Sleuth 能够整合这些日志聚合方案。但从功能层面讲,ELK 的作用主要体现在检索功能上,而不是对请求链路中各阶段时间延迟的关注。而像 Zipkin 这样的框架正是用来处理这种问题,Spring Cloud Sleuth 在框架整合上可以很方便地引入 Zipkin 等工具实现这一难题。本课时就将介绍两者之间的集成方案,以及如何使用 Zipkin 实现可视化的服务调用链路。

集成 Spring Cloud Sleuth 与 Zipkin

在完成 Spring Cloud Sleuth 与 Zipkin 的整合之前,我们有必要先对 Zipkin 的基本结构做一些介绍。

Zipkin 简介

Zipkin 是一个开源的分布式跟踪系统,每个服务向 Zipkin 报告运行时数据,Zipkin 会根据调用关系通过 Zipkin UI 对整个调用链路中的数据实现可视化。在结构上 Zipkin 包含几个核心的组件,如下图所示:

Zipkin 基本结构图(来自 Zipkin 官网)

在上图中,首先我们看到的是日志的收集组件 Collector,接收来自外部传输(Transport)的数据,将这些数据转换为 Zikpin 内部处理的 Span 格式,相当于兼顾数据收集和格式化的功能。这些收集的数据通过存储组件 Storage 进行存储,当前支持 Cassandra、Redis、HBase、MySQL、PostgreSQL、SQLite 等工具,默认存储在内存中。然后,所存储数据可以通过 RESTful API 对外暴露查询接口。更为有用的是,Zipkin 还提供了一套简单的 Web 界面,基于 API 组件的上层应用,可以方便而直观的查询和分析跟踪信息。

在运行过程中,可以通过 Zipkin 获取类似如下图所示的服务调用链路分析效果:

Zipkin 服务调用链路分析示例图(来自 Zipkin 官网)

我们看到 Zipkin 为我们提供了强大的可视化管理功能,关于图中的各个细节将在本课时后续中得到全面展开。

在 Spring Cloud Sleuth 中整合 Zipkin 也非常简单,只需要启动 Zipkin 服务器并为各个微服务配置集成 Zipkin 服务即可完成准备工作。

Zipkin 服务器本身不需要开发人员构建,我们直接从 Zipkin 的官网上进行下载即可。下载下来的是一个可执行的 jar 包,如 zipkin-server-2.21.7-exec.jar。我们通过 java 命令直接启动这个 jar 包即可,如下所示:

java –jar zipkin-server-2.21.7-exec.jar

集成 Zipkin 服务器

为了集成 Zipkin 服务器,在各个微服务中,需要确保添加了对 Spring Cloud Sleuth 和 Zipkin 的 Maven 依赖,如下所示。

<dependency>
     <groupId>org.springframework.cloudgroupId>
     <artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
 
<dependency>
     <groupId>org.springframework.cloudgroupId>
     <artifactId>spring-cloud-sleuth-zipkinartifactId>
dependency>

然后,在配置文件中添加对 Zipkin 服务器的引用即可,配置内容如下所示。

spring:
	zipkin:
	    baseUrl: http://localhost:9411

至此,Zipkin 环境已经搭建完毕,我们可以通过访问 http://localhost:9411 来获取 Zipkin 所提供的所有可视化结果,接下来将演示如何使用 Zipkin 跟踪服务调用链路。

使用 Zipkin 可视化服务调用链路

在本课时中,Zipkin 可视化服务调用链路的构建包含三大维度,如下图所示:

构建 Zipkin 可视化服务调用链路的三大维度

接下来,我们将分别这三个维度介绍 Zipkin 的强大功能。

可视化服务依赖关系

依赖在某种程度上不可避免,但是过多地依赖势必会增加系统复杂性和降低代码维护性,从而成为团队开发的一种阻碍。在微服务系统中一般存在多个服务,服务需要管理相互之间的依赖关系。当系统规模越来越大后,各个业务服务之间的直接依赖和间接依赖关系就会变得十分复杂。我们需要通过一个简洁明了的可视化工具来查看当前服务链路中的依赖关系,Zipkin 就提供了这方面的支持。

在 SpringHealth 案例系统中,当我们通过访问 intervention-service 中的 HTTP 端点http://localhost:5555/springhealth/intervention/interventions/springhealth_admin/device1时,下图展示了通过 Zipkin 获取的服务调用依赖关系:

Zipkin 中的 SpringHealth 案务依赖关系示意图

可以看到在这个服务调用链路中,我们首先通过 zuulservice 访问 interventionservice,然后 interventionservice 又通过 zuulservice 分别访问 userservice 和 deviceservice,从而形成一次完整的业务调用。

那么这张图中的依赖关系是否正确呢?让我们回顾 intervention-service 中生成健康干预信息的代码结构:

public Intervention generateIntervention(String userName, String deviceCode) {
        Intervention intervention = new Intervention();
 
        //通过 UserServiceClient 获取远程 User 信息
        UserMapper user = getUser(userName); 
        …

        //通过 DeviceServiceClient 获取远程 Device 信息
        DeviceMapper device = getDevice(deviceCode);
        …
 
        return intervention;
}


可以看到在 intervention-service 的 generateIntervention 方法中会通过 UserServiceClient 和 DeviceServiceClient 分别访问 user-service 和 device-service。而在这些远程访问中,都将通过 zuul-service 进行路由转发,所以这与上图中的服务依赖关系完全一致。

可视化服务调用时序

可视化服务调用时序是 Zipkin 最重要的功能,对于服务监控而言,服务调用链数据收集、分析和管理的目的是发现服务调用过程的问题并采取相应的优化措施。下图展示了 Zipkin 可视化服务调用时序的主界面:

Zipkin 可视化服务调用时序的主界面

上图该主界面主体是一个面向查询的操作界面,其中我们需要关注服务名称和端点,因为服务调用链路中的所有服务都会出现在服务名称列表中,同时,针对每个服务,我们也可以选择自身感兴趣的端点信息。同时,我们也发现了多个用于灵活查询的过滤器,包括 TraceId、SpanName、时间访问、调用时长以及标签功能。

当然,我们最应该关注的是查询结果。针对某个服务,Zipkin 的查询结果展示了包含该服务的所有调用链路。现在,让我们关注于 user-service 中根据用户名获取用户信息的http://localhost:5555/springhealth/user/users/username/{userName} 端点,Zipkin 上的执行效果图如下所示:

Zipkin 服务调用链路明细图界面

当发起这个 HTTP 请求时,该请求会先到达 Zuul 网关,然后再通过路由转发到 userservice。通过观察上图服务之间的调用时序,我们在前面介绍的服务依赖关系的基础上给出了更为明确的服务调用关系。

上图中最重要的就是各个 Span 信息。一个服务调用链路被分解成若干个 Span,每个 Span 代表完整调用链路中的一个可以衡量的部分。我们通过可视化的界面,可以看到整个访问链路的整体时长以及各个 Span 所花费的时间。每个 Span 的时延都已经被量化,并通过背景颜色的深浅来表示时延的大小。注意到这里 userservice 出现了两个 Span,原因在于 userservice 在该请求中还访问了 OAuth2 的授权服务器。

可视化服务调用数据

在上图中,我们点击任何一个感兴趣的 Span 就可以获取该 Span 对应的各项服务调用数据明细。例如,我们点击“get /users/username/{username}”这个 Span,Zipkin 会跳转到一个新的页面并显示如下图所示的数据:

Zipkin 中 Span 对应的名称、TraceId 和 SpanId

这里看到了本次调用中用于监控的最重要的元数据 TraceId 和 SpanId,以及代表各个关键事件的 Annotation 可视化长条。点击长条下的“SHOW ALL ANNOTATIONS”按钮,可以得到如下所示的事件明细信息:

Zipkin 中 Span 对应的四个关键事件数据界面

上图展示了针对该 Span 的 cs、sr、ss 和 cr 这四个关键事件数据。对于这个 Span 而言,zuulservice 相当于是 userservice 的客户端,所以 zuulservice 触发了 cs 事件,然后通过 (17.160 – 2.102)ms 到达了 userservice,以此类推。从这些关键事件数据中可以得出一个结论,即该请求的整个服务响应时间主要取决于 userservice 自身的处理时间。

当然,我们针对这个 Span,也可以获取如所示的标签明细数据:

Zipkin 中 Span 对应的标签数据

可以看到这些数据包括 HTTP 请求相关的方法、路径等各项基础数据,也包括使用 Spring 构建 RESTful 风格调用时的 Controller 类以及端点信息。

最后,作为数据管理和展示的统一平台,Zipkin 还实现了更为低层的数据表现形式,也就是通过 JSON 数据提供对调用过程的详细描述。如下所示的就是一份示例 JSON 数据,描述的也是关键事件所应该具备的各项信息:

[
     {
         "traceId":"81d66b6e43e71faa",
         "parentId":"6df220755223fb6e",
         "id":"abfed1d715052b08",
         "kind":"CLIENT",
         "name":"get",
         "timestamp":1602337321490104,
         "duration":8594,
         "localEndpoint":{
             "serviceName":"userservice",
             "ipv4":"192.168.247.1"
         },
         "tags":{
             "http.method":"GET",
             "http.path":"/userinfo"
         }
     },
     {
         "traceId":"81d66b6e43e71faa",
         "parentId":"81d66b6e43e71faa",
         "id":"6df220755223fb6e",
         "kind":"SERVER",
         "name":"get /users/username/{username}",
         "timestamp":1602337321489229,
         "duration":135785,
         "localEndpoint":{
             "serviceName":"userservice",
             "ipv4":"192.168.247.1"
         },
         "remoteEndpoint":{
             "ipv6":"::1"
         },
         "tags":{
             "http.method":"GET",
             "http.path":"/users/username/springhealth_user1",
             "mvc.controller.class":"UserController",
             "mvc.controller.method":"getUserByUserName"
         },
         "shared":true
     },
	省略其他Span信息
 ]

在介绍 Zipkin 的基本结构时,我们提到 Zikpin 还提供了专门的 RESTful API 获取一次服务调用链路中所有 Span 对应的 JSON 数据,例如本例中的 SpanName 为“get /users/username/{username}”,所以对应的 API 即为 localhost:9411/zipkin/?serviceName=userservice&spanName=get+%2Fusers%2Fusername%2F{username} ,我们可以根据需要获取所需的各项信息。你可以自己多进行尝试。

小结与预告

承接上一课时内容,今天的内容重点关注如何实现监控数据的可视化管理。我们引入了 Zipkin 这款优秀的开源框架,并完成与 Spring Cloud Sleuth 的无缝集成。基于 Zipkin,我们可以完成可视化服务依赖关系、可视化服务调用时序和可视化服务调用数据等三大维度的可视化监控功能,本课时对如何实现这些功能进行了详细的展开。

这里给你留一道思考题:Zipkin 中所提供的可视化功能主要包括哪几个维度?

到现在为止,尽管我们已经可以利用 Spring Cloud Sleuth 实现了内置的监控功能,但有时候我们可能需要对监控工作做一些定制化的修改和扩展,这时候就需要创建各种自定义的 Span。下一课时我们将来讨论这一话题。


31 监控扩展:如何使用 Tracer 在访问链路中创建自定义的 Span?

在了解了 Spring Cloud Sleuth 的基本工作原理以及与 Zipkin 之间的集成方案之后,我们不禁要想,虽然内置的日志埋点和采集功能已经能够满足日常开发的大多数场景需要,但如果我想在业务系统中重点监控某些业务操作时,是不是有办法来创建自定义的 Span 并纳入可视化监控机制中呢?答案是肯定的,今天的内容我们就围绕如何使用 Spring Cloud Sleuth 底层的 Brave 框架在服务访问链路中添加自定义Span这一话题展开讨论。

使用 Brave 创建自定义 Span

从 2.X 版本开始,Spring Cloud Sleuth 全面使用 Brave 作为其底层的服务跟踪实现框架。原本在 1.X 版本中通过 Spring Cloud Sleuth 自带的 org.springframework.cloud.sleuth.Tracer 接口创建和管理自定义 Span 的方法将不再有效。因此,想要在访问链路中创建自定义的 Span,需要对 Brave 框架所提供的功能有足够的了解。

事实上,Brave 是 Java 版的 Zipkin 客户端,它将收集的跟踪信息,以 Span 的形式上报给 Zipkin 系统。我们首先来关注 Brave 中的 Span 类,该类的方法列表如下所示:

Span 类的方法列表

注意到 Span 是一个抽象类,在上面的方法列表中,我们也看到该类的几乎所有方法都是抽象方法,需要子类进行实现。在 Brave 中,该抽象类的子类就是 RealSpan。RealSpan 中的 start 方法如下所示:

@Override 
public Span start(long timestamp) {
    synchronized (state) {
      state.startTimestamp(timestamp);
    }
    return this;
}

这里的 state 是一个可变的 MutableSpan,而上述 start 方法就是为这个 MutableSpan 设置了开始时间。可以想象,对应的 finish 方法也会为 MutableSpan 设置结束时间,如下所示:

@Override 
public void finish(long timestamp) {
    if (!pendingSpans.remove(context)) return;
    synchronized (state) {
      state.finishTimestamp(timestamp);
    }
    finishedSpanHandler.handle(context, state);
}

对于关闭 Span 的操作而言,上述方法还添加了一个 Handler 以便执行回调逻辑,这也是非常常见的一种实现技巧。我们接着来看另一个非常有用的 annotate 方法,如下所示:

@Override 
public Span annotate(long timestamp, String value) {
    if ("cs".equals(value)) {
      synchronized (state) {
        state.kind(Span.Kind.CLIENT);
        state.startTimestamp(timestamp);
      }
    } else if ("sr".equals(value)) {
      synchronized (state) {
        state.kind(Span.Kind.SERVER);
        state.startTimestamp(timestamp);
      }
    } else if ("cr".equals(value)) {
      synchronized (state) {
        state.kind(Span.Kind.CLIENT);
      }
      finish(timestamp);
    } else if ("ss".equals(value)) {
      synchronized (state) {
        state.kind(Span.Kind.SERVER);
      }
      finish(timestamp);
    } else {
      synchronized (state) {
        state.annotate(timestamp, value);
      }
    }
    return this;
}

回顾《监控原理:如何理解服务监控和 Spring Cloud Sleuth 的基本原理?》中的介绍的四种监控事件,我们不难理解上述代码的作用就是为这些事件指定类型以及时间,从而为构建监控链路提供基础。

RealSpan 中最后一个值得介绍的方法是如下所示的 tag 方法:

@Override 
public Span tag(String key, String value) {
    synchronized (state) {
      state.tag(key, value);
    }
    return this;
}

该方法为 Span 打上一个标签,其中两个参数分别代表标签的 Key 和 Value,开发人员可以根据需要对任何一个 Span 添加自定义的标签体系。

了解了 Span 的定义之后,我们就来讨论在业务代码中创建 Span 的两种方法。一种是使用 Brave 中的 Tracer 类,一种是使用注解。

通过 Tracer 类创建 Span

Tracer 是一个工具类,提供了一批方法用于完成与 Span 相关的各种属性和操作。我们同样挑选几个常见的方法进行展开。

首先,我们来看如何通过 Tracer 创建一个新的根 Span,可以通过如下所示的 newTrace 方法进行实现:

public Span newTrace() {
    return _toSpan(newRootContext());
}

这里用到了一个保存跟踪信息的 TraceContext 上下文对象,对于根 Span 而言,这个 TraceContext 就是全新的上下文,没有父 Span。而这里的 _toSpan 方法则最终构建了一个前面提到的 RealSpan 对象。

Span _toSpan(TraceContext decorated) {
	if (isNoop(decorated)) return new NoopSpan(decorated);
    PendingSpan pendingSpan = pendingSpans.getOrCreate(decorated, false);
    return new RealSpan(decorated, pendingSpans, pendingSpan.state(), pendingSpan.clock(), finishedSpanHandler);
}

这里多了一个新建的对象叫 PendingSpan ,用于收集一条 Trace 上暂时被挂起的未完成的 Span。

一旦创建了根 Span,我们就可以在这个 Span 上执行 nextSpan 方法来添加新的 Span,如下所示:

public Span nextSpan() {
    TraceContext parent = currentTraceContext.get();
    return parent != null ? newChild(parent) : newTrace();
}

这里获取当前 TraceContext,如果该上下文不存在,就通过 newTrace 方法来创建一个新的根 Span;如果存在,则基于这个上下文并调用 newChild 方法来创建一个子 Span。newChild 方法也比较简单,如下所示:

public Span newChild(TraceContext parent) {
    if (parent == null) throw new NullPointerException("parent == null");
    return _toSpan(nextContext(parent));
}

当然,在很多场景下,我们首先需要获取当前的 Span,这时候就可以使用 Tracer 类所提供的 currentSpan 方法,如下所示:

public Span currentSpan() {
    TraceContext currentContext = currentTraceContext.get();
    return currentContext != null ? toSpan(currentContext) : null;
}

基于 Tracer 提供的这些常见方法,我们可以梳理在业务代码中添加一个自定义的 Span 模版方法,如下所示:

@Service
public class MyService {

    @Autowired
    private Tracer tracer;

    public void perform() {
        Span newSpan = tracer.nextSpan().name(“spanName”).start();
        //ScopedSpan newSpan = tracer.startScopedSpan(“spanName”);
        try {
            //执行业务逻辑
        }
        finally{
          newSpan.tag(“key”, “value”);
          newSpan.annotate(“myannotation”);
          newSpan.finish();
        }
    }
}

在上述代码中,我们注入了一个 Tracer 对象,然后通过 nextSpan().name("findByDeviceCode").start() 方法创建并启动了一个“spanName”新的 Span。这是在业务代码中嵌入自定义 Span 的一种方法。另一种方法是使用注释行代码中的 ScopedSpan,ScopedSpan 代表包含一定操作延迟的 Span 对象,可以在操作不脱离当前进程时可以使用。当我们执行完各种业务逻辑之后,可以分别通过 tag 方法和 annotate 添加标签和定义事件,最后通过 finish 方法关闭 Span。这段模版代码可以直接引入到日常的开发过程中。

使用注解创建 Span

在 Brave 中,除了使用代码对创建 Span 的过程进行控制之外,我们还可以使用另一种更为简单的方法来创建 Span,这种方法就是使用注解。

我们先来看 @NewSpan 注解,这个注解可以自动创建一个新的 Span,使用方法如下所示:

@NewSpan
void myMethod();

当然,我们也可以把 @NewSpan 注解和 @SpanTag 注解结合在一起使用,@SpanTag 注解用于自动为通过 @NewSpan 注解所创建的 Span 添加标签,如下所示:

@NewSpan(name = "myspan")
void myMethod(@SpanTag("mykey") String param);

上述代码示例中,我们定义了一个名为“myspan”的新 Span,并在 myMethod 方法中注入了一个标签并定了标签的键,而该标签的值就是方法的输入参数 param。如果我们执行这个 myMethod(@SpanTag("mykey") String param) 方法,那么将生成一个键为“mykey”,值为 param 的新标签。

现在,我们已经掌握了创建自定义 Span 的常见方法,让我们把这些方法都串联起来实现日常开发中常见的自定义 Span 的应用场景,并集成 Zipkin 来实现自定义的可视化跟踪效果。

使用 Zipkin 集成自定义跟踪

在上一课时的介绍中,我们都是基于几个微服务之间的调用关系来讨论 Zipkin 在服务监控可视化过程中发挥的作用,其中完整服务调用链路中的各个 Span 都是采用默认的服务调用结果。在大多数情况下,我们通过这些 Span 就可以分析和排查服务调用链路中可能存在的问题。但在某些特定场景下,我们希望在这些 Span 的基础上能够实现一些定制化的数据收集和展示方式。

我们来考虑如下场景,假设在服务调用链路中,某一个方法调用时间比较长,但通过默认所创建的基于该方法的 Span,通常无法判断响应时间过长的原因。那么就可能出现一个需求,即通过添加一系列的自定义 Span 的方式进一步对长时间的服务调用进行拆分,把该请求中所涉及的多种操作分别创建 Span,然后找到最影响性能的 Span 并进行优化,这也是服务监控系统实现过程中的一项最佳实践,如下图所示:

通过自定义 Span 找到性能瓶颈点示意图

使用 Tracer 添加自定义 Span

让我们回到 SpringHealth 案例系统,来到 device-service。我们知道可以通过 Brave 的 Tracer 工具类创建 Span 并把该 Span 相关信息推送给 Zipkin。现在,我们希望在 DeviceService 的调用过程中添加一个新的 Span 以帮助 device-service 诊断响应时间过长问题,示例代码如下所示:

@Service
public class DeviceService {
 
    @Autowired
    private DeviceRepository deviceRepository;

    @Autowired
     private Tracer tracer;

    public Device getDeviceByCode(String deviceCode) {

        Span newSpan = tracer.nextSpan().name(“findByDeviceCode”).start();
 
        try {
            return deviceRepository.findByDeviceCode(deviceCode);
        }
        finally{
          newSpan.tag(“device”, “dababase”);
          newSpan.annotate(“deviceInfoObtained”);
          newSpan.finish();
        }
    }
}

在上述示例中,我们看到通过以下几个简单的方法调用就可以实现一个自定义 Span。这里基于前面介绍的自定义 Span 模版方法完成了 Span 的创建过程。我们使用 newTrace 方法创建一个自定义的 Span,并为该 Span 命名为“findByDeviceCode”。然后创建了一个键为“device”标签,并把标签值设置为"dababase "指明该标签与数据库操作相关。然后,我们又通过 annotate 方法记录了一个代表设备信息已经被获取的“deviceInfoObtained”事件。最后,我们执行了 finish 方法,在具体操作结束之后必须调用此方法,否则 Span 数据不会被发送到 Zipkin 中。

可视化自定义 Span

我们先来回顾在不添加上述自定义 Span 之前调用 http://localhost:5555/springhealth/device/devices/device1 时 Zipkin 上所生成的效果图,如下所示:

Zipkin 中系统自动生成 Span 效果界面

显然,这三个 Span 都是系统自定生成的。现在我们重新启动 device-service,然后再次访问该端口,就会得到如下图所示的可视化效果:

Zipkin中添加自定义Span效果界面

请注意在上图中,我们看到在原有默认可视化效果的基础上又多了一个名为“findByDeviceCode”的自定义 Span。点击该 Span,我们也将得到这个 Span 对应的各项事件明细数据,如下图所示:

Zipkin 中自定义 Span 中每个关键事件明细数据界面

这里看到了“deviceObtained”这个自定义事件。同时,基于数据,我们也不难发现在 device-service 处理请求的时间中实际上大部分是消耗在访问数据库以获取设备数据的过程中。同样,我们也可以在其他服务中添加不同的 Span 以实现对服务调用过程更加精细化的管理。

小结与预告

自定义 Span 是我们在日常开发过程中进程使用的一项工程实践,通过在业务系统中嵌入各种 Span 能够帮助开发人员找到系统中的性能瓶颈点从而为系统重构和优化提供抓手。在 Spring Cloud Sleuth 中,Brave 框架可以用来创建自定义的 Span,而上一课时中介绍的 Zipkin 框架则也可以对这些自定义 Span 实现可视化。本课时对这些具体的开发工作做了详细的介绍并结合 SpringHealth 案例给出了示例代码。

这里给你留一道思考题:通过 Brave 框架,开发人员创建自定义 Span 有哪些具体的实现方法?

在介绍完服务监控之后,接下来是整个课程的最后一个主题,即微服务测试。我们将先从微服务系统中与测试相关的需求和解决方案讲起并引出 Spring 家族中的 Spring Cloud Contract 框架。


你可能感兴趣的:(java,教程,微服务,架构,spring,cloud,sleuth,zipkin)