要管理它,首先要看到它!
要管理微服务,首先要测量微服务。根据微服务的生命周期,服务度量可以分为四个部分:服务开发质量度量、服务测试质量度量、服务运维质量度量和服务在线性能度量。
服务开发质量度量通过开发过程管理指标来衡量过程效率及质量
在微服务架构下,通常采用小团队和敏捷开发模式,具体需求和研发;过程管理工具用于管理业务需求、研发和管理。用例及研发;整个过程的进展。因此,从开发阶段的过程管理和结果管理中可以获得许多相关的度量。
目前,流行的敏捷过程管理工具有很多,吉拉是其中的典型代表。图2.9是作者团队使用吉拉的敏捷迭代管理的功能截图。
图2.9使用吉拉进行研发;敏捷流程管理
吉拉提供API接口,可以获得非常丰富的研发指标;d过程,包括诸如研发投入的人力资源等信息;d团队在开发一批业务需求和每个环节所花费的时间。以下是吉拉提供的一些典型接口。
1.获取所有项目信息
可以通过API接口http://jira server 3360 port/rest/API/2/project获得吉拉所有项目的详细信息,包括项目ID、项目key、项目名称、项目负责人、项目类比/类型等。
2.获取单个项目的信息
通过API接口http://jiraserver 3360 port/rest/API/2/project/{ projected },可以得到一个项目ID对应的项目的详细信息,包括项目组件列表、版本列表和项目相关角色列表。
3.获取项目敏捷迭代(Sprint)中的所有问题(用户故事、任务、需求、bug)。
http://jiraserver : port/rest/API/2/search?jql=project={projectKey}和sprint={sprintKey}
通过上面吉拉提供的问题API查询接口,您可以通过调用JQL的查询接口,查询某个项目下一个迭代周期内的所有用户故事和任务的详细信息。
4.获取问题的详细信息
可以通过API接口http://jiraserver 3360 port/rest/API/2/issue/{ issue id/issue key }获取一个问题(用户故事、任务、需求、Bug)的详细信息,包括创建时间、状态(及变更)、创建人、负责人、子任务等信息。
通过上述接口,可以定期收集敏捷过程中每个用户故事和相应任务的相关状态信息、变更时间信息、负责人信息(开发人员、测试人员、检查人员)以及相应的服务信息(可以通过自定义字段维护与问题相关联的微服务)。通过基于时间轴对这些信息进行横向和纵向的组织和对比,结合精益看板,可以从不同维度对微服务的开发过程进行全面的审计和控制。整个过程如图2.10所示。
图2.10微服务研发分析;研发过程中的信息收集;d过程
但这就够了吗?
不要!我们还有一个巨大的"宝库"没有被挖掘,那就是R & ampd结果-源代码。
用代码“读懂”代码:衡量开发交付质量
回顾软件开发的过程,从早期的业务需求分析,到产品设计,再到架构设计,所有关于业务和系统的思想、意图和策略,最终都是通过迭代,通过开发者的代码表达出来的。代码已经成为这些活动的最终输出。
可以说,一个系统的源代码就是一本书。看完这本书,我们就知道这个系统的前世了。当然,对源代码的深入(自动化)分析也可以衡量服务开发和设计的质量。
在实际开发工作中,多采用面向对象编程。当我们将现实世界中的业务实体映射到软件中的对象时,实体之间的关系就变成了对象之间的继承、实现和引用关系。所以通过对源代码的分析,可以知道软件系统的一系列关系逻辑,包括系统的调用入口在哪里,系统API的实现和继承关系,类方法之间的引用关系等等。如图2.11所示,如果图形左边的代码关系用图形表示,就可以得到图形右边的调用链路关系图形。这种图非常有助于我们快速梳理和理解系统的逻辑,在此基础上还可以优化微服务的调用质量。
源代码是一个宝库,包含了很多内容。如果有一个“超人”能记住所有的源代码,并彻底理解它们,那么许多治理问题就能迎刃而解。问题一出,超人就能快速定位问题。但是现实中没有超级。
人,全盘读懂源码既是脑力活也是体力活,将成千上万个核心类的调用关系梳理出来并画出关系图,没有核心程序员好几周的辛苦努力是搞不定的。图2.11 代码逻辑关系的梳理
“人力有穷时”,最好的办法是通过某种自动化手段,自动提取源码中的元素,自动梳理这些元素之间的关系,简言之就是“通过代码去理解代码”。
所幸,现在已经有一些能够对源码进行解析的工具和组件,JDT就是其中的典型代表。JDT的全称是Java Development Tools,是Eclipse的核心组件,主要用于Java程序的组织、编译、调试和运行等。在Java源码解析上,JDT提供了一个AST组件(Abstract Syntax Tree,抽象语法树)来做Java程序分析。通过AST,编译器会把代码转化成一棵抽象“语法树”,树上的每个节点代表一个代码元素(变量、方法、逻辑块等),同时针对节点的类型和属性解析提供完整的能力。
利用JDT-AST解析Java源码的基本能力展示如下所示。
01. //获取Java源码
02. String content = read(javaFilePath);
03. //创建语法解析器
04. ASTParser parsert = ASTParser.newParser(AST.JLS4);
05. //设定解析器的源代码字符
06. parsert.setSource(content.toCharArray());
07. //使用解析器进行解析并返回AST上下文结果(CompilationUnit为根节点)
08. CompilationUnit result = (CompilationUnit) parsert.createAST(null);
09. //获取类型
10. List types = result.types();
11. //取得类型声明(可能有多个类定义)
12. TypeDeclaration typeDec = (TypeDeclaration) types.get(0);
13. //取得包名
14. PackageDeclaration packetDec = result.getPackage();
15. //取得类名
16. String className = typeDec.getName().toString();
17. //取得函数(Method)声明列表
18. MethodDeclaration methodDec<> = typeDec.getMethods();
19. //取得函数(Field)声明列表
20. FieldDeclaration fieldDec<> = typeDec.getFields();
21. //继承的类或者实现的接口
22. for (Object obj :typeDec.superInterfaceTypes()){
23. System.out.println("interface:" + obj);
24. }
25. System.out.println("extends:" + typeDec.getSuperclassType());
26. //输出包名
27. System.out.println("包名:" + packetDec.getName());
28. //输出类名
29. System.out.println("类名:" + className);
30. //输出引用import
31. System.out.println("引用import:");
32. for (Object obj :result.imports()) {
33. ImportDeclaration importDec = (ImportDeclaration) obj;
34. System.out.println(" " + importDec.getName());
35. }
36.
37. //循环输出变量
38. for (FieldDeclaration fieldDecEle :fieldDec) {
39. for (Object obj :fieldDecEle.fragments()) {
40. System.out.println("类变量:"+fieldDecEle.getType()+" " + ((VariableDeclarationFragment) obj).getName());
41. }
42. }
43. for (MethodDeclaration method :methodDec) {
44. System.out.println("方法:" + method.getName());
45. //遍历方法内变量
46. ListmParams = method.parameters();
47. if (mParams != null) {
48. for (int i = 0;i < mParams.size();i++) {
49. SingleVariableDeclaration sVar = mParams.get(i);
50. System.out.println(" 方法变量:" + sVar. getType(). toString() + " " + sVar.getName().toString());
51. }
52. }
53. //遍历方法内逻辑块
54. Block body = method.getBody();
55. if (body == null) {
56. continue;
57. }
58. List statements = body.statements();
59. Iterator iter = statements.iterator();
60. while (iter.hasNext()) {
61. Statement stmt = (Statement) iter.next();
62. System.out.println(" 逻辑块类型:" + stmt.getClass(). getSimpleName());
63. }
64. }
通过JDT-AST可以解析出某个类所有引用的其他类(import)列表、类变量列表、类函数列表、函数内变量列表和函数内逻辑块。有了这些基础信息之后,再遍历每个方法中的每一行,通过正则表达式可以获取此代码行所调用的变量及其方法。比如,针对下面的代码:
params.put("isAdded", remind.getIsAdded());
通过正则表达式:
可以识别出如下两个子串:
1.params.put(
2.remind.getIsAdded(
对上面的结果稍加处理,可知上述代码分别调用了变量params的put方法和变量remind的getIsAdded方法。基于这个结论,再根据类变量列表及函数内变量列表匹配到对应的类上,即可获得某个类方法调用其他类方法的情况。微服务本身即以类方法(或接口)的形式存在,因此,通过这种方式可以获得微服务之间的调用关系,具体解析过程如图2.12所示。
图2.12 通过JDT-AST解析Java文件获取方法间的调用关系
有了这些信息,就可以逐个遍历方法,扫描方法的每一行代码,通过前面识别出的类变量及方法变量,找出这些变量的对外调用,从而构建出某个类方法对其他类方法的调用关系。如果把源码库中所有微服务工程的源码都进行扫描,可以获得一个Map<string,list></string,list对象集合,Map的key是某个类方法,Value是其调用的其他类方法的集合(为了程序处理方便,可能还需要构建一个类似的被调用关系集合Map)。在此基础上对这个Map进行递归遍历,就可以找出所有这些类方法的调用链路关系,如图2.13所示。图中的F#Func1和K#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入口,即可自动通知相应的开发人员及时修改。
有了以上信息,通过对最终构建出来的大调用矩阵不同维度的分析,可以获得很多微服务的开发及设计质量方面的度量信息,包括请求的调用链深度、服务间的依赖程度、服务的粒度等。这些度量信息将会作为微服务治理的度量及判定依据。
作者:李鑫 本文节选自《微服务治理:体系、架构及实践》一书