微服务治理:体系、架构及实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 服务度量

要管得到,必须先看得到!

要对微服务进行治理,先要对微服务进行度量。根据微服务的生命周期,可以将服务度量分为服务开发质量度量、服务测试质量度量、服务运维质量度量和服务线上性能度量四大部分。

2.2.1 服务开发质量度量

2.2.1.1 通过开发过程管理指标来衡量过程效率及质量

第1章介绍过,在微服务架构下通常会采用小团队、敏捷的开发模式,使用特定的需求和研发过程管理工具对业务需求、研发用例及研发进度进行全程管理。因此,从开发阶段的过程管理和成果管理中,可以获得很多相关度量指标。

目前流行的敏捷过程管理工具很多,Jira便是其中的典型代表。图2.9是笔者所在团队使用Jira进行敏捷迭代管理的一个功能截图。

图2.9 使用Jira做研发敏捷过程管理

Jira提供API接口,可以获取非常丰富的研发过程度量指标,包括一个研发团队开发一批业务需求所投入的人力资源和各个环节所耗费的时间等信息。以下是Jira提供的一些典型接口。

1.获取所有项目信息

通过API接口http://jiraserver:port/rest/api/2/project可以获取Jira中的所有项目的详细信息,包括项目ID、项目key、项目名称、项目负责人和项目类比/类型等信息。

2.获取单个项目信息

通过API接口http://jiraserver:port/rest/api/2/project/{projectId}可以获取某个项目ID对应项目的详细信息,包括项目组件列表、版本列表及项目相关角色列表等信息。

3.获取项目某个敏捷迭代(Sprint)中的所有issue(UserStory、Task、需求、Bug)

通过如上Jira提供的针对issue的API查询接口,利用带JQL的查询接口调用,可以查询某个项目下某个迭代周期中的所有UserStory及Task详情。

4.获取某个issue的详细信息

通过API接口http://jiraserver:port/rest/api/2/issue/{issueId/issueKey}可以获取某个issue(用户故事、任务、需求、Bug)的详细信息,包括创建时间、状态(及变更)、创建人、负责人、子任务等信息。

通过以上接口,定期收集敏捷过程中每个UserStory及对应Task的相关状态信息、变更时间信息、负责人员信息(开发人员、测试人员和验收人员)、对应服务信息(可通过自定义字段维护issue所关联的微服务)。基于时间轴将这些信息进行横向和纵向的组织及比对,再结合精益看板,即可从不同维度对微服务的开发过程进行全方位审核和把控。整个过程如图2.10所示。

图2.10 通过研发过程信息采集做微服务研发过程分析

但是,这样就够了吗?

不!我们还有一个庞大的“宝库”没有挖掘,那就是研发成果的最终归档物—源代码。

2.2.1.2 用代码读懂代码:衡量开发交付质量

回顾软件开发的流程,从前期的业务需求分析,到产品设计,再到架构设计,通过层层迭代,让所有关于业务及系统的思考、意图和策略最终都通过开发人员的代码表述出来。代码成了这些活动的最终产出物。

可以说,一个系统的源代码就是一本“书”,读懂这本“书”,我们就知道这个系统的“前世今生”。当然,深入(自动化)分析源代码也可以衡量服务的开发和设计质量。

在实际的开发工作中,大都采用面向对象的编程方式。我们把真实世界的业务实体映射成软件中的对象,实体间的关系就演变成了对象间的继承、实现和引用关系。因此通过对源代码的分析,可以知道软件系统的一系列关系逻辑,包括系统的调用入口在哪,以及系统API的实现和继承关系、类方法之间的引用关系等。如图2.11所示,如果将图左边的代码关系用图形化的方式呈现出来,就可以获得图右边的调用链路关系图。这类关系图对我们快速梳理和理解系统的逻辑非常有帮助,在此基础上也可以对微服务的调用质量进行优化。

源码是一个宝库,包含了很多的内容。假如有个“超人”能够记住所有的源码并理解透彻,那么很多治理的难题都能迎刃而解。问题一出来,超人就能快速地定位问题所在。奈何现实中没有超人,全盘读懂源码既是脑力活也是体力活,将成千上万个核心类的调用关系梳理出来并画出关系图,没有核心程序员好几周的辛苦努力是搞不定的。

图2.11 代码逻辑关系的梳理

“人力有穷时”,最好的办法是通过某种自动化手段,自动提取源码中的元素,自动梳理这些元素之间的关系,简言之就是“通过代码去理解代码”。

所幸,现在已经有一些能够对源码进行解析的工具和组件,JDT就是其中的典型代表。JDT的全称是Java Development Tools,是Eclipse的核心组件,主要用于Java程序的组织、编译、调试和运行等。在Java源码解析上,JDT提供了一个AST组件(Abstract Syntax Tree,抽象语法树)来做Java程序分析。通过AST,编译器会把代码转化成一棵抽象“语法树”,树上的每个节点代表一个代码元素(变量、方法、逻辑块等),同时针对节点的类型和属性解析提供完整的能力。

利用JDT-AST解析Java源码的基本能力展示如下所示。

通过JDT-AST可以解析出某个类所有引用的其他类(import)列表、类变量列表、类函数列表、函数内变量列表和函数内逻辑块。有了这些基础信息之后,再遍历每个方法中的每一行,通过正则表达式可以获取此代码行所调用的变量及其方法。比如,针对下面的代码:

通过正则表达式:

可以识别出如下两个子串:

对上面的结果稍加处理,可知上述代码分别调用了变量params的put方法和变量remind的getIsAdded方法。基于这个结论,再根据类变量列表及函数内变量列表匹配到对应的类上,即可获得某个类方法调用其他类方法的情况。微服务本身即以类方法(或接口)的形式存在,因此,通过这种方式可以获得微服务之间的调用关系,具体解析过程如图2.12所示。

图2.12 通过JDT-AST解析Java文件获取方法间的调用关系

有了这些信息,就可以逐个遍历方法,扫描方法的每一行代码,通过前面识别出的类变量及方法变量,找出这些变量的对外调用,从而构建出某个类方法对其他类方法的调用关系。如果把源码库中所有微服务工程的源码都进行扫描,可以获得一个 Map<StringList<String>>对象集合,Map的key是某个类方法,Value是其调用的其他类方法的集合(为了程序处理方便,可能还需要构建一个类似的被调用关系集合Map)。在此基础上对这个Map进行递归遍历,就可以找出所有这些类方法的调用链路关系,如图2.13所示。图中的F#Func1K#Func1是微服务的调用入口,一般都作为调用契约以接口的形式存在。在进行代码扫描时,要注意将其与实现类做关联(接口和实现类的关联关系可以通过AST获得。

图2.13 微服务内部及微服务间的方法级别的调用链路关系

把如图2.13所示的这些调用链路关系合并,可以构建一个如图2.11右边所示的完整的方法级别的调用矩阵,微服务间的调用是这个调用矩阵的一个子集。

图2.14是一个真实静态调用链的示例,以一个类方法为起点,找到它调用的所有其他方法,逐层遍历后,就能得到图中所显示的调用层级关系。图2.14-①是这种调用关系的文本描述,从图中可以清晰地看到方法间调用的先后和层级关系。

要注意的是类的实现和继承关系。接口类方法或者抽象类方法是没有具体实现逻辑的,所以在程序扫描时,还需维护类直接的继承和实现关系。接口方法往往用具体的实现类方法来代替,这样就能顺利地找到它的下一层引用关系。

如果引入诸如mxGraph这类图形化展示组件,可以将图2.14-①中类方法间的调用关系用一棵从上至下、从左至右的调用树图来展示,如图2.14-②所示。调用树上每一个节点就是一个类方法,节点间的箭头连线就是一个调用关系。通过JDT能够识别出方法注释,还可以将方法注释在每一个类方法节点的右边列出。如果系统注释完整,那么通过一张图就可以基本读清楚一个微服务入口方法的完整实现细节。

如果一个方法类的结构比较复杂,例如它有IF…ELSE关系或者FOR循环等嵌套调用关系,也可以用JDT识别,将这种关系在调用线条上列出。这样就能清楚地知道这是一种分支调用关系还是一种循环调用关系。

图2.14 静态调用链的文本展示形式和图形展示形式的对照

由于扫描的是所有相关工程的代码,一张图上就包含了所有层级的服务或系统之间的RPC调用关系。通过包名来对不同的业务层级(前台、中台、后台)进行识别,并为不同包名的图形单元赋予不同的颜色,通过颜色的区分可以清楚地知道一个方法的调用究竟涉及多少个系统,在每个系统中的入口是什么、出口又是什么等。

这里存在一个问题,就是如何将源码扫描获取到的类方法(微服务的API)与需求/开发任务管理系统中的UserStory和Task关联上?可以强制要求在微服务API入口方法(或者微服务类声明)的注解(例如JavaDoc)上标注UserStory和Task的ID,扫描源码时通过对注解的解析即可将方法和需求或任务进行关联。不用担心开发人员不标注或者忘了标注,因为我们可以通过比对需求列表和源码的映射关系来监控开发人员是否贯彻了注解标注规范,如果有需求没有找到对应的方法或API入口,即可自动通知相应的开发人员及时修改。

有了以上信息,通过对最终构建出来的大调用矩阵不同维度的分析,可以获得很多微服务的开发及设计质量方面的度量信息,包括请求的调用链深度、服务间的依赖程度、服务的粒度等。这些度量信息将会作为微服务治理的度量及判定依据。

小贴士

笔者另外准备了一个更完整的JDT-AST源码解析示例,能够详细展示如何通过对一个Java类文件的解析来获得类的相关调用关系,限于篇幅,就不在本书中贴出其详细源码了,请读者自行从本书的GitHub源码下载站点中下载并运行,体验解析过程。

2.2.2 服务测试质量度量

软件系统的发布质量在很大程度上依赖于测试的质量,微服务也不能免俗。微服务开发会涉及大量的测试工作,包括开发过程中的单元测试、调测、集成测试、自动化(接口)测试、契约测试及功能测试等。图2.15是目前业界针对微服务应用常用的测试架构。可以看到,构成这个测试架构体系最基础也是数量最多的是单元测试,随着开发过程逐步推进,其他涉及的测试工作还包括业务服务(组件)测试、契约测试和端到端的自动化测试等。

图2.15 微服务应用架构的测试金字塔

2.2.2.1 单元测试(调测)

单元测试是由开发人员主导的测试行为,它是功能(代码)提交的“准出证”。由于单元测试都以自动化的方式执行,在大量回归测试的场景下能有效提高测试效率。因此在敏捷开发中,鼓励多用单元测试用例来验证功能逻辑,这样更符合持续集成的基本要求—“能自动化尽量自动化”,从而形成良性高效的工作循环。

如图2.15所示,从完备性的角度来说,单元测试的数量应该是最多的。因此要获得较高的微服务开发质量,就必须保证有足够数量的单元测试用例。

如何做到这一点呢?可以使用2.2.1.2节中介绍的诸如JDT-AST的代码扫描技术,在扫描微服务的工程源码时同步扫描测试目录的源码,如果没有专门的测试目录也可以根据源码是否包含测试特征字符(比如,使用JUnit时,在测试方法上会使用@Test等注解声明)来识别测试类。通过对测试类所引用服务方法的自动化分析,获取每个微服务所对应单元测试的归集。这样可以对每个微服务的单元测试用例有一个量上的细化度量。在多个连续的迭代周期下,对每个微服务所对应的单元测试用例的数量进行时间的纵比,可得到微服务单元测试的覆盖率等质量属性的变化趋势。

2.2.2.2 功能测试

常规功能测试主要由测试人员完成,虽然在敏捷流程中提倡由开发人员相互测试,但对于一些涉及交易及合规的功能,还是需要由专职测试人员进行功能测试。功能测试覆盖集成测试和验收测试等相关测试领域。

功能测试用例的管理有很多种方式,可以使用TestLink这类功能较完备的测试用例管理系统,也可以使用更原始的Excel这类工具。不论使用何种方式,通过编写特定的扫描程序均可从这些系统或者工具中获得相关测试用例的详细信息,包括测试用例的创建者、审核者、用例执行状况及执行时间等。将同一个测试用例在多个迭代周期的信息进行比较可以获得这个测试用例的变化趋势;将同一个项目/需求在多个迭代周期的测试用例数量(个数/代码量)拉通进行比较,可以获得项目/需求的测试用例维护成本的变化情况。

测试用例执行后产生的测试Bug的管理同样有很多工具可选择,其中使用较普遍的是Jira。Jira不仅能管理需求,还能以issue的格式对测试Bug进行管理(issueType为“bug”),可以使用2.2.1.1节中介绍的方法从Jira系统中抽取Bug的相关处理流程信息。

通过从各系统或工具中抽取的测试用例信息、测试Bug信息、汇总的千行代码Bug率、服务用例数、服务测试Bug数、服务Bug处理效率及服务Bug重开率等,可以对微服务的开发质量进行客观评估。将同一个微服务多个迭代期间的测试数据进行纵向比较,可以进一步获取服务开发质量的变化趋势情况。换个维度,以测试人员和测试团队为基点进行汇总,则可获得测试工作的生产效率等信息。

2.2.2.3 契约测试

在微服务架构下,不同的微服务由不同的开发团队和开发人员负责,相互之间遵循一定的接口契约。一旦某个微服务的对外接口或接口逻辑发生变化,一定要将变更信息提前通知到依赖此服务的相关服务开发团队和人员,通过团队的沟通协调来保证接口的一致性,这是最常规的做法。在实际工作中,如果团队规模较大,人与人的沟通协调不一定可靠,经常会出现服务新版上线后调用出问题了才发现接口被改了的情况。因此需要一种可靠的机制来保证分布式调用下的接口一致性。“契约测试”可以有效地解决这个问题。

图2.16是“契约测试”的典型描述。所谓契约测试就是把服务调用分成两步走:第一步录制服务消费端的请求及它的预期返回结果,并将录制报告保存下来;第二步将这个报告在服务提供方进行回放,用录制的请求去调用服务提供方的服务,并将结果与之前录制的结果进行比对,一旦结果不一致,则说明接口发生了变化。

图2.16 契约测试

笔者之前所在的某个团队就是基于这个原理来实施契约测试的。它实际上是一个独立的应用程序,你可以把它看成一个放大版的单元测试套件被集成在了CI流程中。在CI的每日构建中,这个程序首先会调用服务的本地Mock服务(通过Mock构建微服务的测试能力将在第6章中详细介绍),把请求和结果都录制下来,这就是服务消费者预期的契约结果;然后用录制的请求去调用实际的服务提供者,并将结果和之前录制的契约结果比对,生成契约测试报告。这样,根据报告可以及时获知实际的接口是否发生了变化。整个系统架构如图2.17所示。

图2.17 集成在CI Pipeline中的契约测试服务架构图

需要强调的是,虽然接口契约定义文件和测试数据文件都定义了输入是什么,输出应该是什么,但接口契约定义和测试数据是不一样的。测试数据不一定是静态数据,它的入参和出参定义可能是脚本,而通过契约测试获取的录制报告(接口契约)一定是静态文件。所以录制的工作必不可少,不能直接拿测试数据文件做接口契约。

2.2.3 服务运维质量度量

为了保障线上系统的安全性和可靠性,对线上系统的管理和维护需要遵循严格的管理及审核机制。IT企业一般都建有完善的IT服务管理(ITSM)系统,按照一定的标准规范(例如ITIL标准)来对企业IT环境所涉及的人、事、物进行管理。

在进行微服务及其相关资源的上线、下线、扩容、缩容等操作时,先走ITSM的审批流程,指定需要使用资源的类型、容量和所在区域(DataZone)等信息。经过审批后,再由自动化运维平台进行资源的调配部署或由运维人员手工进行部署。如图2.18所示,是应用服务上线部署的自助申请流程中资源申请的表单示例。

图2.18 应用服务部署的ITSM申请单示例

通过程序或者脚本定期对ITSM中的申请流程实例进行增量抽取和汇总,汇总维度可以按应用/服务维度,也可以按组织(研发团队、事业部)维度。将汇总后的数据进行横向比较得到服务对线上资源的调度及占用情况分析。如果将同一服务在不同时间点的版本、资源占用和维护人员做成趋势图,可以直观地了解服务的变迁历史和演变轨迹。这些信息都可以为服务的未来规划提供依据。

通过对运维流程的流转状况分析获得运维的效率瓶颈并做出相应优化,可对一些占用人力资源过多的运维流程进行改进,或者增大自动化处理的比重。

2.2.4 服务线上性能度量

对微服务线上指标的监控本质上是对日志数据的监控。要对服务的性能和异常等线上运行状态进行全面客观的度量,需要完善的数据支撑。原始的度量数据来源于生产系统中的监控及日志采集,包含服务自身的度量数据和服务所在服务器及网络的度量数据。并通过IT运营分析(IT Operations Analytics),对采集到的海量运维数据进行多维度的有效推理与归纳,最终得出所需线上指标的分析结论。

2.2.4.1 系统监控

应用服务是部署在服务器上的,对应用服务的度量包含了对所在服务器的各项指标监控。服务器监控包括硬件监控和系统监控。硬件监控属于底层运维的范畴,与本书主题关联性不大,这里不做深入讨论,本节主要讨论部署微服务的服务器系统监控。

系统监控的具体监控指标很多,如图2.19所示。

图2.19 系统监控指标

在目前的服务器市场中,Linux占据了绝对的霸主地位。Linux系统自身提供了丰富的工具,可以对系统状况及性能进行监控。表2.3列举了Linux系统用于系统监控的部分命令和工具。

表2.3 Linux系统监控命令及工具集

续表

通过Agent可定期调用表2.3中的命令或工具来采集系统的各项信息,直接或间接(通过诸如Kafka这类消息服务来缓冲)汇总到统一的监控中心做进一步的分析处理。

目前有很多优秀的开源监控平台可选择,Zabbix就是其中的典型代表。它是一个分布式监控系统,将采集到的数据存放到数据库,然后对其进行分析整理,达到预先设定的阈值条件则触发告警。Zabbix支持多种采集方式和采集客户端,有专用的Agent代理,也支持SNMP、IPMI、JMX、Telnet、SSH等多种协议,具有丰富的功能、极强的可扩展性、完备的二次开发能力和简单易用等特点。读者只要稍加学习,即可利用Zabbix快速构建自己的系统级监控功能。

2.2.4.2 应用服务监控

对应用服务的监控主要通过采集应用系统的日志,进行实时和离线分析来综合获取应用服务的线上运行状态。图2.20是针对应用服务监控的典型架构图。可以看到,从日志的埋点、采集、传输、落地到分析涉及的组件很多,链路较长。

图2.20 监控系统典型架构图

1.日志埋点

要监控应用服务的详细运行状态,首先要在应用系统内部进行完善的日志埋点。当应用逻辑执行到埋点处时,埋点逻辑会收集上下文中的相关指标,并以日志文件的形式落盘记录。常规的日志埋点组件非常多,以Java为例,有Log4J、Logback、JDK自带的JUL、Apache开源的JCL及日志框架SEL4J等著名的开源日志组件。开发人员在必要的逻辑处插入日志记录,可以以灵活的格式记录所需的任何信息。以下是采用SEL4J记录日志的代码示例。

对于一些线上应用监控的基础指标,如调用耗时、异常采集、调用量汇总等,如果采用手动埋点的方式会非常麻烦。例如采集一个具体服务请求的调用耗时,需要开发人员在对应的微服务调用入口方法中加入如下所示的采集代码。

可以想象,如果在每个微服务的调用方法入口都添加这样一段采集埋点代码,是多么烦琐枯燥的事啊!有更简洁高效的埋点采集方式吗?参考2.1.2节中的微服务框架的架构设计,链式过滤器(Filter Chain)是微服务框架中的一个基础组件,每个请求都要被这条链上的每一个过滤器处理。因此可以开发一个专门记录调用请求耗时的过滤器,将其加入调用链中,通过这种AOP方式即可在微服务框架层面实现调用耗时的自动采集埋点。这种埋点方式不仅省时省力,还能有效避免人为处理不完善导致的错误。

除了调用耗时,业务异常和通用及自定义指标采集都可以采用专门定制的链式过滤器来处理。这种基于框架级的日志埋点可以有效降低指标采集的成本和风险,提高指标采集的一致性和稳定性。

针对微服务内部的方法调用,尤其是一些核心方法调用,例如存储资源(数据库、缓存、MQ)操作和核心业务服务处理等方法调用,也需要采集它们的调用耗时、调用状态及异常信息等。这类指标的采集可以借助一些APM产品的能力。APM产品对微服务内部方法的性能和异常监控一般通过一些字节码技术或者动态代理技术来实现。以Java为例,可以通过JDK自带的Instrumentation类加载代理组件或者InvocationHandler动态代理,使用ASM这类字节码框架在字节码层面“Hook”Java常用框架的相关方法。它们的原理有所不同。

● Instrumentation类加载代理组件通过Java Agent被引用。Instrumentation代理指定premain作为入口方法,实现了在main方法之前执行Java Agent。它通过addTransformer方法来加载ClassFileTransformer实现类,实现了在Class被装载到JVM堆栈之前将Class的字节码按预定规则进行转换。利用这种动态注入代码的策略,在调用入口增加方法的调用性能及异常指标的采集能力。

● InvocationHandler代理通过AgentWrapper触发invoke方法,在invoke方法中实现对被调用方法的拦截监控。由于是运行态时的动态拦截,运行效率要比字节码“织入”的实现方式差。

通过以上手工或自动化埋点方式,可以针对微服务应用的运行性能及运行状态进行指标抓取并以日志的形式落地。以下是一个典型的性能日志的格式,记录了一分钟内针对某台主机节点上某一服务接口的调用汇总信息。

这些信息包括了监控时间片(分钟)、服务名称、调用成功总量、失败总量、成功调用中被标识为业务失败的调用总量、平均调用延时、最大调用延时和最小调用延时等,各项信息之间采用逗号分隔。

2.日志采集

有效收集日志数据是线上服务监控的基础,因此对应用服务监控还需要一个灵活、完善、高效的日志采集工具。此类工具中开源的非常多,典型的有Flume、Filebeat、Logstash、Scribe等。这些工具都可以监听日志文件的变化,基于配置来增量采集日志数据,并发送给日志消费端做实时日志分析处理。表2.4展示了一些常用日志采集组件的特性比较。

表2.4 常用日志采集组件的特性比较

3.日志缓存

在微服务架构下,线上需要采集日志的节点数量庞大,如果每个节点上的采集Agent都直接将数据发送给日志分析处理服务,日志分析处理服务可能会被“压死”。参考图2.20,为了提高监控系统的抗压性,一般会在日志分析处理服务的前置步骤中增加一个由分布式消息服务(MQ)构建的“日志缓冲层”。当日志量太大,日志分析处理服务处理不过来时,可以先通过MQ将日志数据暂时缓存下来,后面再慢慢消化,起到“削峰填谷”的作用。MQ服务器的选择有很多,Kafka、RabbitMQ、ActiveMQ及Apache新近推出的Pulsar都是不错的选择。

4.日志实时分析

日志实时分析处理服务从消息队列获取最新采集的日志数据后,会根据预定义策略进行数据的各维度分析及汇总计算。具体有如下三大类操作:

● 根据阈值比较,进行指标告警操作;

● 将原始日志数据持久化;

● 对数据进行分钟级(或其他周期)的汇总统计,并将统计数据入库存储。

实时日志分析处理会源源不断地输入数据,就像流水一样,因此这种处理方式又称为“流式处理”。能进行流式处理的开源工具有Storm、Spark Streaming、Flink等。当然,也可以自主开发相应的实时分析工具。

5.日志存储

日志和分析数据的存储是监控平台面临的另一个挑战。原始日志数据一般是半结构化的,分析数据一般是结构化的,原始日志数据的量要远远大于分析汇总数据,因此它们的存储也各不相同。原始日志数据通常会采用分布式表格系统(例如Hbase、Cassandra等)或者分布式KV数据库(例如BigTable、Dynamo等)来存储。由于分析汇总数据有良好的结构并且总量比较确定,通常会采用关系型数据库(例如MySQL、PostgreSQL等)来存储。不过这也不是绝对的,一些结构化比较良好的原始日志数据也可以存储到关系型数据库或者时序数据库中。

6.日志离线分析

除了对原始日志进行实时流式处理,对存储后的数据还会进行大量的离线计算,以期深度挖掘日志(及其他运维)数据的内在关系和趋势。随着智能运维的兴起,很多算法模型都需要利用海量的运维数据进行大量离线计算来获取。大量离线运算对数据进行聚合或关联,会获得不同维度、不同汇总程度的中间数据,这就构成了一个庞大的数据集市。

对服务治理而言,线上监控数据是非常重要的度量指标。在监控平台相关数据的基础上,辅以一定的推导模型及算法,就能获取线上服务的相关健康度。

2.2.4.3 动态调用链跟踪

以上讨论的常规日志监控能力是以主机节点为视角的。对于一个跨网络、关联多个微服务的完整请求,每个微服务节点上的日志只能描述它的一部分状态,就算将其跨越的所有节点的日志收集完整,要进行关联也很困难。我们只能通过日志中请求业务相关的一些参数(比如用户ID或交易订单号等)找到关联的日志,聚合后才能看到这个请求的全貌。

1.微服务之间的动态调用链

为了解决服务节点之间日志割裂的问题,动态调用链跟踪技术应运而生。所谓的调用链是指完成一个业务过程,从前端到后端把所有参与执行的应用和服务根据先后顺序连接起来形成的一个树状结构的链。从动态调用链的角度看,每个节点上的请求过程都是其生命周期的一部分。同理,每个节点上的日志也都是整体日志的一部分。在请求发起时,会生成一个traceId(跟踪ID),这个traceId随着远程请求的调用被透传到不同的服务节点,并在相应的日志中落地。traceId的透传和落地通常由微服务框架或者APM组件负责,步骤如下。

1)在请求发起时,生成一个traceId,在服务消费方发起远程调用时,把此traceId附带上。

2)服务调用方会在处理业务请求之前,截取traceId,并写入日志组件的上下文中(比如,如果采用Log4J组件,只要开启其MDC功能,即可通过语句MDC.put"traceId"traceID)在线程上下文中写入这个traceId。如果遇到需要跨线程传递traceId的场景,可以考虑采用诸如阿里巴巴开源的Transmittable ThreadLocal这类组件。

3)在业务逻辑中,可以通过线程上下文找到此traceId,在记录日志时将其带上。还是以Log4J举例,只要通过类似如下所示的定义,即可在日志中自动带上traceId。

2.微服务内部的动态调用链

基于traceId可以串联起各个微服务节点之间的日志,并聚合出跨网络的调用链。虽然基于线程上下文的traceId也可以找到单个微服务内部的方法级的调用关系,但由于要在所有方法上进行埋点监控,所以成本非常高。因此很多APM产品在抓取服务应用的内部调用链时,除了使用动态插码技术,还会采用线程的堆栈跟踪技术。如下所示是一个简单的基于堆栈跟踪技术动态抓取内部调用链的示例。

以上程序的运行效果如下所示。从结果可见,通过线程的堆栈跟踪技术(代码行13)能够获取详细的方法调用链路信息,包括调用的方法名称及调用的代码行位置。再结合基于字节码的动态埋点,能够较完善地梳理服务内部方法间的调用关系。

这种方式只能抓取实际发生的调用关系,无法感知未被触发的调用关系。因此通过动态调用链跟踪获得的调用关系往往只是代码中所描述的所有调用关系的一部分。要获得更全面的调用关系需要使用通过代码扫描获得的静态调用链(见2.2.1.2节)。

关于调用链更深入的实现细节和使用场景,我们将在第5章中深入探讨。