大规模分布式系统架构下调测能力构建之道
大规模分布式系统架构下调测能力构建之道
最近有朋友辗转找到我,索要我今年参加QCon全球软件开发大会所用的PPT资料。在这里我将PPT和讲稿做了整理,分享给大家。
图1
这个分享,我首先会给大家总结一下,在分布式环境下做开发,我们都会遇到哪些调测方面的效率问题;并针对这些问题探讨在技术和管理上的应对之道;最后,通过我们所做的一个调测框架的展示来具体说明构建实践中的调测方法论。
图2 大纲
应用的发展演变历史
图3 应用的发展演变历史
在应用早期规模比较小的时候,我们其实并不太强调服务化的概念,所有的功能都聚集在一个单体应用中,整个系统的调用链路很简单,由于团队规模不大,沟通成本很低,协作上不存在大的障碍。
随着业务的发展,系统越来越复杂,所有的团队维护一个大系统的弊端就开始逐渐显现:编译时间越来越长、系统耦合度越来越高、热点模块大量挤占系统资源…。大一统的方式慢慢的不适应业务发展了。
这时候的应对之策,就是将一个大系统进行横向和纵向的拆分,横向上按业务拆分成若干子系统,纵向上则进行服务化的改造。子系统和服务独立部署,并由独立的团队负责,团队之间则通过定期的沟通协调机制来进行合作。
随着系统拆分和服务化的不断推进,各种服务相互依赖,关系错综复杂,团队间的沟通成本和损耗越来越高,这时候不能再靠“说”了,只能立“规矩”:即建立服务的调用协议、消息的传输协议、数据的存储协议,大家都按“规矩”做事,从而形成一系列的“契约”。包括分布式服务框架、分布式缓存、分布式消息就是这些“契约”的具体体现。
服务化的代价
图4 服务化后的问题
服务化是系统发展的必然趋势。服务化过程本质上就是一个“拆”字。系统被拆成了大大小小的应用集群和服务集群,本地调用变成分布式调用,中间增加了序列化、路由、负载均衡等一系列新的技术栈。
可见,在多团队协同的分布式环境下,依赖多了,整个环境变的很“重”,我们很难得到一个既稳定又能快速响应的外部环境,联调一个业务功能,我们要协调一堆的人,构建一整套的环境,成本会非常的高。
举几个典型例子:
1、分布式环境下我们要对一个服务做调测,如果依赖的远程服务还没开发完成,要么等,要么做mock。如果做mock,那我们就要从头到尾梳理代码,再写一堆的mock语句把远程服务全mock掉。每当业务逻辑变化了,我们需要同步修改mock代码;如果依赖服务上线了,还要把相应的mock代码去掉。对测试代码的修改工作贯穿于整个开发工作之中,工作量很大,测试用例的复用率很低。
2、另一种情况,我们现在一般都会用分布式服务框架做服务化的基础,如果把开发好的服务部署到线上去做联调,通过服务框架的路由和负载均衡策略作用后,我们不知道自己的调用请求会落到哪个服务节点上,尤其是在多人同时联调的情况下。这时,你要么写很复杂的路由策略,要么就一个个排队上线调测。这就是你在办公室经常听到“我要调试,你们别动联调环境啊”的原因。
3、还有一种情况是,我依赖的远程服务的调用接口入参没有变化,但实现逻辑变了,悲催的是对方忘了告诉我,这种问题往往非常隐蔽,尤其是数据驱动型的业务,往往要到上线的时候,问题才会暴露出来。
以上种种现象可见,用同一套环境进行调测往往存在服务交叉调用、数据相互覆盖、协调等一系列的问题,如果我们开发的机器性能还不错,往往会想,要不自己搭建一套完整的环境怎么样?一旦真的开始这么做,你会发现这根本是不可能实现的“梦想”!姑且不说你的机器扛不扛得住注册中心、消息服务、缓存服务这些基础服务,单是把所依赖的应用服务都找到并部署在机器上就能分分钟让你崩溃。我之前做电信软件的时候,一套分布式CRM系统的测试环境前前后后搭建了3个月,中间包含了大量沟通协调的成本。
综上所述,服务化后这些调测上的问题,本质上还在于我们对线上的依赖太重,而解决之道就是要通过技术手段,用轻量的方式在本地模拟出远程服务,从而切断我们对远程的依赖!
那么这种“模拟”和传统的“mock”又有什么区别呢?
分布式服务框架mock能力构建
图5 分布式服务框架mock能力构建
针对分布式服务怎么构建它的Mock能力?
分布式服务框架目前基本上是各个公司的标配了,大点的公司会自己构建分布式服务框架,比如像华为、新浪、阿里等;小公司则偏向使用一些开源的产品,比如阿里开源的dubbo或当当的dubbox,这些产品的主要以客户端路由的RPC框架为主。大多数服务框架都会提供基于插件的链式过滤器机制:一个服务调用在发起真实的远程调用之前要被层层过滤。因此,我们的mock能力可以构建在过滤器上,通过Mock数据文件来详细定义要被mock的服务的名称、入参及出参,并在服务消费端构建一个MOCK过滤器。当请求过来的时候,将服务名及入参和mock数据中的定义进行比对,结果吻合,则直接将mock数据文件中的出参作为服务的调用结果直接返回,同时远程调用的所有后续操作被终止。这样,就通过mock数据模拟了一个真实的远程服务。
Mock过滤器的启用可以通过配置文件来实现“开关控制”,可以只在开发和测试环境启用,生产环境关闭。
通过这种方式来构建服务的mock能力,我们就不需要写一堆的mock代码了,而且整个过程对业务逻辑来说完全无感知,完全把mock能力下沉到底层的服务框架。
应用服务Mock数据规范
图6 mock数据规范
通过这种方式构建的分布式服务的mock能力,除了mock过滤器,最核心的就是Mock数据的构建。
mock数据的质量直接决定了调测的质量!
说起mock数据,它所做的无非就是匹配哪个服务、输入的参数是什么,输出的结果又是什么。但实际的情况往往更复杂,你不可能通过静态数据去构建一个所谓的“当前时间”吧!因此,mock数据除了支持静态输入输出数据的比对,还需要支持动态匹配模式,也就是支持脚本匹配,我们所构建的服务mock框架,支持在mock数据中同时使用bsh和groovy这两种脚本。对于入参,可以返回true或者false,同时,出参的返回也可以采用脚本动态生成。
另外,一个服务集群中,往往会存在同一服务的不同版本,因此要真实模拟现实情况的话,mock数据也必须有版本的概念。
所以,要构建好一个Mock数据是需要投入不少工作量的,那么谁来做这个事情呢?这实际上牵涉到管理规范了。我们的规定是,服务谁提供,就由谁来构建这个mock数据,服务调用方可以在这个基础上做修改或者替换。但这还不够,由于服务会非常多,因此,对mock数据的管理一定要体系化和工程化。
我的建议是,可以采用独立的项目工程对mock数据进行独立的管理和发布。
在线抓取Mock数据
图7 在线抓取mock数据
前面我们说了,Mock数据的制作工作量很大,尤其对一些数据驱动的业务,比如,一个电信客服系统中的套餐开户,动不动就几百个字段,几十上百种的数据组合。那么,有没有办法把这个工作量再降一降。在这里,给大家推荐一种能够有效降低mock数据制作工作量的方法,那就是“在线抓取mock数据”。
对分布式服务框架来说,所有的服务请求都要经过链式过滤器,所以可以开发一个专门的请求抓取过滤器,将指定的服务请求的入参和返回结果都抓取下来,并直接写成mock数据文件。
在这个基础上,我们还可以增加一些更完善的功能,比如,可以指定抓取开启的时间段,也可以指定抓取的请求的数量;并针对每个服务生成独立的服务文件,文件中的每一条数据都是独立的请求。
当然了,这里还有一个合规性的问题,对线上数据的抓取是种敏感行为,大部分公司这么干都会很谨慎,所以一般会在测试环境中进行数据抓取。
通过抓取方式获得的mock数据文件,往往有更好的数据质量,毕竟反映的是更加真实的业务场景。
另外,通过抓取的方式,我们还可以获得更多的信息。全服务抓取模式下,数一数生成的mock文件数量就知道有多少服务被调用了,再数数每个mock文件中的数据条数,就知道这个服务被调用了多少次。把抓取插件部署在服务提供方,可以得到服务的负载情况,而如果部署在服务的调用方,则可以知道当前系统所依赖的外部服务情况。
应用服务直连调测
图8 应用服务直连调测
以上我们介绍了针对分布式应用服务的mock能力的构建。但在远程服务已经存在的情况下,我们还是希望能直接调用远程服务,这样才能获得最好的真实调用效果。但是,在分布式服务框架上,由于路由和负载均衡策略的影响,服务的调用往往会受到策略干扰。这时候,服务的直连调测能力就很重要了!
所谓服务直连调测是指,如果我们指定了特定服务的提供方地址,那么针对这个服务的调用就不再走路由和负载均衡策略了,而是直接向指定的远程地址发起服务调用。
很多时候,一个集群中服务会非常多,如果逐个指定服务和IP的映射关系,工作量也不小。实际上,特定团队往往负责某类业务服务的开发,这类服务一般都具有相同的包名,所以,现实中,我们在构建直连调测能力的时候,可以指定服务包名和IP的映射管理,批量的指定直连映射关系,减少配置工作量。
直连调测能力是一种很有效的调测方式。它可以绕过服务注册中心和集群的限制,在实际应用中也很普遍。
应用服务契约测试
图9 应用服务契约测试
通过前面介绍的mock手段及服务直连方式,我们能很好的解决分布式环境下本地调测的问题。使用mock时,mock数据的质量直接决定了服务调测的质量,mock数据应随着真实的外部服务发生的改变而改变。
问题是,我们如何及时的获取到服务接口或者服务的逻辑发生变化的信息?
最容易想到的是:通过团队的沟通协调来保证接口的一致性。但实际上,一旦团队规模变大了,人与人的沟通协调往往不一定可靠。我们经常是在服务上线,调用出问题了才发现接口被改了。所以,我们需要一种可靠的机制来保证分布式调用下的接口一致性。而“契约测试”可以很有效的解决这个问题。
图9是“契约测试”的一个典型描述。所谓契约测试就是把服务调用分成两部分来看待,首先,录制服务消费端的请求以及它的预期返回结果,并将录制报告保存下来;接下来,将这个报告在服务的提供方进行回放,用录制的请求去调用服务提供方的服务,并将结果和之前录制的结果进行比对,一旦比对不一致,则说明接口发生变化了。
我们也是基于这个原理来构建契约测试能力的。实际上,它是一个独立的应用程序,你可以把它看成一个大号的单元测试套件,并且被集成在了CI流程中。在CI的每日构建中,这个程序首先会调用服务的本地mock调测能力,把请求和结果都录制下来;接着,用录制的请求去调用实际的分布式服务,并将结果和之前的录制结果比对,生成契约测试报告。这样,根据报告,我们就可以及时获知实际的接口是否发生变化,我们是否要更新对应的mock文件。
诶,我们不是已经有测试数据文件了吗?为什么不拿它直接作为契约测试的报告呢?我们之前已经说过了,测试数据不一定是静态数据,它的入参和出参定义可能是脚本,而契约测试的录制报告一定是静态文件,所以录制的这个工作还是不能少。
应用服务综合mock能力
图10 应用服务综合mock能力
现实的开发工作中,我们开发的应用所依赖的服务往往很分散,一部分服务在本地,一部分服务需要其它的团队来提供,还有一部分服务则只有一个接口定义,需要通过定义mock文件来进行模拟。这时候,我们就需要综合利用前面所介绍的mock及直连调测能力来保障日常开发中对应用服务的正常调测。
图10是我们目前在使用的一个典型的调测能力组合,采用的是本地服务优先机制,即如果这个服务本地有,则直接本地调用;如果没有,再判断在直连调测中是否定义了这个服务的IP地址,如果有,绕过路由和负载均衡策略直接发起远程调用;如果没有,再继续判断是否定义了服务的mock数据,如果定义了,则走mock调用;如果没有,就走正常的分布式调用模式。
当然了,大家也可以根据自己的实际开发需求来灵活组织这些能力。
以上,我们都在讨论分布式应用服务的本地调测能力的构建及相关保障机制。
接下来,再介绍一下针对其它分布式服务的调测能力构建。
分布式消息Mock
图11 分布式消息mock
分布式环境下,另外一个很重要的服务就是消息服务。现在很多应用都会用消息服务来做业务及技术上的架构解耦。图11是一个典型的分布式消息服务架构图,可以看到,不同的Topic分布在了不同的broker上,消息的生产者、消费者、主题等信息都在注册中心进行注册和调度,整个的架构比较复杂。
如果我们要在单机上模拟MQ服务,是不是也要做得这么复杂呢?当然不用!
分布式消息队列首要考虑的是消息的海量并发和消息的可靠性。在架构设计上要做很多的冗余设计,而这些在本地消息服务的mock上都不是我们要关注的重点。本地调测中,我们基本上不会发出海量消息,也不关注消息是不是持久可靠,我们更关注的是消息接口的一致性,不能让消息的调用方和消费方感觉到接口不一致。所以,对功能以及接口的完整复现是我们的优先关注点。
图12是对分布式消息服务进行本地黑盒仿真的技术架构图。可以看到,用JVM的blockQueue队列来承载每个独立的Topic,用单个线程作为选择器来统一进行消息的分发。不让消息消费者直接监听消息队列,而由独立选择器来处理的这种架构可以有效解决多个消息消费者订阅同一种消息以及BlockQueue队列只能被取出一次的矛盾。
分布式缓存Mock
图12 分布式缓存mock
接下来,我们再介绍一下针对分布式缓存的本地调试能力的构建。
对分布式缓存服务而言,我们同样也能通过JVM的相关机制来构建它的本地Mock。目前分布式缓存用的比较多的主要是memcached和redis两种。Memcached的模拟比较简单,用JVM自带的comcurrentHashMap即可很好的模拟。但redis的情况就复杂了,我们知道,redis的一大特点就是支持的数据类型多,除了经典的KV模型,它还支持Hash、List、Set、SortedSet等数据格式,针对这些redis数据类型,我们需要用不同的jvm类型组合来映射它。图12右面的表格就是我们用来模拟redis的相关java类型,对于Hash,可以用Map类型来作为它的value,List类型则可以用ArrayBlockingQueue,Set可以用HashSet,而SortedSet则可以用TreeMap。
另外,redis还支持分布式锁功能。在本地,可以采用ConcurrentHashMap的putIfAbsent来模拟它。
分布式缓存的数据还可以设置时效性,这是一个应用很普遍的功能,那么我们该怎么去模拟它呢?这里,我给大家的建议是,不用去模拟,给一个空实现即可!为什么呢?因为在现实的调测活动中根本就用不上这个时效性,功能调测一般都是短时行为,基本上不会牵涉数据的过期问题。
如果真的要实现时效性,也很好解决,用取时检查机制可以很好的解决这个问题。另外,用DelayQueue可以实现主动的到期删除缓存数据的能力。
当然了,如果大家不想花这么多力气去构建redis的缓存模拟能力,也可以使用redis的单机服务。目前,redis提供针对不同操作系统的版本,在一般应用强度下,对资源的占用并不高(几十M到几百M)。当然了,现实中,考虑一体化的问题,可能还要将redis服务和开发平台进行整合,至于整合到什么程度,就要看规划及资源投入程度了。
其它分布式服务的调测支持
图13 其他分布式服务的调测支持
其它的分布式服务,如果其在逻辑上支持“租户”隔离机制的话,那么这种服务对多团队并行开发的支持会比较好,一般情况下不会成为调测效率的瓶颈。针对这类服务,不用急于构建其Mock服务,可以根据其特性,寻找成本较低的实现途径。
比如说,针对分布式存储,可以为不同开发团队和人员配置独立的文件路径(Direction、Bucket)来进行有效的资源隔离。当然了,如果确实有必要,我们也可以采用JVM的本地文件I/O来对它进行模拟。
至于数据库,手段就比较多了,从单机数据库到独立数据库服务的不同schema均可选择。
另外,针对持久化存储的分布式服务,还要考虑预置数据的前置导入及后置清除能力。这些都需要通过测试框架来实现。
完整的调测框架
图14 完整的调测框架
以上我们介绍了针对各类分布式服务的本地调测能力的构建。在工作中,我们还是要根据实际情况,去综合的使用这些能力。
如图14,是我们之前构建的一套调测平台,以其做为示例,来详细说明如何在开发框架中整合这些调测能力。从图上我们可以看到,这些分布式服务的调测能力被代理模式包装了一层,再内置到开发框架之中,通过配置文件的多层开关机制即可以对他们进行启用和关闭。
我们模拟的是分布式远程服务,除了对分布式远程服务的的调用模拟外,有时候还要对中间经过的网络进行模拟,包括网络的延时和网络错误。这部分的模拟也可以构建在调用过滤器上。
同时,为了给上层的业务开发提供更好的体验,还需提供支持预置数据管理的测试骨架。通过测试骨架,我们可以快速构建针对各类场景的单元测试用例和套件。
这是运行框架层面的能力,但对于一个完整的调测平台而言,只有这些能力还不够,我们还需要对一堆的Mock数据、预置数据、测试用例、测试套件、以及用例的执行提供统一的管理能力。开发环境下,管理能力最好的载体就是IDE了,我们以Eclipse插件的形式构建了一系列的管理能力,并集成在eclipse中。这样,才能给开发人员提供一个成体系化的开发调测环境。
调测方法论
图15 调测方法论
最后,我们再探讨一下如何在开发阶段有效的使用以上的调测能力。前面实际上已经介绍了很多的使用方法,包括mock数据的制作管理规范等等。这里,我们主要介绍一下在开发的不同阶段怎么组合利用这些调测能力。
在开发的早期阶段,我们可能只是定义了大家遵循的服务的接口,但是还没有接口的具体实现,这时候,mock能力会被频繁的使用,服务直连调测的比例会比较低。随着开发的进行,服务被不断的开发出来,并部署到线上,这时候,mock的比例会逐步降低,直连调测的比例逐步增加。当开发完成时,已经没有被mock的服务了,所有的服务都是线上服务,我们要么直连调测,要么走正常的分布式调用。
联系方式
以下是我联系方式,如果大家在相关领域有疑问或者建议,可以与我联系,共同探讨。
-完-
欢迎关注,欢迎分享!