你的微服务敢独立交付么?

最近经常在项目或是社区里听到大家谈论微服务架构,但谈论的焦点更多集中在微服务拆分,分布式架构,微服务门槛,DevOps配套设施等话题上。

但是在我眼里,真正能称之为微服务架构的少之又少。原因也很简单,我所见到的很多所谓的微服务架构项目,大多都没有做到微服务架构的一个基本要求:服务的独立部署(交付)。

这里的独立部署和自动化部署还不是一个概念,服务的自动化部署相对简单,已有大量的工具可以帮助我们做到。但是这里所谈的独立部署,我认为关键和难点并不在于“部署”,而在于“独立”。

如果失去了服务独立部署(交付)的能力,一个微服务架构的威力将大打折扣,我们的系统虽然在物理上被拆分成了多个小的服务,但是如果从最终交付的角度来看,仍然是以一个整体存在的,就像单体应用一样,存在诸多的问题。

为什么服务的独立交付并不简单?

那为什么不能让每一个服务都独立部署到产品环境呢?问题的答案是:不是不能,而是不敢

为了表达清楚,让我们来看个例子吧。

像下图一样,我现在就是那个程序员帅哥(本色出演),突然有一天心血来潮,动手开发了一个网上商城。代码Push到Github并通过CI构建持续交付流水线,最终自动化部署到云端产品环境,供用户访问使用。

随着用户和访问量的增加,需求和功能也越来越多,系统也变得越发复杂。

从网上了解到最近有个叫微服务的架构非常火爆,我也赶了回时髦,当然也觉得这种架构确实可以帮助我解决现在的一些问题。

经过对系统的分析,我将商城的后台部分拆分出了3个服务,为了简单我们就称之为ABC三个服务。

我们假设一个比较极端的情况,三个服务相互调用(先不考虑这样是否合理),每个服务通过自己的持续交付流水线独立部署到产品环境。当前产品环境的各个服务的版本是:A:1.0、B:2.0、C:3.0

一切都非常完美是不是?看!我们已经做到了服务的独立部署!So easy~

当然,事情肯定不会那么简单。

问题出现在当我对A服务做了一次新的提交之后,A服务的最新版本升级到了1.1。不幸的是,这个新的版本意外的破坏了A与B之间的契约,错误的调用了B的接口,导致出现了错误。

虽然我的A服务和B服务都有比较完备的UT(单元测试),但因为UT无法发现服务之间的集成是否被破坏,所以只有UT作为质量保障的A服务持续交付流水线也自然没有能力发现AB服务集成被破坏的这个问题。最终导致存在问题的A1.1版本被部署到了产品环境,产品环境出现了严重的Bug。

请问在座的同学,碰到这样的情况,你会如何处理?

“加集成测试啊!”

这位同学说的极是,我这么聪明自然也想到了这一点,不就是要测集成吗?UT干不了就加集成测试不就成了。

为了统一语言,毕竟对于各种测试的叫法太容易引起混淆,参考Martin Fowler在《微服务测试策略》中的定义,我们在本文中将这种测试多服务集成的测试统一称作端到端测试(End-to-End tests,简称E2E测试)。

添加了E2E测试之后,我的交付流水线就变成了下面这个样子。

因为有了E2E测试的存在,问题迎刃而解,当A服务的新版本破坏了与B服务的集成时,E2E测试就会及时诊断出来,并阻止A服务的最新版本向产品环境流动,保证产品环境不被破坏。

这样看似没有什么问题,通过添加E2E测试,解决了服务间集成的验证问题,但在不知不觉中,我们也失去了微服务架构的那个重要的特性:“服务的独立交付”。

怎么讲?别急,我们再往下看。

假设A服务的修复过程中,B和C服务也提交了新的代码,我们假设这两个提交是没有问题的,但因为A服务的1.1版本导致E2E测试挂掉的问题还没有被修复,所以B和C的新版本也被E2E测试拦了下来,此时的E2E测试就像是一个亮起红灯的路口,阻塞了所有服务通往产品环境的通道。

所以说,随着集中E2E测试的添加,质量被保障的同时,我们的“微服务架构”也已悄然失去了服务独立交付的能力,杀敌一千自损八百,损失惨重!

这并不是我假想的场景,在我自己经历的几个真实项目中,这个问题都在一直困扰着我们。带来了各种各样的衍生问题,例如E2E测试长时间失败,无人修复,修复难度大,服务交付堵塞,为了保持交付通路畅通还不得不引入同样存在很大副作用的CodeFrezze机制和提交Token机制等。

可以看到,虽然我们能够在代码库,在部署结构上,甚至在组织上进行服务化拆分,但就因为这最后一个交付的十里路口,最后这一个红绿灯,让所有的服务又纠缠在了一起,所有的服务化拆分形同虚设,最终我们得到的也只是一个看起来像微服务架构的单体应用而已

拆除红绿灯,各行其道,收复失地!

那,如何才能将这个“红绿灯”拆除,让服务可以在有质量保障的前提下还可以做到独立交付呢?这就是本文要解决的问题,让我们继续往下看。

我的解决方法其实也很简单:Inline E2E tests

即并不添加新的集中的Pipeline做E2E测试,而是为每一个服务的Pipeline都添加一个相同的E2E测试的Stage,就相当于将E2E测试Inline到每个服务各自的部署流水线中,如下图所示。

其实Inline E2E测试还不是最关键的,最关键的变化点就是假设A服务有了新的提交,运行到A服务自己Pipeline的E2E测试的时候,此时的E2E测试并不是像之前一样获取B和C服务的最新代码库版本做集成验证,而获取当前产品环境上的B和C服务的已部署当前版本做集成验证

例如,如图所示A服务的版本从1.0升级到了1.1,当前产品环境的B和C的版本是2.0和3.0。在执行A服务Pipeline上的E2E测试时,验证出A1.1和B2.0集成存在问题,测试变红,Pipeline挂掉,从而阻断了A服务的1.1版本部署到产品环境,保证了产品环境不会被A的1.1版本破坏。

同样,假设A还没有被修复之前,B也有了新的提交,产生了一个新的版本B2.1,这时在B服务Pipeline上的E2E测试并不获取当前A服务的代码库最新版本1.1做集成测试,而是获取产品环境上的当前版本A1.0版本做集成测试。我们假设B2.1和A1.0之间的集成没有问题,测试通过,所以B的2.1版本就被成功的交付到了产品环境,而此时产品环境的A服务的版本仍是1.0。

看!服务之间的阻塞被神奇的解决了,服务再也不会被堵在一个统一的十字路口,而是各行其道,A的车道出了事故,是A的问题,应该由A来承担后果和解决问题,不应该影响到其他服务,其他服务依然可以持续的交付到产品环境。

向前看是持续集成,向后看是持续交付!

看到这里可能有些小伙伴会感到有些失望。咋呼半天,不就是将E2E测试整到每个服务的Pipeline里,再把获取版本从最新代码改成产品环境么?有啥厉害的。

但是,在我看来,这个看似简单的变化,意义却是重大的:它揭示了“持续集成”和“持续交付”的一个主要区别。

“持续集成”和”持续交付”,这两个概念相信大家一定都不陌生,在软件领域也被提了不少年头了,不算什么新概念新技术。但对于这两个概念,我们经常一起提及,也经常混淆,搞不清楚两者的区别到底是什么,可能认为持续交付只不过是持续集成的演进版,新瓶装旧酒而已。

但其实它们却有着本质的区别。

“持续集成”关注的是各个集成单元之前最新版本的集成问题,即是不是某个集成单元的最新版本破坏了系统整体的集成,我管这种视角叫:向“前”看。

而“持续交付”关注的应该不是集成单元最新版本之间的集成问题,而是某个集成单元的最新版本是否可以(能和敢)部署到产品环境。换句话说就是维持产品环境的其他服务不变,只将当前集成单元的最新版本部署到产品环境,产品是否依然可用,不被破坏。所以在“持续交付”的视角下,应该关注的是当前集成单元与产品环境上的其他服务的版本是否兼容,我管这种视角叫:向“后”看。

向前看是持续集成,向后看才是持续交付,如果前后都不看那就是在裸奔。

但是肯定早有同学在心里疑惑,将E2E测试下放到每一个服务自己的Pipeline中,靠谱么?是不是太重了?根据测试金字塔,E2E测试应该是属于靠近金字塔顶端的测试种类,无论从数量和覆盖范围应该也都不会太多,怎么能靠它来保障服务之间的所有集成点和契约呢?

主角登场-契约测试

细心的同学肯定已经发现上面最后一张图中,我已经悄悄的把E2E测试变为了CT,即Contract Test,契约测试。

契约测试也是这两年伴随微服务架构的兴起,经常被提及的一种比较新的测试类型。在测试金字塔中,他的位置介于E2E和Component Tests(可以理解成单个服务的API测试)之间。

简单的理解,契约测试就是一种可以用类似于单元测试的技术验证两两服务之间集成的测试技术。它相比于更低层次的单元测试的优势是可以测集成(两两服务之间),相比于更高层次的E2E测试的优势是实现方式上又类似于单元测试,更轻量,跑的更快,覆盖的范围也自然可以更广更细。

使用契约测试替换掉E2E测试之后,整个架构也会变得更复杂一些,目前契约测试的框架也有很多,如大家常常提到的Pact或是SpringContracts等等。这里我先以Pact为例予以说明,其他框架实现上可能有些差别,但是思路是一致的。

A服务调用B服务的一个API,我们就称为A和B之间存在了一个契约,即B应该按照这个契约提供一个满足契约要求的API,而A也应该按照这个契约约定的方式来调用B的这个API。在这个过程中A作为调用方,我们称之为Consumer端。B作为被调用方,我们称之为Provider端。

如果A和B都履行契约,按照契约定义的约定调用和被调用,我们就可以认为集成不会有问题。但无论是B擅自修改了API破坏了契约,还是A擅自修改了调用API的方式破坏了契约,都会导致契约被破坏,反应到测试上就是契约测试会失败,反应到产品上就是功能被破坏,出现Bug。

每个契约,例如A->B,都会有Consumer端和Provider端生成的两个产出物:分别是a-b.consumer.json.1.1(由Consumer端生成的契约文件,所以版本也是Consumer端A的版本号)和a-b.provider.jar.2.0(由Provider端生成的契约验证测试包,他由Provider端生成,所以版本是B的版本)。这个jar包其实就是一组测试,他的输入是a-b.consumer.json,产出则是测试的结果,也就是契约的验证结果:成功或是失败。

可以把A服务产出的契约文件a-b.consumer.json.1.1想象成一把钥匙,把B服务产出的Provider端的测试a-b.provider.jar.2.0想象成一把锁。那契约测试的执行过程就像是用这把钥匙试着去打开这把锁:如果可以打开,我们认为这A1.1->B2.0的契约是满足的,反之契约就是被破坏了。

值得注意的一点就是,契约测试不像E2E测试,它是有方向的,所以我们看到a-b和b-a是两个不同的契约。

所以,只有当A1.1->B2.0和B2.0->A1.1双向的契约都被验证通过后,我们才能认为A1.1版本和B2.0版本的集成是没有问题的。

用契约测试替换E2E测试

回到前面的例子上,假设我们已经构建了ABC三个服务两两之间的契约测试。此时,A服务有了新的提交升级到了1.1版本,那我们如何才能通过契约测试来验证A1.1版本能否交付到产品环境呢?

答案就是只要通过A的1.1版本的最新代码,生成所有A作为Consumer端的契约文件(a-b.consumer.json.1.1和a-c.consumer.json.1.1),用这两把“钥匙”去试着开(作为输入执行Provider端测试)产品环境对应的两把“锁”(a-b.provider.jar.2.0和a-c.provider.jar.3.0)。

如果都可以打开(测试通过)的话,就证明A的新版本1.1作为Consumer端与产品环境的B和C服务是兼容的。

等等,别着急,还没完……

因为我们还需要考虑A作为Provider的情况,做法还是通过A的1.1版本的最新代码生成A版本作为Provider端的契约测试(b-a.provider.jar.1.1和c-a.provider.jar.1.1),拿着这两把“新锁”,然后试着用产品环境上的两把“钥匙”(b-a.consumer.json.2.0和c-a.consumer.json3.0)去开。

如果也都可以打开(测试通过)的话,就证明A的新版本1.1作为Provider端与产品环境的B和C服务也是兼容的。

至此,当验证了A的新版本1.1无论是作为调用端还是被调用端都与产品环境上的其他服务契约满足后,我们就认为A1.1与B2.0和C3.0集成是没有问题的,也就代表A1.1可以被放心地部署到产品环境中,替代现在的1.0版本。

这块稍微有些复杂,用文字也很难讲的特别清楚,如果大家对我上边讲的内容感兴趣,但又没有完全理解。请大家移步去看一下我2017年9月份在北京CDConf(持续交付大会)上针对这个主题做的一次分享,讲的应该比写的更清楚一些。在那个分享的最后,也详细介绍了一些我们在这个方案实施过程中碰到的一些问题:例如对于契约变更,并发提交,多环境支持的解决方案,感兴趣的也可以拖到最后看一下。

《契约测试-微服务持续交付的金钥匙》(CDConf 北京 2017)

最后,敲黑板划重点

  • 微服务架构下的独立部署(交付)很重要,但往往容易被忽视,没有被引起足够重视。
  • 为了实现微服务的独立持续交付,我们要向“后”看,不要向“前”看,即关注当前变更服务与部署环境中其他服务的兼容性而不是关注当前变更服务与其他服务最新版本的兼容性。
  • 用契约测试来替代E2E测试,降低测试成本,提高测试覆盖,尽早测试。并通过不断地完善契约管理,保障微服务架构质量和避免微服务架构腐化僵化。

更多精彩洞见,请关注微信公众号:思特沃克

Share

测试矩阵

迷阵

“单元测试,集成测试,端到端测试,安全测试,性能测试,压力测试,契约测试,冒烟测试,验收测试,API测试,UI测试,兼容性测试……”

不知道你是不是像我一样,曾被这些各种各样的“测试”搞得晕头转向。作为一个有追求的开发人员,保证所写的程序、所构建的系统具备良好的质量自然是分内之事。但是面对这些千奇百怪的测试难免会望而却步,只能劝自己一句“专业的事情还是交给专业的人去做吧”,然后把测试的工作一把推给QA,闷头写自己的代码去了。

不光是测试种类众多,每个人对于某一个测试的理解也都不一样。就拿大家最熟悉的“单元测试(unit testing)”来举例,问题的关键就被聚焦到了“到底如何才算是一个单元(unit)?”有人说是一个方法,有的人说是一个类,有的人说都不对,应该是一个最小的业务单元(至少是API级别的)。还有人提出了Integration Unit Test的概念,即集成级别的单元测试。

不光是我等软件小辈,就连很多IT界的神级人物也常常为此争论不休。

古话说的好,一千个人心中有一千种单元测试,看来说的是有道理的。

列表法

(列表法)

这是昨天陪闺女写作业的时候,看到她使用了一种被称作“列表法”的方法去解一个小学2年级的逻辑题。闺女说,这种方法很神奇,原本看起来弯弯绕的问题,画个表勾勾叉叉就解决了。

随后我也查了一下:“列表法是小学数学学科中经常使用的一种方法,使用列表法可以解决许多复杂而有趣的问题。运用列出表格来分析思考、寻找思路、求解问题,经常用来解决类似于鸡兔同笼的经典问题……”

虽然我一直没有搞清楚为啥要把鸡和兔子放到一个笼子里,但回到测试迷阵的问题,好像这种小学3年级就教授的方法也能适用。

测试矩阵

(测试矩阵)

测试的种类繁多,难于理解,难于沟通。我觉得主要是在于我们将两个测试分类的维度混杂在了一起。

其中第一个维度是测试实现的层次或粒度,说白了就是在哪个层次上的测试,也可以理解成测试到底测的是哪儿。是方法?是类?是API?是单个Service?是两两Service?还是应用?还是系统?还是平台?

我们常说的单元测试,API测试,端到端测试,UI测试都是侧重于按照这种维度去分类不同的测试种类的。

但是我们在谈论这些测试的时候,其实隐含了一个概念就是他们测的是什么?也就是测试的目标。例如当我们提到上面的单元测试、API测试、端到端测试的时候其实隐含的想表达的是单元级别的功能测试,API级别的功能测试和端到端级别的功能测试。

这时候你肯定会想,这不废话么,不测功能我测什么?

这就是我想说的第二个测试分类的维度:我们测试的标的物,或是说测试的目标。如果说第一种测试维度是根据“测哪儿”区分的,那第二个维度就是根据“测什么”区分的。

例如,我们常常提到的:功能测试、集成测试、性能测试、安全测试、压力测试、兼容性测试,契约测试都是这种按照这个维度去区分不同的测试种类的,他们都不是关注于我们要测哪儿,而是更侧重于我们到底要测什么:业务功能是否正确?是否能按预期集成?契约是否被保证?安全能否达到要求?性能是否满足预期和要求?

只不过我们日常工作中,大多数情况下测试都是在验证功能是否正确,所以我们常常忽略了第二个维度,只关注于测哪儿。只有当我们去测试像性能和安全这种非功能需求的时候才会想到第二个维度,但有趣的是往往我们这时候又会忽略第一个维度,例如当我们听到有人提及性能测试的时候,并没有明确的表达测的是方法的性能、API的性能,还是UI的性能,进而导致了理解的不一致和混乱。

换个叫法

可见,之前之所以被测试迷阵困扰,其本质原因就是并没有明确区分开这两个维度,甚至将之混为一谈,从而使我们对于“XX测试”的定位和理解包括沟通都变得模糊而不准确。

如果我们不再提“单元测试”、“性能测试”这种含糊不清的概念,而是通过测试矩阵上的二维定位法,改称“方法级别的功能测试”和“API级别的性能测试”,我想我们对于测试的沟通讨论甚至学习实现将明确的多,也简单的多。


更多精彩洞见,请关注微信公众号:思特沃克

Share

从汽车贴膜看专业团队

前几天去给新车贴膜,体验了一把什么叫“专业团队的专业服务”。

听老板说这家店刚开张两个月,但是团队并不是新组建的,而是已经在一起配合了很久。这从后来的整个过程,也看得出来。整个过程我几乎一直站在旁边,虽然被冻得够呛,也被老板怀疑我是在监工,说了好几次让我放心,绝对做到令我满意。但我其实是在观察,或者说是在学习,因为我觉得他们同样作为一只专业服务团队,比我们更敏捷,也更精益。

在制品限制(WIP Limited)

汽车美容这种工作,由于存在场地限制,天然就满足精益中的WIP Limited。像我这次来的这家汽车美容店,只有三个工作台,也就形成了最自然的WIP Limited。就算是有再多的活,再多的车需要贴膜装饰,也只能排在外边,整个团队最多也只能工作在三台车上。

这种天然的WIP Limited存在,也限制了大家并行工作的最大车数。那为了获取更大的利润,也就是为更多的车服务。大家的关注点自然而然的就落在如何以最快的速度完成每一台车的贴膜装饰过程,也就是我们常说的单件流和Lead Time。

自组织全功能团队

(自组织全功能团队)

为了尽量缩短每一辆车从开始装饰到完成交车的整个过程,也就是缩短单个车的Lead Time,我观察到整个团队是在以一种几乎完美的方式协同工作。

首先,所有的工作被高度并行化。例如我的车最多的时候有四个人在同时施工,一个人在缝真皮方向盘套,一个负责贴车左侧窗户的膜,一个负责贴车右侧的膜,一个负责贴前后挡风的膜。

其次,大家并没有清晰的角色划分,缝方向盘套的人在完成了手头的工作后,立刻自觉的加入到贴膜的工作之中;而两侧的膜贴完后,两名工人立刻开始帮车打蜡和做内饰清洁;整个过程自然而连贯,完全自组织,不需要人安排和督促。

所有人都掌握了缝方向盘套、贴膜、打蜡、内饰清洗的工作技能,并没有严格的角色分工,很难说清楚谁是贴膜师,谁是打蜡师,他们每个人都像一个专业的全栈工程师。你也很难说清楚整个过程的流程,是先做贴膜,还是先做内饰清洁,整个过程已经被高度优化过,环环相扣,环环相融,无论是时间还是材料的浪费都被降到了最低。

Leader VS Manager

不用担心,这不是发生了意外,而是在做“新车去异味”项目。而这个一头扎进充满烟雾车厢的人就是这家店的老板。是的,他还是我上面提到的四名“工人”之一,分别完成了缝方向盘套,新车除味和右侧的贴膜工作。

在我的眼里,他就是一个称职的Leader。凡事冲在前面,以身作则,勇于承担一些困难甚至危险的工作。而不是坐在舒服温暖的办公室里指点江山。有了这样的老板,这样的Leader,员工们自然也干的格外起劲。而对于作为客户的我,自然也对这样的团队平添了一份信任和钦佩。

质量内建

关注Lead Time并不代表做的越快越好,更不意味着忽略质量,毕竟残次品也是一种常见的浪费。这不,在我的车几乎贴膜完成的时候,工人在做复检过程中发现左后窗户的贴膜有了一个小气泡。

老板在亲自检查、确认无法修复的前提下,二话不说直接将已经贴好的膜撕掉,重新亲自上阵贴了一个新的给我。整个过程迅速而敏捷,还保持了较高的质量和水准。

总结

一个小时之后,我的车焕然一新。

不得不佩服这样一只专业的团队和那个令人钦佩的老板。他们的技术是那样的全面而专业,整个团队的协作是那样的高效而自制。

而回顾整个过程,让我对于自己的团队有了很多反思,对于精益软件开发中的很多概念也有了更深刻的理解和认同。


更多精彩内容,请关注微信公众号:软件乌托邦

Share

重构之十六字心法

这篇文章是我写过的所有文章里最难产的一篇,前前后后斟酌酝酿了好几个月。因为重构对于我来讲真的太重要也太深刻了,包含的内容和想说的也太多了。如果说这几年自己觉得在哪些方面的收获最大的话,非重构莫属了。

重构的威力

软件开发的难点在于不确定性,前几天邱大师刚写了一篇《软件开发为什么很难》就提到

软件的复杂性来自于大量的不确定性,而这个不确定事实上是无法避免的。

需求在变,语言在变,框架在变,工具在变,架构在变,趋势在变,甚至连组织结构都在不断的变化。

随着变化的不断产生,软件变得越来越复杂。就像《架构腐化之谜》中提到的一样,我们的软件也会像一个生命体,经历从新生到衰老腐化的过程。而重构就像是一次手术,通过优化内部结构,减慢腐化衰老,让软件“青春永驻”,可见重构的威力。

重构教会了我如何通过高效安全地改善内部设计以使之适应外部的不确定性和频繁变化。

重构威力无边,就像是武侠小说中的一件插在石头上的上古神器,但同样也不是一般人可以轻松驾驭的。如果运用不当,造成的损害也会同样巨大。

如何将重构这件神器运用自如,发挥其最大的威力,也是我一直在探寻的,即重构的手法和心法。

合格的重构

在谈手法和心法之前,可能很多人会有疑惑,觉得重构并不像你说的那么难啊,我们每天都在做,就是改改代码改改设计,哪有你说的那么邪乎。那我就先来讲讲我认为怎么样才算是一次合格的重构。

对于什么是重构,《重构》书中已经有明确的定义,分名词和动词两种形式。

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

就像“看板”不是“我们看到的那个白板”一样,“重构”也不是“重新修改代码”那么简单。

我就看到过太多打着重构的幌子,把系统改的面目全非,最后出了问题直接甩锅到重构身上的场景了。那怎样才算是一次合格的重构呢?我觉得至少需要做到以下几点:

  • 消除味道:一个重构应该是从识别一个坏味道(Bad Smell)开始,以消除一个坏味道结束,任何不以消除坏味道为目标的重构都是耍流氓。
  • 始终工作:即重构定义中的“在不改变软件可观察行为的前提下”,说白了就是重构过程不能破坏甚至改变软件外在功能。
  • 持续集成:不需要为重构单建分支,重构过程可以做到Feature开发在同一分支上持续集成持续交付。
  • 随时中止:例如一个方法重命名,需要修改100个调用点,当改到50个的时候有个紧急的Feature,我可以随时暂停重构,立即切换到Feature开发上,且不需要回滚已做的重构。
  • 断点续传:还是上边的例子,假如我已经完成了紧急Feature的开发,可以随时继续之前的重构,完成剩下50个调用点的重命名。
  • 过程可逆:对于重构,经常有人会问:你怎么保证重构就会更好而不是更坏呢?重构的伟大就在于他跳出了对错之争,将关注点放到如何快速平滑安全的变化上,当然也包括反向重构。所以我的回答是:无法保证,但是我可以一分钟就重构回来。如果仔细看,《重构》书里的所有重构手法都是双向的,比如“Extract Method”和“Inline Method”。

可以反思一下,我们平时自认为的那些重构,是否都符合了以上的这些要求?

  • 多少次我们打着重构的旗号,七零八碎,无法复原。
  • 多少次我们打着重构的旗号,分支开发,集成困难。
  • 多少次我们打着重构的旗号,半途而废,迷途难返
  • 多少次我们打着重构的旗号,孤注一掷,进退两难。

在我的眼里,这些都不是合格的重构,甚至都不能称之为重构,好的重构应该像一边开车一边换轮胎一样,保证系统随时可工作的前提下,还可以对其结构做出安全高效的调整。

可见重构并不简单,那要怎样才能达到上述的那些要求呢?

重构的心法

在过去的几年,我一直在学习和思考重构的各种手法。从刚开始的乱改一气,到学习基于IDE和插件的各种快捷键流的重构手法,以及研究如何通过组合各种基础重构手法形成“连招”,从而快速实现更复杂的重构过程。

随着对于基于IDE的快捷键重构手法越来越娴熟,在IDE和插件的帮助下,我的重构手法越来越华丽而迅捷,在沾沾自喜的同时心里也慢慢萌生了一些质疑:难道这就是重构么?如果没有IDE没有了插件,我还会做重构么?如何用编辑器(Vim,Emacs)做重构?重构只是代码级别的么?数据库如何重构呢?系统架构如何重构呢?工具框架如何重构呢?微服务架构下的服务重构呢?公司组织重构呢?

这种感觉就像是武侠小说中的某个柔弱书生,无意中掉到了一个悬崖下,找到了一本武林秘籍,照着上边的招式练了练就自以为已绝学在身,结果出去虽然能招架一时,但禁不住更大的挑战。被打的体无完肤后,重新掏出那本秘籍,收起浮躁,怀着诚敬之心努力去参悟那些招式背后更深的哲理,也就是所谓的心法。此时对于我来说,而那本武林秘籍就叫做《重构》

在带着这些疑问重读《重构》的过程中,我欣喜地发现书中那些细致入微但看似笨拙拖沓的重构手法(例如Rename,使用现代IDE一个快捷键就可以搞定,但是老马用了很多步骤才完成),其实都蕴含着重构最重要最基本的原则和思路,只要按着这些原则去做,无论什么层次的重构:代码重构、架构重构、服务重构甚至是组织重构,都可以做到上面提到的一个合格重构的基本要求,即平滑安全可停可续。

把其中的原则思路抽取出十六个字,即所谓的:重构十六字心法

解释起来也很简单,往往我们做”重构“的时候就是在旧的结构(这里的结构可以是一个方法、一个对象、一个服务、一个数据库、一个服务甚至是一个组织结构)上直接修改,导致系统长时间处于一个中间不可用状态,这个状态持续的时间越长,”重构“失败的可能性和负面影响就会越大。

而《重构》告诉我们,做内部结构调整时,先不要直接修改旧的结构,保持旧的结构不变,先按照新的设计思路创建一个新的结构,因为这个过程中对于旧的内部结构没有任何影响,所以是安全的,可持续集成的。当新的结构构件完成时,我们再把对于旧结构的依赖一个个的切换到新的结构上,即所谓的”一步切换“。最后当确认所有对于旧的结构都切换到新的结构上,而且没有问题后,再将已经没有任何引用的旧结构删除掉,完成整个重构过程。

这里的“一步切换”并不是说整个重构的切换过程必须是一步完成的,例如前面重命名的例子,100个调用点的切换可能是分多次完成的,在这个例子里一步切换指的是每一个调用点的切换过程。这个切换过程是最容易暴露出问题的,所以越简单越快速越好,一旦出现了问题,就快速的切换回旧的结构后再慢慢排查问题,从而实时保证系统的可用性。

大道至简,一旦领悟并掌握了这个心法,就发现自己一下从之前狭义的代码重构中跳脱出来,任何广义上的重构都立刻变得有章可循。

在架构重构中常用的抽象分支(BranchByAbstraction),以及在微服务架构下服务重构常用到的绞杀者模式,其实都是这种原则的一种体现。

总结

重构可以使软件更容易地被修改和被理解。通过不断地改进软件设计以达到简单设计的目标,减少由于设计与业务的不匹配带来的架构与设计腐化。

掌握了重构的手法和心法,会让重构变得更加简单安全高效可控,从而真正的发挥出其巨大的威力,让我们的软件永葆青春。


更多精彩洞见,请关注微信公众号:思特沃克

Share

技术雷达之微服务架构

最近几年,微服务架构异军突起,与容器技术相辅相成,成为架构设计领域热议的话题。而《技术雷达》作为ThoughtWorks出品的一份关于技术趋势的报告,在技术社区也一直有着非常好的口碑。本篇文章就试图结合技术雷达与微服务架构,以往期技术雷达中微服务架构的演变来审视一下这个新兴架构的整个发展过程。

相信大家了解微服务架构或者听说微服务架构也都是近两年的事情,从Google Trends的搜索数据统计上看,微服务架构确实也是从2014年才逐渐兴起,到目前呈现出一个爆发的趋势。

但技术雷达在2012年的3月份就已经提及了微服务架构相关的内容,在当时,其所处的状态还是评估(Assess)阶段,这就说明技术雷达早在2012年初的时候就已经成功捕获到微服务架构这个新的技术架构。

(微服务2012年第一次出现在技术雷达上)

到底什么才是微服务架构,Martin Fowler在那篇著名的描述微服务架构的文章中第一次定义了微服务架构并阐述了其九大特性。而我一开始接触微服务架构的时候也觉得这好像不是一个新的概念,很早之前就有RPC和SOA这种面向服务的分布式架构,又冒出一个新的微服务架构,他们到底有什么区别?

看到Martin Fowler的定义以后,才慢慢清楚他们的区别,在Martin Fowler的定义中有几个关键字可以让我们甄别一个分布式架构是传统的面向服务架构还是新的微服务架构:每个服务是不是跑在独立的进程中?是不是采用轻量级的通讯机制?是不是可以做到独立的部署?

(微服务架构的定义)

时间来到了2012年的10月份,在这期的技术雷达中,微服务架构已经从评估(Assess)阶段被移到实验(Trial)阶段。什么叫实验阶段?ThoughtWorks内部有一个解释,就是这项技术已经可以运用在实际项目中,但你仍要控制风险,也就是说此项技术已经可以在风险比较低的项目中使用了。

一个项目要能被移到试验的阶段,还有一个必须要满足的条件,就是必须在ThoughtWorks自己的项目中已经开始实际使用。幸运的是,我当时所在的项目也是在2012年10月份左右开始采用微服务架构的,结果也是非常好的。我们在3个月完成一个新的应用并成功上线,当时客户评价很高。

实际体验下来,微服务架构对我们究竟有哪些好处?这几点是我体会到的:

首先是组件化,作为一个软件开发人员,我们一直都有一个梦想,就是希望有朝一日可以把一堆组件像乐高一样通过直接拼装的方式快速构建我们的应用。无论是最早基于拖拽的方式构建应用,还是现在大热的前端组件化,我们一直都在试图寻找一种更好的组件化方式,微服务架构也是其中之一。但构建软件本身仍是一个非常复杂的过程,微服务架构为我们提供了一种组件化的可能,但直到现在还不好说它能不能达到我们作为整体组件化的目标,但是至少从实际体验来看,它确实能给我们带来组件化的很多好处。

然后是弹性架构,在2015年11月期技术雷达中推荐了亚马逊的弹性计算平台,如果我们的系统是由按业务划分的服务构成,结合容器技术和云平台我们就可以构建一个极具弹性的架构。通过云平台实时的监控,一旦发现资源紧张,立刻就可以通过云平台和容器技术自动瞬间扩展出成百上千的服务资源,在高峰过去之后又可以立即把所有的服务注销掉,释放资源,整个过程完全是自动化的。

去中心化和快速响应也是微服务架构给我们带来的好处。在单体架构下,会非常依赖于项目一开始时对于技术选择,一旦选择了一个技术栈,那么之后几年都被绑定在了这样一个技术栈下,很难应对变化。微服务架构则给我们提供了一个更细粒度使用技术的可能,在不同的服务里可以使用完全不同的技术栈,不同的语言、框架甚至数据库,真正做到用最适合的技术解决最适合的问题,从而让我们可以更加敏捷地响应需求和市场的变化,增加了竞争力。

(微服务架构的好处)

从2012年10月份一直到2014年的7月份,在这个时间段有大量与微服务架构相关的工具、技术和框架出现在技术雷达上。包含了很多领域:语言、测试,框架、CI、CD、持续交付,安全等等。

从2012年的3月份微服务架构第一次出现在技术雷达上一直到2014年7月份,虽然微服务架构已经有了比较大的发展,技术雷达上也已经推荐了大量相关的内容,但在当时社区中谈论微服务架构的声音并不多,这也体现出了技术雷达的前瞻性。

(技术雷达上微服务架构相关项目)

从2014年7月份开始微服务在社区就开始呈现出一种爆发的趋势,但在紧接着的2015年1月刊的技术雷达中却出现一个非常有意思的项目:Microservice Envy。通俗点儿讲就是“微服务红眼病”,或者说是“微服务你有我也要”。

这意味着在社区刚刚爆发,对于微服务架构踩下油门的时候,我们已经踩下了一脚刹车。但这并不是代表我们不看好微服务架构,而是认为需要认真思考我们是否真正需要以及何时以何种方式使用微服务架构,不能看别的人都在使用也盲目切换到微服务架构下。

这是因为微服务架构并不是免费的午餐,使用微服务架构是需要门槛和成本的。我们需要问自己:用微服务我们够“个”吗?或是说用微服务我们够“格”么?我们是否有这个能力和足够的资源驾驭这个新的架构?

Martin Fowler在他的《企业应用架构模式》中,就提到了分布式对象设计的第一原则:“设计分布式对象的第一个原则就是不要使用分布式对象”。因为分布式系统会给我们带来很大的挑战,让系统复杂度大幅增加的同时,我们还需要面对开发环境、测试、部署、运维、监控,一致性和事务等一系列的问题。

(Microservice Envy)

所以说,微服务架构虽然看起来非常美好,但是也是有很大附加成本的。通过下面这张图可以看到,横轴是时间轴,纵轴是生产力。当软件的复杂度很低的时候,单体架构下的生产力是要高于微服务架构的,但随着复杂度的不断增加,无论是单体应用还是微服务应用的生产力都会下降,只是微服务架构的下降会相对缓慢一些。

这也容易理解,因为在微服务架构中,我们的系统是由很多的小的服务组成,每一个服务都很小,相对简单,技术栈也很独立。这样做局部的变更也会更加容易,随着系统复杂度的不断增加,微服务的优势也就慢慢地体现出来了。

那要如何应对呢?为了追求生产力的最大化,一开始我们可以选择从一个单体架构开始,然后争取在微服务架构生产力超越单体架构的那个复杂度点切换到微服务架构上来,这样才能实现生产力的最大化。这就是Martin Fowler提出的单体应用优先原则(MonolithFirst),以单体架构开始,通过演进式设计逐渐重构到微服务架构。

(MonolithFirst)

为了保证从单体架构演进到微服务架构的重构过程安全可控,还需要有一套良好的质量守护机制。下图描述的就是Martin Fowler提出的微服务架构下的测试策略,我所在项目就是按照这种方式来划分和设计我们各种不同类型的测试,帮助我们在对于服务的抽取合并分离的重构过程中做到安全可控。

(Testing Strategies in a Microservice Architecture)

我们刚才提到了康威定律,康威定律说的是设计系统的组织产生的设计和架构等价于组织间的沟通结构。而康威定律还有一个逆定律:如果想改变一个设计架构方式,首先要改变组织结构。我们经常发现推动技术架构的转型和演进很难,因为我们在调整技术架构的同时却忽略了组织结构也要对应做相应的调整以匹配技术架构的变化,当组织结构与技术架构不匹配的时候,就会相互拉扯,这些都是在当时的技术雷达中着重强调的。

截至目前,以上内容都还是在谈论2015年以前各期技术雷达里的内容。在这之后直到现在,技术雷达也还在持续地推荐微服务架构相关的内容。所以说踩下刹车并不是因为我们走错了路,只是走的太快了,需要时刻提醒自己不要盲目,要清楚微服务给我们带来了什么和有着什么样的挑战,最终解决我们的问题。

来到最新的几期技术雷达,微服务架构还在不断的演进,而且慢慢的与其他新兴的技术融合形成一整套的新的不同以往的构建软件的解决方案。例如无服务器架构、对Docker的应用、对PaaS各种云的应用等,这些技术的发展,会不会对微服务架构的演进提供更多可能?是否可以为微服务架构早一天落地、改变我们的开发方式提供可能?让我们大家一起拭目以待。


更多精彩洞见,请关注微信公众号:思特沃克

Share