面试题:微服务链路追踪
微服务系统的监控主要包含以下三个方面:
Logging 就是记录系统行为的离散事件,例如,服务在处理某个请求时打印的错误日志,我们可以将这些日志信息记录到 ElasticSearch 或是其他存储中,然后通过 Kibana 或是其他工具来分析这些日志了解服务的行为和状态。大多数情况下,日志记录的数据很分散,并且相互独立,比如错误日志、请求处理过程中关键步骤的日志等等。
Metrics 是系统在一段时间内某一方面的某个度量,例如,电商系统在一分钟内的请求次数。我们常见的监控系统中记录的数据都属于这个范畴,例如 Promethus、Open-Falcon 等,这些监控系统最终给运维人员展示的是一张张二维的折线图。Metrics 是可以聚合的,例如,为电商系统中每个 HTTP 接口添加一个计数器,计算每个接口的 QPS,之后我们就可以通过简单的加和计算得到系统的总负载情况。
Tracing 即我们常说的分布式链路追踪。在微服务架构系统中一个请求会经过很多服务处理,调用链路会非常长,要确定中间哪个服务出现异常是非常麻烦的一件事。通过分布式链路追踪,运维人员就可以构建一个请求的视图,这个视图上展示了一个请求从进入系统开始到返回响应的整个流程。这样,就可以从中了解到所有服务的异常情况、网络调用,以及系统的性能瓶颈等
常见 APM 系统
APM 系统(Application Performance Management,即应用性能管理)是对企业的应用系统进行实时监控,实现对应用性能管理和故障定位的系统化解决方案。APM 作为系统运维管理和网络管理的一个重要方向,能够对关键服务进行监控、追踪以及告警,帮助开发和运维人员轻松地在复杂的应用系统中找到故障点,提高服务的稳定性,保证用户得到良好的服务,降低 IT 运维的成本。 当下成熟的互联网公司都已经建立了全方位监控系统,力求及时发现故障,并为优化系统提供性能数据支持。
国内比较常见的 APM 如下:
-
CAT: 由国内美团点评开源的,基于 Java 语言开发,目前提供 Java、C/C++、Node.js、Python、Go 等语言的客户端,监控数据会全量统计。国内很多公司在用,例如美团点评、携程、拼多多等。CAT 需要开发人员手动在应用程序中埋点,对代码侵入性比较强。
-
Zipkin: 由 Twitter 公司开发并开源,Java 语言实现。侵入性相对于 CAT 要低一点,需要对web.xml 等相关配置文件进行修改,但依然对系统有一定的侵入性。Zipkin 可以轻松与 Spring Cloud 进行集成,也是 Spring Cloud 推荐的 APM 系统。
-
Pinpoint: 韩国团队开源的 APM 产品,运用了字节码增强技术,只需要在启动时添加启动参数即可实现 APM 功能,对代码无侵入。目前支持 Java 和 PHP 语言,底层采用 HBase 来存储数据,探针收集的数据粒度非常细,但性能损耗较大,因其出现的时间较长,完成度也很高,文档也较为丰富,应用的公司较多。
-
SkyWalking: 国人开源的产品,2019 年 4 月 17 日 SkyWalking 从 Apache 基金会的孵化器毕业成为顶级项目。目前 SkyWalking 支持 Java、.Net、Node.js 等探针,数据存储支持MySQL、ElasticSearch等。 SkyWalking 与 Pinpoint 相同,Java 探针采用字节码增强技术实现,对业务代码无侵入。探针采集数据粒度相较于 Pinpoint 来说略粗,但性能表现优秀。目前,SkyWalking 增长势头强劲,社区活跃,中文文档齐全,没有语言障碍,支持多语言探针,这些都是 SkyWalking 的优势所在,还有就是 SkyWalking 支持很多框架,包括很多国产框架,例如,Dubbo、gRPC、SOFARPC 等等,也有很多开发者正在不断向社区提供更多插件以支持更多组件无缝接入 SkyWalking。
SkyWalking
SkyWalking 整体架构与核心概念
SkyWalking 是一个基于 OpenTracing 规范的、开源的 APM 系统,它是专门为微服务架构以及云原生架构而设计的。从 SkyWalking 6.0 开始,SkyWalking 将自身定义为一个观测性分析平台(Observability Analysis Platform,OAP)。SkyWalking 的核心功能有:
-
服务、服务实例、端点指标分析。
-
服务拓扑图分析
-
服务、服务实例和端点(Endpoint)SLA 分析
-
慢查询检测
-
告警
SkyWalking 如下特点:
-
多语言自动探针,支持 Java、.NET Code 等多种语言。
-
为多种开源项目提供了插件,为 Tomcat、 HttpClient、Spring、RabbitMQ、MySQL 等常见基础设施和组件提供了自动探针。
-
微内核 + 插件的架构,存储、集群管理、使用插件集合都可以进行自由选择。
-
支持告警。
-
优秀的可视化效果。
SkyWalking 分为三个核心部分:
-
Agent(探针):Agent 运行在各个服务实例中,负责采集服务实例的 Trace 、Metrics 等数据,然后通过 gRPC 方式上报给 SkyWalking 后端。
-
OAP:SkyWalking 的后端服务,其主要责任有两个。
- 一个是负责接收 Agent 上报上来的 Trace、Metrics 等数据,交给 Analysis Core (涉及 SkyWalking OAP 中的多个模块)进行流式分析,最终将分析得到的结果写入持久化存储中。SkyWalking 可以使用 ElasticSearch、H2、MySQL 等作为其持久化存储,一般线上使用 ElasticSearch 集群作为其后端存储。
- 另一个是负责响应 SkyWalking UI 界面发送来的查询请求,将前面持久化的数据查询出来,组成正确的响应结果返回给 UI 界面进行展示。
-
UI 界面:SkyWalking 前后端进行分离,该 UI 界面负责将用户的查询操作封装为 GraphQL 请求提交给 OAP 后端触发后续的查询操作,待拿到查询结果之后会在前端负责展示。
这里通过电商系统中的一个接口(请求 path 为"/query/userInfo"),来介绍一下 SkyWalking 中的三个核心概念,如下图所示:
-
Service(服务):用户服务是一个提供独立功能的模块,单独部署成一个集群并对外提供服务,这就是 SkyWalking 中的 Service(服务),这与微服务架构中的一个服务几乎是一样的。
-
ServiceInstance(服务实例):用户服务的集群是由多个部署了同一套代码的 JVM 节点构成的,对外提供了相同的处理能力,当请求进入系统时,由接入层进行负载均衡选择一个节点处理请求。用户服务中一个 JVM 节点即为一个 ServiceInstance(服务实例)。
-
Endpoint(端点):服务对外暴露的接口,例如这里的 "/query/userInfo" 接口,或是其他的 RPC 接口,就是 SkyWalking 中的 Endpoint(端点)。
Java Agent
Java Agent 是从 JDK1.5 开始引入的,算是一个比较老的技术了。作为 Java 的开发工程师,我们常用的命令之一就是 java 命令,而 Java Agent 本身就是 java 命令的一个参数(即 -javaagent)。-javaagent 参数之后需要指定一个 jar 包,这个 jar 包需要同时满足下面两个条件:
- 在 META-INF 目录下的 MANIFEST.MF 文件中必须指定 premain-class 配置项。
- premain-class 配置项指定的类必须提供了 premain() 方法。
在 Java 虚拟机启动时,执行 main() 函数之前,虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。用一句概括其功能的话就是:main() 函数之前的一个拦截器。
使用 Java Agent 的步骤大致如下:
-
定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。
-
创建 premain-class 配置项指定的类,并在其中实现 premain() 方法,方法签名如下:
public static void premain(String agentArgs, Instrumentation inst){
...
}
-
将 MANIFEST.MF 文件和 premain-class 指定的类一起打包成一个 jar 包。
-
使用 -javaagent 指定该 jar 包的路径即可执行其中的 premain() 方法。
Attach API
在 Java 5 中,Java 开发者只能通过 Java Agent 中的 premain() 方法在 main() 方法执行之前进行一些操作,这种方式在一定程度上限制了灵活性。Java 6 针对这种状况做出了改进,提供了一个 agentmain() 方法,Java 开发者可以在 main() 方法执行以后执行 agentmain() 方法实现一些特殊功能。
agentmain() 方法同样有两个重载,它们的参数与 premain() 方法相同,而且前者优先级也是高于后者的。
public static void agentmain (String agentArgs,
Instrumentation inst);[1]
public static void agentmain (String agentArgs); [2]
gentmain() 方法主要用在 JVM Attach 工具中,Attach API 是 Java 的扩展 API,可以向目标 JVM “附着”(Attach)一个代理工具程序,而这个代理工具程序的入口就是 agentmain() 方法。
Attach API 中有 2 个核心类需要特别说明:
- VirtualMachine 是对一个 Java 虚拟机的抽象,在 Attach 工具程序监控目标虚拟机的时候会用到该类。VirtualMachine 提供了 JVM 枚举、Attach、Detach 等基本操作。
- VirtualMachineDescriptor 是一个描述虚拟机的容器类,后面示例中会介绍它如何与 VirtualMachine 配合使用。
Byte Buddy
在 Java 的世界中,代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:
Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。
CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了CGLIB 。
Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。
Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。
Byte Buddy 动态增强代码总共有三种方式:
- subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。
- rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。
- redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。
OpenTracing
Trace 简介
一个 Trace 代表一个事务、请求或是流程在分布式系统中的执行过程。OpenTracing 中的一条 Trace 被认为是一个由多个 Span 组成的有向无环图( DAG 图),一个 Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 一般会有一个名称,一条 Trace 中 Span 是首尾连接的。
Span 简介
Span 代表系统中具有开始时间和执行时长的逻辑单元,Span 之间通过嵌套或者顺序排列建立逻辑因果关系。
每个 Span 中可以包含以下的信息:
-
操作名称:例如访问的具体 RPC 服务,访问的 URL 地址等;
-
起始时间;
-
结束时间;
-
Span Tag:一组键值对构成的 Span 标签集合,其中键必须为字符串类型,值可以是字符串、bool 值或者数字;
-
Span Log:一组 Span 的日志集合;
-
SpanContext:Trace 的全局上下文信息;
-
References:Span 之间的引用关系,下面详细说明 Span 之间的引用关系;
在一个 Trace 中,一个 Span 可以和一个或者多个 Span 间存在因果关系。目前,OpenTracing 定义了 ChildOf 和 FollowsFrom 两种 Span 之间的引用关系。这两种引用类型代表了子节点和父节点间的直接因果关系。
- ChildOf 关系:一个 Span 可能是一个父级 Span 的孩子,即为 ChildOf 关系。下面这些情况会构成 ChildOf 关系:
- 一个 HTTP 请求之中,被调用的服务端产生的 Span,与发起调用的客户端产生的 Span,就构成了 ChildOf 关系;
- 一个 SQL Insert 操作的 Span,和 ORM 的 save 方法的 Span 构成 ChildOf 关系。
- FollowsFrom 关系:在分布式系统中,一些上游系统(父节点)不以任何方式依赖下游系统(子节点)的执行结果,例如,上游系统通过消息队列向下游系统发送消息。这种情况下,下游系统对应的子 Span 和上游系统对应的父级 Span 之间是 FollowsFrom 关系
很明显,上述 ChildOf 关系中的父级 Span 都要等待子 Span 的返回,子 Span 的执行时间影响了其所在父级 Span 的执行时间,父级 Span 依赖子 Span 的执行结果。除了串行的任务之外,我们的逻辑中还有很多并行的任务,它们对应的 Span 也是并行的,这种情况下一个父级 Span 可以合并所有子 Span 的执行结果并等待所有并行子 Span 结束。
Logs 简介
每个 Span 可以进行多次 Logs 操作,每一次 Logs 操作,都需要带一个时间戳,以及一个可选的附加信息。在前文搭建的环境中,请求 http://localhost:8000/err 得到的 Trace 中就会通过 Logs 记录异常堆栈信息,如下图所示,其中不仅包括异常的堆栈信息,还包括了一些说明性的键值对信息:
Tags 简介
每个 Span 可以有多个键值对形式的 Tags,Tags 是没有时间戳的,只是为 Span 添加一些简单解释和补充信息。
SpanContext 和 Baggage
SpanContext 表示进程边界,在跨进调用时需要将一些全局信息,例如,TraceId、当前 SpanId 等信息封装到 Baggage 中传递到另一个进程(下游系统)中。
Baggage 是存储在 SpanContext 中的一个键值对集合。它会在一条 Trace 中全局传输,该 Trace 中的所有 Span 都可以获取到其中的信息。
需要注意的是,由于 Baggage 需要跨进程全局传输,就会涉及相关数据的序列化和反序列化操作,如果在 Baggage 中存放过多的数据,就会导致序列化和反序列化操作耗时变长,使整个系统的 RPC 的延迟增加、吞吐量下降。
虽然 Baggage 与 Span Tags 一样,都是键值对集合,但两者最大区别在于 Span Tags 中的信息不会跨进程传输,而 Baggage 需要全局传输。因此,OpenTracing 要求实现提供 Inject 和 Extract 两种操作,SpanContext 可以通过 Inject 操作向 Baggage 中添加键值对数据,通过 Extract 从 Baggage 中获取键值对数据。
JDK SPI 机制
什么是微内核架构?
微内核是一种典型的架构模式 ,区别于普通的设计模式,架构模式是一种高层模式,用于描述系统级的结构组成、相互关系及相关约束。微内核架构在开源框架中的应用也比较广泛,除了 ShardingSphere 之外,在主流的 PRC 框架 Dubbo 中也实现了自己的微内核架构。那么,在介绍什么是微内核架构之前,我们有必要先阐述这些开源框架会使用微内核架构的原因。
为什么要使用微内核架构?
从组成结构上讲, 微内核架构包含两部分组件:内核系统和插件 。这里的内核系统通常提供系统运行所需的最小功能集,而插件是独立的组件,包含自定义的各种业务代码,用来向内核系统增强或扩展额外的业务能力。一个概念就是经常在说的 API ,这是系统对外暴露的接口。而另一个概念就是 SPI(Service Provider Interface,服务提供接口),这是插件自身所具备的扩展点。就两者的关系而言,API 面向业务开发人员,而 SPI 面向框架开发人员。微内核架构本质上是为了提高系统的扩展性 。所谓扩展性,是指系统在经历不可避免的变更时所具有的灵活性,以及针对提供这样的灵活性所需要付出的成本间的平衡能力。也就是说,当在往系统中添加新业务时,不需要改变原有的各个组件,只需把新业务封闭在一个新的组件中就能完成整体业务的升级,我们认为这样的系统具有较好的可扩展性。
SPI(Service Provider Interface)主要是被框架开发人员使用的一种技术。例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,每个数据库厂商使用的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制为 java.sql.Driver 接口寻找具体的实现。
当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。
Sleuth+Zipkin
Spring Cloud Sleuth
SpanId
SpanId 一般被称为跨度 Id。在上图中,针对服务 A 的访问请求,通过 SpanId 来标识该请求的到达并返回的具体过程。显然,对于这个 Span 而言,势必需要明确 Span 的开始时间和结束时间,这两个时间之间的差值就是服务 A 对这个请求的处理时间。
TraceId
除了 SpanId 外,我们还需要 TraceId,也就是跟踪 Id。同样是在上图中,要想监控整个链路,我们不光需要关注服务 A 中的 Span,而是需要把请求通过所有服务的 Span 都串联起来。这时候就需要为这个请求生成一个全局的唯一性 Id,通过这个 Id 可以串联起如上图所示,从服务 A 到服务 F 的整个调用,这个唯一性 Id 就是 TraceId。
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 等第三方工具的应用和集成。
Zipkin
Zipkin 是一个开源的分布式跟踪系统,每个服务向 Zipkin 报告运行时数据,Zipkin 会根据调用关系通过 Zipkin UI 对整个调用链路中的数据实现可视化。
日志的收集组件 Collector,接收来自外部传输(Transport)的数据,将这些数据转换为 Zikpin 内部处理的 Span 格式,相当于兼顾数据收集和格式化的功能。这些收集的数据通过存储组件 Storage 进行存储,当前支持 Cassandra、Redis、HBase、MySQL、PostgreSQL、SQLite 等工具,默认存储在内存中。然后,所存储数据可以通过 RESTful API 对外暴露查询接口。更为有用的是,Zipkin 还提供了一套简单的 Web 界面,基于 API 组件的上层应用,可以方便而直观的查询和分析跟踪信息。
Zipkin 有四个组件:Collector,Storage,Search,Web UI。
Collector:是 Zipkin 的数据收集器,链路跟踪的数据到达 Zipkin 收集器,收集器会进行数据验证、存储。
Storage:是存储组件,Zipkin 默认是在内存中存储数据,内存存储是为了方便用户体验,真实使用中必须要将数据落地。支持 Elasticsearch 和 MySQL 等存储方式。
Search:是 Zipkin 的查询 API,当链路跟踪的数据被存储后,我们需要查询这些数据。Search 组件提供了简单的 JSON API,用于查找和检索数据。这个 API 的主要使用者是 Web UI。
Web UI:提供了可视化的操作界面,让我们能够方便,直观地查询链路跟踪的数据。