JLow 2020-11-12
【51CTO.com快译】您是否已在自己手头的项目中使用到了微服务?在使用的过程中,您是否碰到过一些意料之外的问题?本文将通过分析基于Spring Cloud的微服务系统、jQAssistant和Neo4j,与您讨论如何用图形技术,来实现检测反模式(antipattern)、可视化全系统、以及跨服务影响分析。
背景介绍
几年前,我应客户要求用微服务来重写他们的IT系统。可是到了开发的末期,我们碰到了代码缺陷、未能回归等问题。虽然现场团队进行了反复审查,也召开了重构会议,但是由于系统相当庞大(生产环境中的微服务超过了13个),实在难以调整,而且收效甚微,因此最终产品的质量远未达到他们的期望。
可见,人们往往会低估从传统方法转换为微服务架构的难度,特别是当生产环境中有成千上万行代码时,人们时常无法对目标系统拥有清晰的认识,未能确定待执行任务的优先级,直到出现问题时已为时已晚。
使用图形分析来识别问题并修复微服务架构
经过研究,我发现了一个非常实用的工具—jQAssistant。它能够帮助我将代码、软件架构、及其附带的所有规则,都转换成为Neo4j中的图形。
我发现图形的转换将有助于:检测反模式,进行影响分析,改善数据治理,以及促进团队之间的沟通等方面。下面,我们将通过分析一个具体应用示例,来详细讨论图形化转换的过程。
一个应用示例
在此,我们将使用经典的Piggy Metrics项目。它是由Spring Boot和Spring Cloud开发,基于MongoDB数据库的个人记账式微服务应用。
该应用程序的图形转换相对比较简单。如上图所示,我们只需准备构建应用所需的JAR文件,然后通过命令行将JAR文件提供给jQAssistant即可。几秒钟之后,您将会获得对应的简单图形。
如上图代码段所述,来自Piggy Metrics应用的类–UserController是一个标准的Spring RestController。它定义了两个名为getUser和createUser的方法。两者都带有绿色的@RequestMapping注释。在这些注释中,我们可以找到与各个NPC相映射的HTTP动词。最后则是名为userService的调用。下图便是该controller所对应的图形。
由图可见,代码的不同部分会被通过各种节点与关系来表示。例如:在左侧,红色的节点表示JAR文件,它通过CONTAINS关系链接到蓝色UserController类。而UserController类本身会链接到getUser和createUser方法,以便进一步调用其他方法。当然,我们在前面代码中看到的注释、请求映射、以及参数等,都会以绿色显示在图形中。据此,我们将能够轻松地检查代码、类或模块之间的循环依赖关系,命名约定的遵循,以及测试是否覆盖了特定代码等架构规则。
松耦合的微服务
许多人认为:松散耦合的微服务是通过HTTP、异步协议、以及消息协议进行通信的,而此类通信并不能反映到字节码上。我们却需要在此基础上使用图形来表示代码。也就是说,除了语言和框架,我们还会在应用中通过软件架构,这一更高层次的概念,来表示API、以及不同的工程实践。
下面让我们回到上面的UserController例子。由于采用了Spring框架,因此我们可以遍历图形,以使用各种正确的注释来标识出方法,进而关联映射到一些HTTP动词上。
上图展示了一个Cypher查询。通过浏览带有@RequestMapping注解的方法,HTTP信息被输入到了图中。端点(Endpoint)作为一个新的概念被引入到了图中。图中左侧蓝色的代码行,能够以指令的形式,将endpoint标签添加到发现的内容中,并将HTTP映射信息,添加到那些作为REST端点被公开的已知方法节点上。在此,我们定义了一些浅蓝色的REST端点。它们能够与前文提到的getUser和createUser两种方法,及其不同的路径相匹配。
参照端点定义的方式,我们还可以定义HTTP客户端的相关概念。例如:由于Piggy Metrics应用采用了Feign库在两项服务之间进行HTTP调用,因此我们可以使用它来遍历图形,并通过查找FeignClient注释、及其相关信息,来创建新的概念。
就像公开某些REST端点的控制器一样,我们制作了如上图所说的HTTP客户端。它通过HTTP动词来调用URL。也就是说,我们可以将带有URL信息的FeignClient标签作为新的概念添加到图形中。
一旦确定了HTTP客户端和REST端点,我们就可以根据这些新的概念,轻松地将它们连接到各种匹配的HTTP方法,及其用到的URL上。例如,在如下代码图中,我们通过被称为INVOKES_REMOTE的关系,在图中展示该过程。
由于服务是松散耦合的,因此,我们可以确定跨服务的依赖关系,让这些松耦合能够在图中变得显而易见。从下图可视化的内容中,我们能够清晰地查看到四项服务,以及它们之间的相互调用。
据此,我们可以找到其中可能存在的反模式,例如那些导致“鸡生蛋、鸡生蛋”的服务间循环问题。当然,我们也可以通过采取影响分析,以确定诸如修改端点的难度等方面的问题。
微服务还是分布式整体?
通过图表分析,我们也可以查看微服务是否遵循了最佳实践,进而提高服务实现的成熟度。如下图例子所示:
数据治理
在前面的Piggy Metrics应用中,我们通过MongoDB和Spring Data,不但可以轻松地定义持久性实体的概念,而且能够检查跨服务的MongoDB集合的使用情况。下图展示了我们查询到的该帐户集合被两个不同的服务所调用的情况。
根据这两个服务之间的隔离情况,我们判断应当合并它们。当然,我们还能够发现更多同一个数据库同时在多个位置被管理的情况。据此,我们也可以定义哪个数据存储库的更改,会直接或间接地影响到哪些端点。
如上面的数据表所示,底部的“UserRepository”被远程服务间接地使用着。如果想更改它,我们需要检查远程服务可能受到的影响。而这些是仅靠查看存储库的相关代码、或单个服务,所无法获悉的。
通过前文提到的jQAssistant,我们可以针对JDBC去扫描数据库中的元数据,并将该数据库的结构映射到图形中。据此,我们进而可以对MongoDB采取更高级的影响分析,或针对验证进行数据列级别的分析。也就是说,如果我们想更改某个数据库的数据格式、或者是列的长度,那么就需要识别与该列相关的所有对象。
文档是否最新?
微服务领域的另一个最佳实践是:事先采用契约优先(contract-first)的方法,即:先定义API规范,然后予以实施。例如,我们可以先选用目前行业标准化的OpenAPI规范,然后使用YAML文件来编写API契约。不过,您可能遇到的问题是:如何知晓自己实施的规范或文档是否为最新呢?
如上图所示,我们可以通过扫描YAML文件,在文件内容中找到OpenAPI的相关描述。也就是说,通过遍历所有的YAML文件,我们查找名为OpenAPI的密钥,以检查每个服务是否包含了至少一个规范文件。而且,这是一种快速检索文档缺失的方法。
此外,就文档本身而言,我们甚至可以进一步深入地了解文档的具体内容。例如,我们既可以从中提取参数名称与类型,又可以使用Spring controller执行相同的操作,并将两者的实际检测差距进行比较。
如下图所示,通过查询,我们很快发现到某项服务缺少对应的文档。即:在三个端点中,只有两个端点拥有实际文档。此外,我们还能深入地“挖掘”API参数,及其参数类型。
鲁棒性
鲁棒性是非常重要的方面,它能够应对生产环境中的故障,以免整个系统的崩溃。通常,我们可以采用诸如断路器(circuit breakers)和响应回退(response fallbacks)之类的主流机制,以避免让故障影响到上层服务。
为了检查所有端点、或HTTP客户端是否已实施了正确的回退,我们可以使用上述简单的查询,通过遍历FeignClient注释,来查看它们是否具有声明为fallback的属性。如上图所示,在Piggy Metric示例中,我们发现服务中缺少了两个fallbacks。
信息共享
为了共享,我们往往需要创建一个可视化的文件。而拥有开放式图形数据库的优势就在于:您可以有选择地将图形导出为GraphML文件,进而采用yEd之类的工具将GraphML文件导出为可视化的数据。而且,在yEd中,您可以自定义样式与布局,以满足大量元素的实际需求。
如下图所示,您可以导出许多不同形状的架构数据。作为服务之间依赖关系的简单示例,我们既可以通过对其进行更改,以提供所需的内容,又可以导出为实用的dock可视化。
可见,此类可视化转换对于开发者团队和架构师之间的交流,产品所有者与项目经理之间的对话,以及让他们更好地了解需要完成的工作,都是非常实用的。为了让您对系统的图形化有个整体的印象,下图展示了一个真实的应用示例。
值得一提的是,如果不借助工具,我们很难用手动完成此类图形的构建。何况我们的系统往往也是动态发展的。
更多实用场景
上述示例可能过于简单。在实际应用中,图形分析还可以被运用到更多的实际场景中。例如,我们可以通过依赖项分析,更深入地研究安全性问题,进而通过寻找端点上的注释予以安全加固。此外,我们可以通过导入运行时(runtime)的数据,以获悉某个API每天被调用的次数,进而为该API重要性分配权值。当然,我们也可以从版本控制系统中导入数据,以便根据源代码的依赖性,对修改进行优先级排序。显然,那些被鲜少修改的服务,就意味着它基本能够正常工作,我们也就不必过于关注了。