「第5章」- 持续交付的软件系统架构

  CREATED BY JENKINSBOT

在2000年,著名的电商网站亚马逊仍旧是传统的「巨石应用」,而不是今天大家看到的「微服务架构」。这种「巨石应用」每次部署时必须将整个网站作为一个整体统一进行部署。在大型促销活动期间,网站的稳定性遇到了严峻挑战。尽管团队在活动之前做了预估扩容,但活动期间的流量还是远远超出了团队的预期。生产事件频发,常常修复一处问题却引发另一处出现问题。

公司管理层对这种现象进行复盘,并认为,最主要的原因是:系统耦合度太高,比较复杂。但是,由于业务需求太多,时间比较紧迫,工程师们忙于开发自己手上的功能特性,没有时间进行沟通,了解系统的整体架构。为了解决这一问题,工程师们应该在开发需求之前进行更加充分的讨论。而公司CEO贝索斯却认为,不应当再增加沟通,而是应该减少沟通,即增加小团队内部的沟通,减少团队之间的沟通。为了做到这一点,应该将网站的「巨石架构」全面改造为「面向服务架构」(Service-Oriented Architecture,SOA),并提出以下要求(参见《程序员的呐喊》一书):

	(1)所有的团队都要以服务接口的方式,提供数据和各种功能。
	(2)团队之间必须通过接口来通信。
	(3)不允许任何其他形式的互操作:不允许直接读取其他团队的数据,不允许共享内存,不允许任何形式的后门。唯一许可的通信方式,就是通过网络调用服务。(!不要共享服务组件,如DB、MQ等等!)
	(4)具体的实现技术不做规定,HTTP,Corba,Pub/Sub、自定义协议皆可。
	(5)所有的服务接口,必须从一开始就以可以公开为设计导向,没有例外。这就是说,在设计接口的时候,就默认这个接口可以对外部人员开放,没有讨价还价的余地。
	(6)如果不遵守上面规定,就会被解雇。

截至2011年,其生产环境的部署频率已经非常高了。工作日的部署频率达到了平均每11.6秒一次,一小时内最高部署次数达到了1079次(参见Jon Jenkinst 在O’Reilly Velocity Conference 2011上发表的「Velocity Culture」)。

这归功于将「巨石应用」改造为「面向服务架」(SOA)。由此可见,为了能够更好地应对业务发展,「持续交付」是必然趋势,在「软件系统构」方面的“大系统小做”原则是促进这一目标达成的必要条件。

5.1 “大系统小做”原则

5.1.1 持续交付架构要求

为了提升交付速度,获得持续交付能力,系统架构在设计时应该考虑如下因素:

	**¶ 为测试而设计(design for test)**
	如果我们每次写好代码以后,需要花费很大的精力,做很多的准备工作才能对它进行测试的话,那么从写好代码到完成质量验证就需要很长周期,当然无法快速发布。
	**¶ 为部署而设计(design for deployment)**
	如果我们开发完新功能,当部署发布时,需要花费很长时间准备,甚至需要停机才能部署,当然就无法快速发布。
	**¶ 为监控而设计(design for monitor)**
	如果我们的功能上线以后,无法对其进行监控,出了问题只能通过用户反馈才发现。那么,持续交付的收益就会大幅降低了。
	**¶ 为扩展而设计(design for scale)**
	这里的「扩展性」指两个方面,一是:支持团队成员规模的扩展;二是:支持系统自身的扩展。
	**¶ 为失效而设计(design for failure)**
	俗语说:“常在河边走,哪能不湿鞋。”快速地部署发布总会遇到问题。因此,在开发软件功能之前,就应该考虑的一个问题是:一旦部署或发布失败,如何优雅且快速地处理。(!MAVEN!)

5.1.2 系统拆分原则

“大系统小做”的方法由来已久,并不是一个新概念。1971年, David Parnas发表了一篇题为「On the Criteria To Be Used in Decomposing Systems into Modules」的论文,讨论了将「模块化」作为「提高系统灵活性和可理解性」,同时「缩短开发时间」的一种机制。

那么,对今天的系统架构来说,“大系统小做”要遵循哪些原则呢?

大系统应该由很多「组件」(service)组成。它们通常会以jar/war/dll/gem等形式出现,其粒度要比一个类(class)大,但是要比整个系统小很多。「组件」通常在「编译构建」或者「部署」时被集成在一起,而「服务」可以由多个「组件」构成,能够独立启动运行,并在运行时与整个系统进行通信,成为整个系统的一个组成部分。根据论文,结合目前软件的发展趋势,以及持续交付的要求,对系统进行拆分有以下几个原则:

	(1)作为系统的一部分,每个组件或服务有清晰的业务职责,可以被独立修改,甚至被另一种实现方案所替代。
	(2)“高内聚、低耦合”,使整个系统易于维护,每个组件或服务只知道尽可能少的信息,完成相对独立的单一功能。
	(3)整个系统易于构建与测试。将系统拆分后,这些组件仍需要组合在一起,为用户提供服务。因此,如果构建和测试困难,就很难缩短开发周期,无法达到“持续交付”这一目标。
	(4)使团队成员之间的沟通协作更加顺畅。

当然,这种拆分也带来了新的问题。例如:

	(1)对由多个服务组成的系统来说,一个请求可能要经过很多次不同服务之间的相互调用才能完成调用链路过长;(!PINPOINT!)
	(2)当有成百上千的服务时,没有服务发现机制是不可想象的;
	(3)如果代码中调用了他人的服务,则查找问题的难度要高很多,除非有统一的方式在沙箱里运行所有服务,否则几乎不可能进行任何调试。

因此,在系统拆分的同时,我们必须同时建立相应的构建、测试与部署和监测机制,而且,这些机制的建立与系统拆分工作同等重要。只有这样,才能既获得系统拆分的益处,又能管理因拆分带来的复杂性。例如,谷歌公司的C++代码统一放在同一个代码仓库中,有很多个组件,且这些组件之间有很多依赖关系。因此,公司内部开发了一个强大的编译构建平台(名为Bazel,现已开源),用于这些组件的构建。

5.2 常见架构模式

关于与软件架构相关的论著已经有很多了,书中仅讨论了3种架构模式:

	* 「微核架构」- 常用于需要向用户分发的客户端软件;
	* 「微服务架构」- 用于企业自身可控的后台服务端软件;
	* 「巨石应用」- 常见于创业公司的产品项目。

5.2.1 微核架构

微核架构(microcore architectureplugin architecture)指的是软件的核心框架相对较小,而其主要业务功能和业务逻辑都通过插件实现,如图所示。核心框架部分通常只包含系统启动运行的基础功能,例如基础通信模块、基本渲染功能、界面整体框架等。插件则是互相独立的,插件之间的通信只通过核心框架进行,避免出现互相依赖的问题。

这种架构方式的优点有以下几个:

	良好的功能延伸性:需要什么功能,开发一个插件即可。
	易发布:插件可以独立地加载和卸载,使它比较容易发布。
	易测试:功能之间是隔离的,可以对插件进行隔离测试。
	可定制性高:适应不同的开发需要。
	可以渐进式地开发:逐步增加功能。

当然,它也有不足,具体有以下几点:

	扩展性差:内核通常是一个独立单元,不容易做成分布式,但对客户端软件来说,这就不是一个严重问题。
	开发难度相对较高:因为涉及插件与内核的通信,以及内部的插件登记机制等,比较复杂。
	高度依赖框架:既享受框架带来的方便性,但是当框架接口升级时,也可能会影响所有插件,导致大量的改造工作。

基于这些特点,我们将其用于对客户端软件的架构改造中,给团队的持续发布带来了巨大的收益。《持续交付 2.0》第14章介绍的案例进行了这种微核架构的改造,以便更容易在保障软件质量的情况下,同时提升PC客户端应用的发布频率。

5.2.2 微服务架构

「微服务架构」(Microservice Architecture)是一种架构模式,它提倡将「单一应用程序」划分成「一组小的服务」,「服务」之间互相协调、互相配合,为用户提供最终价值。每个「服务」运行在其独立的进程中,「服务」与「服务」间采用轻量级的通信机制互相沟通(通常是基于HTTP协议的RESTful API)。每个「服务」都围绕着「具体业务」进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建。

这种软件架构的优点有以下几个:

	(1)扩展性好 - 各个服务之间低耦合。可以对其中的个别服务单独扩容,如图所示的D服务:
	{{./pasted_image.png?width=300}}
	(2)易部署 - 每个服务都是可部署单元;
	(3)易开发 - 每个组件都可以进行单独开发单独部署,不间断地升级。
	(4)易于单独测试 - 如果修改只涉及单一服务那么只测试该服务即可。

但是,它也有不足,具体有以下几点:

	(1)由于强调互相独立和低耦合,服务可能会被拆分得很细。这导致系统依赖大量的微服务,变得很凌乱和笨重,网络通信消耗也会比较大。
	(2)一次外部请求会涉及内部多个服务之间的通信,使得问题的调试与诊断比较困难,需要更强大的工具支持。
	(3)为原子操作带来困难,例如需要事务类操作的场景。
	(4)跨服务的组合业务场景的测试比较困难,通常需要同时部署和启动多个微服务。
	(5)公共类库的升级管理比较难。在使用有一些公共的工具性质的类库时,需要在构建每个微服务时都将其打包到部署包中。

正是因为这些困难之处,所以在使用微服务架构模式时,除确保每个服务一定要能够独立部署之外,还要确保在部署升级时不影响其下游服务(例如通过支持API的多版本兼容方式),同时建立全面的微服务「监测体系」。

5.2.3 巨石应用

「巨石应用」(monolithic application)也称「巨石架构」,是指由单一结构体组成的软件应用,其用户接口和数据访问代码都绑定在同一语言平台的同一应用程序。一个巨石应用是一个自我完整的系统,独立于其他应用程序。其设计理念就是自己从头到尾完成某项功能所需的所有步骤,而不只是实现其中某个环节。这种巨石架构应用通常表现为一个完整的包,如一个Jar包或者一个Node.js的完整目录结构。只要有了这个包,就什么都有了。

组织良好的「巨石架构」同样也有其优势,包括以下几个:

	(1)利于开发和调试:当前所有开发工具和IDE都很好地支持了巨石应用程序的开发系统架构简单,调试方便;
	(2)部署操作本身比较简单:例如,只需要有运行时所需部署的一个WAR文件(或目录层次结构)即可;
	(3)很容易扩展:只要在负载均衡器后面运行这个应用的多个副本就可以扩展应用程序;

它的劣势有以下几个:

	(1)对整体程序不熟悉的人来说,容易产生混乱的代码,污染整个应用,给老代码的学习和理解带来困难。
	(2)难与新技术共同使用。
	(3)只能将整个应用作为一个整体进行扩展(如图所示):
	{{./pasted_image001.png?width=300}}
	(4)持续部署非常困难。为了更新一个组件,必须重新部署整个应用程序。

对创业公司或者中小型项目来说,巨石应用可以快速迭代,不需要太多资源。而且对人员技术要求不高,常常单一技术栈就能搞定,人力资源容易获取。

巨石应用也能做到持续交付,但是需要经过良好的设计。例如,2011年时Facebook公司每天部署一次,而这个部署包约1GB的大小。这么大的二进制包的编译时间也仅需要二十几分钟,将其全部分发到近万台机器上,也只需要不到两分钟的时间。

# 总结

事实上,无论什么样的架构,只要没有针对代码的整个生命周期(开发、测试和部署)进行良好设计,对“快速交付”来说,就会存在困难。

	例如,国内某互联网公司开发了一个微服务框架,可以快速开发出一个微服务,而且也容易部署。然而,在践行持续交付过程中,团队才发现,这个微服务框架在多人并行开发多个服务的升级版本时,开发的调试过程和测试环节都遇到了很大困难。原来,这个微服务框架只允许一个主控服务存在,而且只能通过服务名进行服务注册与发现,**并不支持同一服务的多个版本同时存在。**当两个开发人员同时开发各自的服务模块A和B时,由于服务A的新版本还没有开发完成,因此服务B需要使用服务A的旧版本进行联调。但是,如果将服务A的两个版本同时部署到开发调试环境中,如图所示,服务就会出现混乱。那么,如果我们同时准备两个调试环境,分别部署两个主控服务,似乎是可行的,如图5-b所示。但这只是两个开发人员的并行开发场景,如果是多人并行开发调试,则所需要的资源会更大。
	{{./pasted_image002.png}}
	当然,解决这个问题还有其他办法。例如,某互联网公司的后台微服务数量众多,很难为每一个开发人员建立单独的一套测试环境。因此,他们开发了一个路由机制,在同一套测试用的标准微服务环境下,开发人员可以单独部署自己正在修改的微服务,并通过路由机制,与标准微服务环境中的其他服务形成一个虚拟的微服务环境,用于自己调试,如图所示:
	{{./pasted_image003.png}}

5.3 架构改造实施模式

对部署频率较低的「遗留系统」来说,很少会仔细考虑易测试、易部署、易扩展这3个因素。为了保持业务的敏捷性,软件架构也需要保持敏捷性。这里的“敏捷”是指具有快速且轻松做出改变的能力。因此,我们总会遇到架构改造的需求。通常,这类改造有3种实施模式,分别是「拆迁者模式」、「绞杀者模式」、「修缮者模式」。其中,「绞杀者模式」和「修缮者模式」都有利于持续交付,降低架构改造和发布的风险。

5.3.1 拆迁者模式

“拆迁者模式”就是指根据当前的业务需求,对软件架构重新设计,并组织单独的团队,重新开发一个全新的版本,一次性完全替代原有的遗留系统,如图所示:

这种方式的好处在于,它与旧版本没有瓜葛,没有历史包袱,可以按预期进行架构设计。

但是,这种模式的风险包括以下几个方面:

	* 业务需求遗漏。软件的历史版本中,有很多不为人熟知的功能还在使用。
	* 市场环境变化。由于新版本架构无法一蹴而就,当市场需求发生变化时,就会错失市场良机。
	* 人力资源消耗大。必须分出人力,一边维护旧版本的功能或紧急需求,一边要安排充分人力进行架构改造。
	* “闭门造车”。新版本上线后,无法满足业务需求。

当然,并不是说这种模式不可实施:

	惠普激光打印机的固件架构改造项目就是一个架构重写的成功案例(资料来源:Gary Gruver 等的《大规模敏捷开发实践:HPLaserJet产品线敏捷转型的成功经验》一书)。2008年,该团队已经筋疲力尽,整个团队只有5%的人力能够用于开发新特性。。。经过3年的努力,到2011年做新特性开发的资源提升了8倍,全部研发成本下降了40%。其软件架构变成了“微核”架构模式,即每台打印机都安装有一个最小的固件初始版本。当打印机联网以后,该固件可以根据实际业务需要,从网络下载必要的功能模块,并自动部署安装。
	在架构重写过程中,该团队同时还改变了原有的分支模式。从“分支地狱”转变为“主干开发模式”(参见第8章),并建立了自己的持续交付部署流水线。

当然,这么做也有失败的案例:

	网景通信公司(Netscape Communications Corporation是一家美国计算机服务公司,曾以其生产的同名网页浏览器 Netscape Navigator而闻名。由于其老旧的软件架构使得用户体验越来越差,并且很难快速应对互联网浏览器的发展,于是公司高层决定使用「拆迁者模式」对软件架构进行改造在此期间,微软公司凭借IE浏览器与Windows的成功,一跃成为浏览器市场的第一,而网景公司从此一蹶不振。

5.3.2 绞杀者模式

“绞杀者模式”是指保持原来的遗留系统不变,当需要开发新的功能时,重新开发一个服务,实现新的功能。通过不断构建新的服务,逐步使遗留系统失效,并最终替代它,如图5-7所示:

!它具有以下的特点:

	* 保持原来的遗留系统不变;
	* 在遗留系统之外,构建新服务,并使用新服务;
	* 逐渐替代遗留系统中的功能;

!适用范围:

	* 对于无法修改的遗留系统,推荐采用绞杀者模式:在遗留系统外面增加新的功能做成微服务方式,而不是直接修改原有系统,逐步的实现对老系统替换;^{[1]}

这种方式的好处在于:

	* 不会遗漏原有需求;
	* 可以稳定地提供价值,频繁地交付版本可以让你更好地监控其改造进展;
	* 避免“闭门造车”现象;

其劣势在于:

	* 架构改造的时间跨度会变大;
	* 产生一定的迭代成本;

5.3.3 修缮者模式

「修缮者模式」是指将遗留系统的部分功能与其余部分隔离,以新的架构进行单独改善。在改善的同时,需要保证与其他部分仍能协同工作,如图所示,上面的步骤与我们在第12章所讲的抽象分支技术一样。这种方式与绞杀者模式类似,但改造只发生在同一个系统内部,而非遗留系统外部。

!!!它具有以下的特点:

	* 在一个系统的内部进行;
	* 遗留部分与某个部分隔离;
	* 以新的架构进行单独改善(!!!加入中间层,然后替换旧系统)

其收益包括:

	* 系统外部无感知;
	* 不会遗漏原有需求;
	* 可以随时停下改造工作,响应高优先级的业务需求;
	* 避免“闭门造车”现象。

而其劣势在于:

	* 架构改造的时间跨度会变大;
	* 会有更多额外的架构改造迭代成本;

案例,使用「修缮者模式」将「巨石应用」向微服务架构演进时,最后应将分离出来的部分独立成一个新的服务,如图所示。要将接缝处的代码X一分为二,其中属于原有应用职责的X应留在原有的巨石应用中,而与分离后的微服务紧密相关的X2应与微服务结合在一起。重复这个步骤,直至完成所有微服务的分离

我们过去所做的拆分中多为「修缮者模式」,其基本原理来自Martin Fowler的「branch by abstraction」的重构方法。[2]

5.3.4 数据库的拆分方法

一般来说,关系型数据库很可能是巨石应用中的最大耦合点。因此,对于有状态微服务的改造,我们需要非常小心地处理数据库数据。

做数据库拆分时,我们应该遵循以下步骤,如图所示:

	(1)详细了解数据库结构,包括外键约束、共享的可变数据、事务性边界等,如图a所示。
	(2)先拆分数据库,并按照「12.3.2」节的介绍进行数据迁移,如图b所示。
	(3)数据库双写无误后,找到程序架构中的缝隙,如图c所示。
	(4)将拆分出来的程序模块和数据库组合在一起,形成微服务,如图d所示。

# 应该围绕业务目标进行架构改造 #

对巨石应用进行拆分时,可以先拆分成颗粒度相对较大的服务。当拆分完成后,如果达到拆分的目标(如已支持更快的发布频率),那么就可以停下来了,不应该为了架构而架构,为了技术而技术。同时,还需要注意的是,在拆分成微服务架构时,你必须考虑要建立相应的基础设施,例如服务治理、服务监控、自动化测试与自动化部署等工具。

5.4 总结

本章讨论了“持续交付2.0能力”对软件系统架构的要求,在软件开发设计时就考虑可测试性、易部署性、易监测性、易扩展性,以及对可能失败的处理,并且讨论了系统架构拆分原则。

我们也对常见的3种软件架构模式及其适用的不同场景进行了分析与比较。例如,微核架构模式适合于客户端软件;微服务架构模式适合于大型后台服务端系统;巨石应用则适合于创业公司或中小型项目。

最后,我们讨论了对遗留系统进行架构改造的3种方式:

	(1)拆迁者模式,就是一次性重写所有代码。这是大家最熟悉的方式。
	(2)绞杀者模式,就是不改变或少改变原有遗留系统,通过增加新的服务来不断替代遗留系统的功能。
	(3)修缮者模式,就是通过迭代,对原有遗留系统进行逐步改造,同时开发新的功能。

同时,也介绍了如何解决「绞杀者模式」和「修缮者模式」中可能遇到数据库表及数据的拆分和迁移问题。

为了能够持续交付,并且降低架构改造的风险,建议团队根据实际情况,采用「绞杀者模式」或「修缮者模式」进行遗留系统的架构改造。

参考文献

从单体架构迁移到微服务,8 个关键的思考、实践和经验
服务拆分与架构演进