微服务实战

978-7-115-52987-9
作者: [英]摩根·布鲁斯(Morgan Bruce)保罗·A.佩雷拉(Paulo A. Pereira)
译者: 李哲
编辑: 吴晋瑜

图书目录:

详情

本书主要介绍如何开发和维护基于微服务的应用。本书源自作者从日常开发中得到的积累和感悟,其中给出的案例覆盖从微服务设计到部署的各个阶段,能够带给你真实的沉浸式体验。通过阅读本书,你不仅能够了解用微服务搭建高效的持续交付流水线的方法,还能够运用Kubernetes、Docker 以及Google Container Engine 进一步探索书中的示例。

图书摘要

版权信息

书名:微服务实战

ISBN:978-7-115-52987-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [英]摩根·布鲁斯(Morgan Bruce)

     [英]保罗·A.佩雷拉(Paulo A. Pereira)

译    李 哲

责任编辑 吴晋瑜

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Original English language edition, entitled Microservices in Action by Morgan Bruce and Paulo A. Pereira published by Manning Publications , USA. Copyright ©2018 by Manning Publications.

Simplified Chinese-language edition copyright ©2019 by Posts & Telecom Press Co., LTD. All rights reserved.

本书中文简体字版由Manning Publications授权人民邮电出版社有限公司独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

版权所有,侵权必究。


本书主要介绍与微服务应用开发和部署相关的内容,并辅以实际示例来引导读者体验从设计到部署微服务的全过程。

全书共13章,分为4部分。第一部分介绍微服务的设计和运行,并把微服务方案运用到一个示例(SimpleBank)中;第二部分先介绍微服务应用的架构,然后通过为SimpleBank设计新功能来讲述如何决定微服务的职责范围,还介绍了微服务的事务与查询、高可靠服务的设计以及可复用微服务框架的构建等内容;第三部分展示了微服务部署的一些最佳实践,包括基于容器和调度器的部署、构建微服务交付流水线等;第四部分着重探讨微服务的可观测性以及微服务开发中“人”的因素。

通过学习本书的内容,读者将了解如何进行微服务应用的开发和部署、如何通过微服务来实现有效的持续交付,以及如何用Kubernetes、Docker和Google Container Engine开发实例。

本书适合了解企业级应用架构和云平台(如AWS和GCP)的中级开发人员和架构师阅读,也适合对微服务感兴趣的读者参考。


摩根 · 布鲁斯(Morgan Bruce)有着丰富的复杂应用开发经验,具备金融、身份验证(非常重视精度、可恢复性和安全性的行业)等行业的专业知识和技术。作为一名工程师主管,他负责过大规模的代码和架构重构工作。他还亲身经历和推动了从单体应用到健壮的微服务架构的演进过程。

保罗 · A. 佩雷拉(Paulo A. Pereira)正带领团队实施从单体应用到微服务的迁移。单体应用系统对安全和精确性的要求非常高,这已经成为影响其发展的拦路虎。保罗所带领的团队正在处理这方面的问题。他热衷于为工作选择合适的工具以及将不同的语言与范式进行组合。他现在正主要通过Elixir研究函数式编程。保罗编写了Elixir Cookbook这本书,还是Learning Elixir一书的技术审校人。


自马丁·福勒(Martin Fowler)在2014年发表了以“微服务”为主题的文章后,微服务随之变得炙手可热。短短数年间,“微服务”已经成为一个大家所熟知的技术名词,受到无数人的追捧。在各大技术会议上,各大互联网企业也都纷纷分享自己在微服务实践方面的经验和总结,甚至于出现了跟风和言必称“微服务”“微服务嫉妒”(Microservice envy)的情况,仿佛设计架构上不是“微服务”风格的话,就在技术上有不足似的。人们的认识也不断变化,“Monolith First”“Microservice First”以及“Micro Frontends”都是人们认识不断深入的体现,乃至于偶尔媒体上出现哪家公司放弃了“微服务”的新闻报道或者“微服务之死”的讨论,都会成为热点。

最近看到这样的一个段子。

Q:大师,大师,微服务拆多了怎么办?

A:那就再合起来啊。

Q:那太没面子了啊。

A:你就说你已经跨域了微服务初级阶段,在做中台了。

其实“微服务”从来就不是“银弹”,也不可能成为“银弹”。通过将现有的单体应用或者业务领域进行拆分,分而治之来降低系统的复杂性和维护难度,这是一种很普遍的理念。随着单体应用越来越臃肿,业务和需求越来越复杂,微服务的出现也是行业发展到一定阶段的必然产物,每个微服务负责一块业务或技术能力,独立部署,独立维护和扩展,甚至于在某些情况下随着业务的变化,将某些微服务再进行合并,也并不是不可以的。

回到“微服务”和“中台”这两个概念,上面的段子漏洞百出,不值得讨论。微服务体现去中心化、天然分布式,与阿里的中台战略思想类似,是战略的具体实现方式之一,是连接业务架构和中台的一座桥梁;中台目前还更多停留在初级阶段,但微服务架构已经有了较为成熟的理论和方法论,能极大推动和提高中台战略的落地成功率。由此可见,现实并非上面段子那样简单粗暴的解释。

面对快速发展变化的IT行业,各种和微服务相关的技术和框架不断涌现,架构师和开发人员需要具备透过这些表象看清事物本源的能力。微服务架构不仅涉及架构设计和开发阶段,还包含了测试、部署以及运维阶段,是一个完整的生命周期。以往的许多图书要么过于理论化,要么过于偏向技术应用层面的实践,要么仅侧重于某一个微服务的某一个阶段。本书理论与实践相结合,不仅有理论介绍,也介绍了很多微服务架构生命周期中各个阶段的优秀设计模式和最佳实践,是非常难得的学习用书。

在我看来,微服务不会消亡,随着各种技术、框架和工具的丰富和强大,尤其是service mesh 之类的技术的演进,未来也许微服务架构的许多内容会像空气那样无处不在但大家又不会感知到它的存在。到了那时,大家开发过程中可能不会意识到微服务的存在,但是微服务已经是架构血液的一部分。作为有追求的开发人员和架构师,很有必要了解微服务的点点滴滴。

早在本书英文版出版之前,我就联系杨海玲编辑询问翻译事宜,期望能参与到中文版的翻译工作中。非常感谢杨海玲编辑的认可,最终承担了该书的翻译工作。本来计划能在短期内完成翻译稿件,但由于翻译期间个人家庭和工作原因,导致交稿时间一直拖延,影响了中文版图书的出版进度,非常感谢吴晋瑜编辑在整个过程中的理解和支持,希望以后能有更好的合作。另外,还要感谢公司的其他编辑们,他们为保证本书的质量做出了大量的编辑和校正工作,在此深表谢意。

读者在阅读过程中,发现有任何问题、错误或不妥之处,请随时联系我(lizhe2004@163.com)或出版社,我们将及时改正。也非常欢迎大家对本书提出宝贵的意见和建议。

李 哲


在过去5年中,微服务架构风格(通过一系列细粒度的、松耦合的、可以独立部署的服务来组织应用)变得越来越流行。且不论公司规模多大,单就工程团队来说,微服务也变得越来越可行。

对我们来说,在Onfido公司使用微服务进行项目开发的经历让我们大开眼界。我们也把自己这一路上学到的很多东西记录到了本书中。通过拆分产品,我们让产品的交付速度变得更快、冲突更少,不再被臃肿的单个代码库里其他人的代码所影响。微服务方案可以让工程师构建的应用能够随着时间持续演进——即使产品复杂度和团队规模都在不断增长,应用也可以持续演化。

最初,我们打算写一本关于我们在项目中运行微服务应用的工作经验的书,但在确定这本书的具体内容时,我们的目标发生了变化。我们决定把微服务的整个应用生命周期(微服务设计、部署和运维)的工作经验提炼成一份内容更广泛且具有实用性的总结。我们还选择了一些工具来对这些技术(如Kubernetes 和 Docker)进行解释说明——它们都是非常流行的技术,并且和微服务的最佳实践有着非常紧密的联系。但是,我们希望不管读者最后使用哪种语言和工具来构建应用,都可以借鉴本书中介绍的这些经验。

我们真诚地希望这本书能成为读者重要的参考资料和指南,也希望书中的知识、建议和示例能有助于读者构建良好的微服务产品和应用。

这是一本关于微服务应用的开发和部署主题的书,非常有实用性。本书解决了将微服务部署到生产环境的难题,是写给那些对面向服务开发技术掌握得比较扎实的开发人员和架构师的。基于读者对传统系统的理解,本书会先对微服务设计原则进行比较深入的概述,然后会指导读者如何将服务可靠地发布到生产环境。在学习搭建集群和维护这些已部署的系统时,本书中的例子会用到Kubernetes、Docker和Google Container Engine这样的工具和技术。

本书所使用的技术适用于以大部分流行的编程语言开发的微服务。在本书中,我们决定以Python作为主要语言,因为它的风格比较自由,语法比较简洁。这样可以使书中的代码示例可以更加清晰和明确。如果读者不熟悉Python,也不用担心——在运行这些代码时,我们会专门进行说明。

本书第一部分简单介绍了微服务,研究了微服务系统的特性和益处,以及开发过程中可能面临的挑战。

第二部分研究微服务应用的架构和设计。

第三部分给出微服务部署的一些最佳实践。

在本书最后一部分(第四部分),我们会研究微服务的可观测性以及微服务开发中“人”的因素。

本书包含许多源代码示例。这些代码既有按编号列出的,也有内嵌在普通文本中的。在这两种情况中,我们会用等宽字体来表示源代码,以便于和普通文本进行区分。有时候代码是粗体的,这些代码有的是对某些特定的代码行进行突出显示,有的是用来区分输入的命令与输出的结果。

在许多例子中,我们用黑体字体来表示最初的源代码;我们增加了一些换行,并通过修改缩进来适应本书的页面尺寸。除此之外,如果我们已经通过文字对代码进行了描述,通常会将源代码中的注释去掉。

你在本书中可以看到运行实例代码的说明。我们通常使用Docker 和docker-compose来简化运行步骤。附录给出了第10章中所使用的Jenkins的配置,以便让本地部署的Kubernetes能够顺利运行。


本书由异步社区出品,社区(https://www.epubit.com/)为读者提供相关资源和后续服务。

要获得本书配套的源代码,请在异步社区本书页面中单击,然后在下载界面按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎读者将发现的问题反馈给我们,帮助我们提升图书的质量。

若读者发现错误,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对读者提交的勘误进行审核,确认并接受后,将赠予读者异步社区的100积分(积分可用于在异步社区兑换优惠券、样书或奖品)。

我们的联系邮箱是contact@epubit.com.cn。

如果读者对本书有任何疑问或建议,请发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果读者有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ selfpublish/submission即可)。

如果读者来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果读者在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请将怀疑有侵权行为的链接发邮件给我们。这一举动是对作者权益的保护,也是我们持续为广大读者提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


在这一部分,我们将介绍微服务架构、探讨微服务应用的特性及优点并展示微服务应用开发过程中所要面临的某些挑战。我们还将介绍一家虚拟的SimpleBank公司,这家公司正在尝试构建微服务应用,也将是本书中许多示例的故事主线。


本章主要内容

为了解决各种各样的复杂问题,软件开发者一直致力于努力提供各种有效而及时的解决方案。通常大家所要解决的第一个问题就是:客户到底想要什么?如果开发者比较擅长挖掘用户的需求或者运气好的话,还是有机会把这个问题搞清楚的。但是我们的工作不大可能就此终结。应用会继续发展壮大:开发者要对应用出现的问题进行调试,要开发新的功能,还要保证应用的可用和平稳运行。

面对日渐壮大的应用,即便团队成员受过最专业的训练,他们可能也只是挣扎着来尽量维持之前的节奏和灵活性。最坏的情况下,曾经简洁、稳定的产品变得越来越难以处理、越来越脆弱。开发者们疲于处理服务故障,焦虑于发布新的版本,迟迟都不能交付新的功能或者补丁,无法再为顾客持续地交付更多价值。不管是客户还是开发成员,都会因此而感到失落。

微服务为我们提供了一种更好地持续交付业务影响的方式。相较于单体应用,使用微服务构建出来的应用是由一系列松耦合的、自治的服务组成的。通过开发这些只做一件事的服务,开发者可以避免大型应用中所存在的缺乏活力和混乱的状态。即便是对于已有的应用系统,开发者也可以一步步地将其中的功能抽取为独立的服务,这样可以使整个系统的可维护性更强。

在采用微服务以后,我们很快就意识到,开发更小的、更独立的服务只是保证关键业务型应用稳定运行的一部分工作而已。毕竟,所有成功的应用在生产环境里所度过的时间要远远长于在代码编辑器里的时间。如果想要通过微服务来交付价值,团队就不能只关注开发这一步,还需要在部署、监控和诊断这些运维领域具备专业能力。

微服务应用是一系列自治服务的集合,每个服务只负责完成一块功能,这些服务共同合作来就可以完成某些更加复杂的操作。与单体的复杂系统不同,开发者需要开发和管理一系列相对简单的服务,而这些服务可能以一些复杂的方式交互。这些服务之间的相互协作是通过一系列与具体技术无关的消息协议来完成的,这些协议可能是点到点形式的,也可能是异步形式的。

这种想法听起来很简单,但是它确实能够显著降低复杂系统开发过程中的摩擦和冲突。传统的软件工程实践倡导设计良好的系统都应该具备高内聚、低耦合的特点。具备这些特性的系统更加易于维护,并且在面对变更时,也更加容易适应和扩展。

内聚度是用来衡量某个模块中的各个元素属于一个整体的紧密程度的指标,耦合度则是衡量一个元素对另一个元素的内部运行逻辑的了解程度的指标。在讨论内聚度时,罗伯特·C.马丁(Robert C. Martin)的单一职责原则是一种非常有用的方式:

将那些因相同原因而修改的内容聚合到一起,将那些因不同原因而修改的内容进行拆分。

在单体应用中,开发者会在类、模块、类库的层面来设计功能属性;而在微服务应用中,开发者的目标则变成了可独立部署的功能单元——要为这些功能单元设计功能属性。单个微服务应该是高内聚的:它应该只负责应用的某一个功能。同样,每个服务对其他服务的内部运行逻辑知道得越少,就越容易对自己的服务或者功能进行修改,而不需要强迫其他服务一起进行修改。

为了更全面地了解如何搭建微服务应用,我们会通过一个在线投资工具展开介绍。接下来,我们考虑这一工具的一些功能:开户、存取款、下单购买或出售金融产品(如股票)以及风险建模和金融预测。

我们再研究一下出售股票的过程:

(1)用户创建一个订单,用来出售其账户里某只股票的股份;

(2)账户中的这部分持仓就会被预留下来,这样它就不可以被多次出售了;

(3)提交订单到市场上是要花钱的——账户要缴纳一些费用;

(4)系统需要将这个订单发送给对应的股票交易市场。

图1.1展示了提交出售订单的流程,这可以看作整个微服务应用的一部分。可以看到,微服务有三大关键特性。

(1)每个微服务只负责一个功能。这个功能可能是业务相关的功能,也可能是共用的技术功能,比如与第三方系统(如证券交易所)的集成

(2)每个微服务都拥有自己的数据存储,如果有的话。这能够降低服务之间的耦合度,因为其他服务只能通过这个服务提供的接口来访问它们自己所不拥有的数据。

(3)微服务自己负责编排和协作(控制消息和操作的执行顺序来完成某些有用的功能),既不是由连接微服务的消息机制来完成的,也不是通过另外的软件功能来完成的。

图1.1 应用中各微服务间的通信流程图:用户出售金融股票持仓

除了这三大特性,微服务还有两个基本特性。

(1)每个微服务都是可以独立部署的。如果做不到这一点,那么到了部署阶段,微服务应用还是一个庞大的单体应用。

(2)每个微服务都是可代替的。每个微服务只具备一项功能,所以这很自然地限制了服务的大小。同样,这也使得每个服务的职责或者角色更加易于理解。

微服务与传统的面向服务架构(SOA)在思想上的一个关键区别就是微服务负责协调系统中的各个操作,而SOA类型的服务通常使用企业服务总线(ESB)或者更复杂的编排标准来将应用本身与消息和流程编排拆分开。在SOA模型下,服务通常缺乏内聚性,因为业务逻辑会不断地被添加到服务总线上,而非服务本身。

思考一下,这个“在线投资系统”功能解耦的方式是很有意思的。它能够帮助开发者在未来面对需求变更时更加灵活。想象一下,当需要修改收费的计算方式时,开发者可以在不修改上下游服务的情况下,直接修改和发布fee服务。再考虑一个全新的需求:用户下单以后,如果订单不符合正常的交易方式,系统需要向风控团队发送告警。这也是容易实现的,只要基于order 服务发出的事件通知开发一个新的微服务,让这个新的服务来执行这个操作即可,同样不需要修改系统其他模块。

同样,可以思考一下如何通过微服务来对应用进行扩展。在《可扩展性的艺术》(The Art of Scalability)一书中,阿尔伯特(Abbott)和费舍尔(Fisher)定义了一个被称为“扩展立方体”的三维扩展方案,如图1.2所示。

图1.2 应用扩展的三个维度

单体应用一般都是通过水平复制进行扩展的:部署多个完全相同的应用实例。这种方式也称作饼干模具(cookie-cutter)扩展或X轴扩展。相反,微服务应用是一个Y轴扩展的例子,我们将整个系统分解为不同的功能模块,然后针对每个模块自己特有的需求来进行扩展。


注意 

Z轴是指对数据进行水平分区:sharding。不管是微服务应用还是单体应用,开发者都可以采用数据分区分片的方法。但是本书不再针对这一主题进行展开。


我们回过头来再看一下这个“在线投资系统”案例的几个特点:金融预测是计算量特别繁重的功能,但是极少会被使用;复杂的监管和业务规则控制着投资账户;市场交易的规模是海量的,而且还要求极低的延迟。

如果采用微服务的方式开发,为了满足这些功能要求,我们可以针对每个问题选择最合适的技术工具,而不是像将方钉楔进圆洞那样死板地采用固有的技术和工具。同样,自治和可独立部署意味着工程师可以分别管理这些微服务所对应的资源需求。有意思的是,这也包含了一种与生俱来的减少故障的方式:如果金融预测服务出现了故障,它不会导致市场交易服务或者投资账户服务也产生连锁故障。

微服务应用具备一些很有意思的技术特性:按照单一功能来开发服务可以让架构师能够在规模和职责上很自然地划定界限;自治性使得开发者可以独立地对这些服务进行开发、部署和扩容。

支撑微服务开发的五大文化和架构原则为:自治性、可恢复性、透明性、自动化和一致性。

工程师在开发和运行微服务应用时,应该运用这些原则来推动自己做出技术和组织决策。下面我们逐一研究。

1.自治性

我们已经明确了微服务是自治的服务,每个服务的操作和修改都是独立于其他服务的。为了保证自治性,开发者需要将服务设计得松耦合、可独立部署。

(1)松耦合——每个服务通过明确定义的接口或者发布的事件消息来与其他服务进行交互,这些交互独立于协作方的内部实现。比如我们在前面介绍过的order服务并不需要知道 account transaction服务的具体实现方式,如图1.3所示。

(2)可独立部署——不同的服务通常是由多个不同的团队并行开发的。强迫所有团队按照同样的步调或者按照专门设计的步骤进行部署,都会导致部署阶段有风险更大、更加使人焦虑。理想情况下,大家想要这些服务都能够快速、频繁地发布小的改动。

图1.3 服务按照定义好的契约来通信以实现松耦合,契约隐藏了实现的细节

自治性也是一种团队文化。将各个服务的责任和所有权委派给有责任交付商业价值的团队,这是至关重要的。正如我们所确定的,组织设计会对系统设计产生影响。清晰的服务所有权有助于团队基于他们本身所处的环境和目标来迭代开发和做出决策。同样,当团队同时负责一个服务的开发和生产时,这种模式也能够促进提升团队端到端的主人翁意识,是非常合适的。


注意 

在第13章中,我们将讨论有责任感和自治性的工程团队的培养及其在微服务中的重要性。


2.可恢复性

微服务与生俱来地具备故障隔离的机制:如果开发者独立地部署这些微服务,那么当应用或者基础设施出现故障后,故障将只会影响到整个系统的一部分功能。同样,部署的功能粒度越小,开发者越能更平缓地对系统进行变更,这样才不会在发布新功能的时候发布的是一个有风险隐患的大炸弹。

再次考虑一下那个“投资工具”,如果market服务不可用,系统将不能把订单发布到市场上。但是用户仍然可以创建订单,当下游的功能恢复以后,market服务能够把这些订单筛选出来继续发到市场上。

尽管将应用拆分成多个服务能够隔离故障,但它还是会存在多点故障的问题。同样,当故障发生的时候,开发者需要能够解释到底发生了什么问题以避免连锁反应。这包括设计层面的(在可能的情况下支持异步交互以及适当地使用熔断器和超时),还包括运维层面的(比如,使用可验证的持续交付技术和对系统活动进行稳定可靠地监控)。

3.透明性

最重要的一点,当故障发生时,开发者需要记得微服务应用是依赖于多个服务(而非单个系统)之间的交互及其表现的,而这些服务可能是由不同的团队开发的。不管在什么时候,系统都应该是透明的、可观测的,这样既可以发现问题,也可以对问题进行诊断。

应用中的每个服务都会产生来自于业务、运营和基础设施的数据、应用日志以及请求记录。因此,开发者需要搞清楚这些大量数据的含义。

4.自动化

通过开发大批的服务来缓解应用不断变大所带来的痛苦,这看似是有悖常理的。事实上,相对于开发一个单体应用,微服务确实是一种更加复杂的架构。通过采用自动化和在基础设施内保持服务之间的一致性,开发者可以极大地降低因这些额外的复杂性引入的管理代价。开发者需要使用自动化来保证部署和系统运维过程中的正确性。

微服务架构的流行与两种趋势是同时发生的—— 一种趋势是DevOps技术得到主流接纳,其中的典型就是基础设施即代码(infrastructure-as-code)技术;另一种趋势是完全通过API进行编程的基础设施环境(如AWS和Azure)的兴起。这三者的同时发生并不是巧合。后两种趋势做了大量的基础工作,这才使得微服务在小型团队里具有可行性。

5.一致性

最后,以恰当的方式调整开发工作是至关重要的。开发者的目标应该是围绕业务概念来组织服务和团队,只有这样安排,服务和团队的内聚性才能更高。

为了理解一致性的重要性,我们考虑一下另外一种方式。许多传统的SOA系统都是分别部署它们的技术层的——UI层、业务逻辑层、集成层和数据层。

SOA和微服务架构的对比如图1.4所示。

图1.4 SOA和微服务架构的对比

一方面,在SOA中使用横向拆分是有问题的,因为这样会导致内聚的功能被分散到多个系统中。新的功能可能需要协调发布到多个服务中,并且可能与其他在同一技术抽象层次的其他功能产生耦合,而这种耦合是不可接受的。另一方面,微服务架构应该偏向于纵向拆分。每个服务应该与一个独立的业务功能相匹配,并且将所有相关的技术层的内容封装在一起。


注意 

极少数情况下,构建一个实现了某个特定技术功能的服务也是合理的,比如多个服务都需要与某个第三方服务进行集成,那就可以将这一集成工作封装成一个服务。


开发者应该时刻牢记是谁在消费这些服务。为了保证系统的稳定性,开发者需要在开发过程中有足够的耐心来保持所开发服务的向后兼容性(不论是显式地兼容,还是同时运行多个版本的服务),这样就可以确保不需要强迫其他团队升级或者破坏服务之间已有的复杂交互。

牢记这五大原则有助于开发者更好地开发微服务,进而使系统更易于修改、扩展性和稳定性也更强。

许多组织已经成功地构建和部署了微服务,业务领域也跨越很大,涉及媒体(The Guardian)、内容分发(SoundCloud、Netflix)、交通物流(Hailo、Uber)、电子商务(Amazon、Gilt、Zalando)、银行(Monzo)和社交媒体(Twitter)。

上述领域的大部分公司都是采用了单体先行的方案[1]。他们从开发单个大型应用开始,然后逐渐迁移到微服务中,以解决他们所面临的发展压力,见表1.1。

表1.1 软件系统的增长压力

压  力

描  述

规模

系统的处理规模可能大大超过了最初的技术选型的承载能力

新功能

新的功能可能和现有的功能关系并不紧密,或者换一种技术可能更容易解决问题

工程团队的壮大

随着团队越来越大,需要沟通的人和渠道也越来越多。新加入的开发者要花更多的时间来理解现有的系统,相应地,创造产品价值的时间也变少

技术债务

系统所增加的复杂度(包括之前的开发决策导致的债务)导致修改的难度提高

国际分发

国际分发使得数据一致性、可用性和延迟性面临巨大挑战

比如,Hailo公司想要拓展国际市场,这对他们最初的架构而言已经成为巨大的挑战,此外,他们还想加快功能交付的速度[2]。SoundCloud公司想要提升生产力,而当初的单体应用的复杂度阻碍了公司的发展[3]。有时候,这种转变和业务优先级的改变是一致的:Netflix从实体DVD分发转移到流媒体内容领域。有些公司已经完全将它们最初的单体应用下线了。但对于很多其他公司而言,这还是一个进行中的过程,一个单体应用的周围有一系列更小的服务。

现在,微服务架构已经十分普及。很多早期采用者通过开源、写博客和做演讲的方式介绍了他们所采用的实践方案,因此越来越多的团队开始直接用微服务来开发他们的新项目,而非先开发一个单体应用,比如,Monzo已经开始将微服务作为其构建更良好的、更具可扩展性的银行系统的使命之一。[4]

有大量成功的业务是基于单体应用软件来开发的,我们能立刻想到的有Basecamp、StackOverflow和Etsy。在单体应用的世界里,我们有很多可以借鉴的东西,这其中包括传统传承下来的思想和看法、长期建立起来的软件开发实践和知识。那么,我们为什么还要选择微服务呢?

1.技术差异性为微服务开路

有一些公司同时采用了多种不同的技术,这使得微服务成为很自然的选择。在Onfido公司,我们引入一个由机器学习驱动的产品时,发现它和我们当时使用的Ruby技术栈并不完全匹配,于是开始开发微服务。即便开发者还没有完全决定使用微服务方案,运用微服务的一些原则也会让开发者在解决业务问题的时候有更大的技术选择范围。不过,技术的差异性并不总是这么明显。

2.开发冲突随着系统发展而增加

归根到底,这就是复杂系统的特点。在本章开头,我们提到,为了解决复杂的问题,软件开发者努力在设计提供高效、及时的解决方案。但是我们开发的软件系统天生就具有复杂性,没有哪种方法论或者架构能够消除系统核心的这种本质复杂性(essential complexity)[5]

但是,这并不是沮丧的理由。开发者还是可以有所作为的,可以通过采用恰当的开发方案来确保开发出的是一套良好的复杂系统,而从那种偶然的复杂性(accidental complexity)[6]中解脱出来。

花时间考虑一下,作为一名企业软件开发者,开发者想要获得的是什么。丹·诺斯(Dan North)讲得很好:

软件开发的目标是持续地缩短交付周期来产生积极的商业价值。

在复杂的软件系统中,我们的困难是:面对变化却还要持续地交付价值,即便系统越来越庞大越来越复杂,也要在保持敏捷、节奏和安全的情况下持续交付。因此,我们相信一个良好的复杂系统应该能在整个生命周期内将冲突和风险这两个因素的影响最小化。

冲突和风险会限制开发速度和敏捷性,进而会影响交付商业价值的能力。随着单体应用越来越大,下面这些几个因素会导致冲突。

(1)变更周期耦合在一起,导致协作障碍并增大回滚的风险。

(2)在没有严格规范的团队中,软件模块和上下文边界含混不清,导致组件之间产生意料之外的紧耦合。

(3)应用的大小成为痛点:持续集成作业、系统发布(甚至是本地应用启动)都会变得越来越慢。

并不是所有单体应用都存在这几个问题,但是很遗憾,我们见过的大部分系统都有这三个问题。同样,在前面提到的那些公司中,这些问题也是他们共同的故事主线。

3.微服务降低冲突和风险

微服务通过三种方式来降低冲突和风险:在开发阶段将依赖进行隔离和最小化;开发者可以对单个的内聚组件进行思考,而非整个系统;能够持续交付轻量的、独立的变更。

在开发阶段将依赖进行隔离和最小化——不论是多个团队之间的依赖,还是已有代码层面的依赖,这种方法都可以让开发者的行动更加迅速。开发可以并行执行,还减少了单体应用中存在的对历史决策的长期依赖。技术债务很自然地被限制在服务的边界内。

和单体应用相比,微服务更易于独立构建和理解。这非常有益于提升成长中的组织的开发生产力。同时,微服务还提供了一种灵活且令人信服的模式,让大家可以积极应对不断增长的规模或顺利地引入新技术。

小的服务也是持续交付的重要推动者。在大型应用中,部署的风险是很高的,而且涉及漫长的回归和验收周期。通过部署更小的功能元素,开发者可以降低每次独立部署的潜在风险,更好地隔离对线上系统的改动。

现在,我们达成两个结论:其一,开发小的、自治的服务能够降低在开发长期运行的复杂系统时出现的冲突;其二,通过交付内聚的独立功能,开发者可以开发出一个面对变化时灵活、易扩展且具备可恢复性的系统,这有助于开发者在降低风险的同时交付商业价值。

这并不意味着每个人都应该构建微服务。当有人问“我需要微服务吗”时,如果真的有一个客观的标准答案,那是最好不过的,但是实际上开发者只能说“要视情况而定”。这个“情况”包括开发者的团队、开发者的公司以及开发者要开发的系统的特点。如果系统的领域范围并不重要,那么开发和运行这种细粒度的系统所增加的复杂度会超过开发者所获得的好处。但是如果开发者已经面临着本章前面提到的那些挑战,那么有充分的理由来采用微服务的解决方案。


一个发人警醒的故事 

我们听过一个因实施微服务而出现问题的故事。这家出现问题的初创公司当时已经开始扩大规模,他们的CTO认为唯一的解决方案就是将应用按照微服务架构重新开发。如果开发者听到这句话还无动于衷,那么现在可以开始祈祷了,因为这将是噩梦的开始。

技术团队开始改造他们的应用。这花费了他们5个月的时间,在这段时间里,他们既没有发布任何新功能,也没有将任何微服务的功能发布到生产环境。在业务最繁忙的那段时间里,这个团队上线了他们这套新的微服务应用,上线后完全是一团乱麻,最终他们被迫将系统回滚到当初的单体应用上。

这种迁移给微服务带来了很坏的名声。很少有业务允许有好几个月的时间完全停滞新功能开发,也很少有业务会纵容一个新的架构方案突然地直接发布上线。好在这样的例子很少,大部分我们关注过的成功的微服务迁移都是一点一点推进的,会在架构愿景、业务需求、优先级和资源约束之间进行平衡。尽管这需要耗费更长的时间并需要做更多的技术工作,但是开发者永远不会希望成为上述故事里的主人公。


我们进一步深究和分析一下设计和运行微服务系统的代价和复杂度。微服务并不是唯一通过分解和分布式实现“涅槃”(解决一切麻烦)的架构模式,但是过去的一些尝试(如SOA)已经被大家认为是不成功的。没有哪一种技术是“银弹”,比如,我们提到的微服务架构,就极大地增加了系统中运行的模块的数量。在将功能和数据所有权分发到多个自治的服务上的同时,开发者也将整个应用的稳定性和安全操作的责任分配到了这些服务上。

在设计和运行微服务应用时,开发者会遇到很多挑战,如下所示。

(1)识别和划定微服务范围需要大量专业的业务领域知识。

(2)正确识别服务间的边界和契约是很困难的,而且一旦确定,是很难对它们进行改动的。

(3)微服务是分布式系统,所以需要对状态、一致性和网络可靠性这些内容做出不同的假设。

(4)跨网络分发系统组件以及不断增长的技术差异性,会导致微服务出现新的故障形式

(5)越来越难以了解和验证在正常运行过程中会发生什么事情。

这些挑战是如何影响微服务开发的设计和运行阶段的呢?我们前面介绍了微服务开发的五大核心原则。首当其冲的就是自治性。为了让服务实现自治,开发者需要将它们设计为:从整体看,它们是松耦合的;而单独看每一个服务的话,它们内部封装了高度内聚的功能单元。这是一个不断演进的过程。服务的功能范围可能会随着时间而发生变化,开发者未来也可能会经常从现有的甚至可能将要下线的服务中剥离出新的功能来。

做出这些选择是很困难的——尤其是在应用开发的初期!服务解耦的主要驱动力就是开发者所确定的服务边界,如果这一步出错的话,服务将难以修改,整体而言,也就会导致应用不够灵活、不易于扩展。

1.划定微服务范围需要业务领域知识

每个微服务都只负责一个功能。识别这些功能需要丰富的业务领域知识。在应用生命周期的初期,开发者的领域知识充其量是不够完整的,而最糟糕的情况下,开发者了解的这些知识可能是错误的。

对问题领域理解不充分可能会导致错误的设计决定。和单体应用中的模块相比,微服务应用的服务边界更加僵化。这也就意味着,如果范围划定出错,可能给下游造成更高的代价(图1.5):开发者可能需要在多个不同的代码库上进行重构;可能需要将数据从一个服务的数据库迁移到另一个服务中;可能没有发现服务间的隐式依赖,导致在部署阶段出现错误或者不兼容的问题。

但是,基于并不充分的业务领域知识做出设计决策的事情并不是微服务所独有的问题。区别只在于这些决策所造成的影响。


注意 

在第2章和第4章中,我们将通过一个示例来讨论服务识别和范围划定的最佳实践。


2.服务契约的维护

每个微服务都应该独立于其他服务的实现方式。这样才能实现技术多样性和自治性。为了做到这一点,每个微服务应该对外暴露一个契约(类比于面向对象设计中的接口)——用于定义它所期望接受和返回的消息。一个良好的契约应该具有以下特点。

(1)完整:定义了交互所需的全部内容。

(2)简洁:除了必需的信息,没有多余的内容,这样消费者就能在合理的范围内组装消息。

(3)可预测:准确反映了所有实现的真实表现。

任何设计过API的人可能都知道实现这些要求是多么困难。契约会成为服务之间的黏合剂。随着时间的推移,开发者会对契约逐渐做出调整,但是还需要保持对现有协作方的向后兼容性。稳定性和变化这两个矛盾体之间的紧张关系是很难把握分寸的。

图1.5 错误地划分服务范围可能导致跨多个服务边界进行复杂且代价巨大的重构

3.微服务应用是多个团队设计的

在规模大一些的组织中,通常是由多个团队来开发和运行微服务应用的。每个团队负责不同的微服务,他们有自己的目标、工作方式和交付周期。如果开发者还需要和其他的独立团队协调时间表和优先级,就很难设计出一个内聚的系统。因此,要协调任何庞大的微服务应用的开发,都需要跨多个团队在优先级和实践层面达成一致。

4.微服务应用是分布式系统

设计微服务应用也就意味着设计分布式系统。关于分布式系统的设计,有许多谬论,其中包括网络是可靠的、网络延迟为0、带宽是无限的以及数据传输成本为0。

显然,开发者在非分布式系统中可以做出的那些假设(如方法调用的速度和可靠性)都不再合适,基于这些假设实现的系统会非常糟糕和不稳定。开发者必须考虑到延迟性、可靠性以及应用中的状态一致性。

一旦应用成为一个分布式应用,应用的状态数据就会分布到许多地方—— 一致性就会成为难题。开发者不再能保证操作的顺序。在多个服务上进行操作时,开发者也不再能像ACID这样继续保证事务。这还会影响到应用层面的设计:开发者需要考虑服务如何在不一致的状态下进行操作以及如何在事务失败的情况下进行回滚。

微服务方案本身会使系统中可能出现的故障点增多。为了说明这一点,我们回到前面介绍过的“投资工具”那里。应用中可能出现的故障点如图1.6所示。可以看到,有些类型的故障可能会在多处发生。这些故障都会影响订单的正常处理流程。

图1.6 提交出售订单时可能的故障点

在生产环境中运行应用时,开发者可能需要回答下面几个问题,请考虑一下。

(1)如果用户不能提交订单出现故障,如何判断是哪里发生了故障?

(2)如何在不影响下单操作的情况下部署一个新版本的服务?

(3)如何知道要调用哪些服务?

(4)如何在多个服务间测试应用是否正常工作?

(5)如果某个服务不可用,会发生什么事情?

微服务并不能消除风险,而是将这个成本移到了系统生命周期的后半阶段:降低了开发过程中的冲突,但是增加了运维阶段系统部署、验证以及监控的复杂度。

微服务方案推荐在系统设计中采用演进式的方案,这样开发者可以在不修改现有服务的情况下开独立开发新的功能,就能将变更的风险和代价最小化。

但是在不断变化的解耦系统中,清楚地了解整体的情况可能变得极度困难,这又使得问题诊断和支持变得更具有挑战性。当出现故障时,开发者需要通过一系列的方式来跟踪系统实际发生的行为(调用了哪个服务、顺序是什么以及输出是什么),但是还需要一些途径来了解系统应该发生的行为。

最后,工程师会面对微服务的两大运维挑战:可观测性和多点故障。下面我们来一一展开介绍。

1.难以实现的可观测性

我们在1.1.2节中介绍了透明性的重要性。但是为什么在微服务应用中,透明性会变得更困难呢?这是因为开发者需要对整体有所了解。开发者需要将许许多多的碎片拼接起来形成整体的蓝图,所以需要将每个服务所生产的数据关联并连接到一起,进而在了解了交付商业价值整体的来龙去脉之后理解每个服务所做的工作。每个服务的日志提供了系统运行的部分视图,这是很有用的,但是开发者需要同时从微观细节和宏观整体两方面来更加全面地理解这个系统。

同样,开发者现在运行了多个应用,根据所选择的部署方式,基础设施指标(像内存和CPU利用率)和应用之间的相关性可能不再那么明显了。这些指标依旧有用,但是不再像在单体应用中那么重要了。

2.不断增加的服务使得故障点增多

如果我们说“任何可能出现故障的东西最终肯定会出现故障”,这并不是说我们太悲观。这非常重要,从现在开始,开发者就要牢牢地把这句话记在自己的脑海里:如果开发者提前认定构成系统的这些微服务是有缺陷的和脆弱的,那么就能够更好地提醒自己如何对系统进行设计、部署和监控,而不会等到出现故障时才大吃一惊。开发者需要考虑如何让系统能够在单个组件出现问题的情况下继续运行。这意味着,每个服务都需要更具鲁棒性(考虑到错误检查、故障切换、恢复),同样,整个系统也应该运行更加可靠,即便单个组件做不到100%的可靠。

在个体层面,开发者应该熟悉每一个微服务——即便它比较小。为了开发一个微服务,开发者会使用很多相同的框架和技术:Web应用框架、SQL数据库、单元测试、类库等,这些都是开发者在开发应用程序时经常会用到的。

在系统层面,选择微服务架构会对开发者设计和运行应用的方式产生重要影响。纵览本书,我们会聚焦于微服务应用开发生命周期的三大阶段(图1.7):服务设计、将服务部署到生产环境中和功能监控。

图1.7 微服务开发周期的三大迭代阶段:设计、部署和监控

在这三大阶段中,每个阶段所做出的合理决定都有助于构建出具备可恢复性的应用,即便面对不断变化的需求和不断增加的复杂度,应用仍然具备可恢复性。下面我们逐一介绍这三大阶段,并思考用微服务交付应用所要采取的措施和步骤。

在开发微服务应用时,开发者需要做出一些设计决策。这些设计决策在开发单体应用时并不会遇到。开发单体应用时,我们通常都会遵循一些已知的模式或者框架,比如三层架构或者模型-视图-控制器(MVC)。但是微服务的设计技术还处于相对起步的阶段。鉴于此,开发者需要考虑以下问题。

(1)是从一个单体应用起步,还是一开始就使用微服务?

(2)应用的整体架构以及开放给外部消费者的接口。

(3)如何识别和划定服务的边界?

(4)服务之间是如何通信的?同步还是异步?

(5)如何实现服务的可恢复性?

有太多内容要讲。现在,我们会针对这些问题一一进行解答,以便开发者能够理解关注这些内容对于设计出良好的微服务应用的重要性。

1.单体应用是否先行

在开始采用微服务这件事情上,开发者会看到两种截然不同的趋势:其一,单体先行;其二,只使用微服务方案。赞成前者的开发者给出的理由是:始终应该以单体应用开始,因为在前期,开发者还没了解系统中各个组件的边界,而在微服务应用中,如果这一步出错的话,代价会大得多。换句话说,在单体应用中选择的边界与精心设计过的微服务应用中的边界并不需要保持一致。

虽然在开始的时候,微服务方案的开发速度会慢一些,但是能够降低未来开发的冲突和风险。同样,随着工具和框架越来越成熟,微服务最佳实践不再那么令人生畏,会变得越来越容易应用。不管开发者是考虑从单体应用迁移到微服务,还是直接开发一个新的微服务应用,这两条路都可以选,本书的建议都是有帮助的。

2.服务的范围划定

为每个服务选择恰当水平的职责——功能范围——是设计微服务应用中最困难的挑战之一。开发者需要基于服务提供给组织的业务功能对其进行建模。

我们来对本章开头的例子做一下扩展。如果开发者想引入一个新的特殊类型的订单,如何对服务进行修改呢?开发者可以通过三种方式来解决这个问题(图1.8):①对现有的服务接口进行扩展;②添加一个新的服务接口;③添加一个新的服务。

每种方案都各有优缺点,也会影响到应用中各个服务之间的内聚和耦合性。


注意 

在第2章和第4章中,我们会研究服务的功能范围划分,并且会讨论如何在服务职责划分上做出最优的决策。


图1.8 为了划定功能范围,开发者需要决定是新设计一个服务,还是将这个功能划到现有的服务中去

3.通信

服务之间的通信可以是异步的,也可以是同步的。虽然同步系统更易于进行问题排查,但是异步系统的解耦性更高,能够降低变更的风险,还能让系统更易于恢复。但是这种系统的复杂度比较高。在微服务应用中,开发者需要在同步和异步消息之间进行平衡,以有效地对多个服务的行为进行协调。

4.可恢复性

在分布式系统中,一个服务不能完全信任它的协作方服务,这不一定是因为他们的代码很糟糕或者人为失误,还因为开发者不能想当然地认为服务之间的网络以及这些服务的行为是可靠的、可预测的。服务在遇到故障的时候需要能够进行恢复。为了做到这一点,开发者需要通过在出现错误的时候进行回退、对于一些不友好的调用方要限制其请求速率、动态寻找健康服务等方式来使服务具有防御性。

在构建微服务时,开发和运维是相互交织在一起的。开发者将服务开发完成以后就当甩手掌柜,让其他人来部署和运维的方式是行不通的。在由大量的自治服务组成的系统中,如果这个服务是开发者开发的,就应该由开发者来运行它。对服务的运行方式了解清楚,反过来有助于开发者在系统发展壮大以后做出更好的设计决策。

记住,应用的特别之处是它所交付的商业价值。这来源于多个服务之间的协作。实际上,开发者可以将每个服务所提供的特有功能标准化和抽象化,以保证团队聚焦于商业价值。最后,开发者应该达到一个阶段,也就是在部署新的服务时,不涉及任何客套的东西。如果做不到这一点的话,开发者将要投入大量的精力来“通下水道”[在英文中“通下水道”(plumbing)用来比喻一些价值不大的脏活累活],而不能为客户创造任何价值。

在本书中,我们会教开发者如何将已有的服务和新开发的服务可靠地部署到生产环境。为了能够快速地进行创新,部署新服务的成本必须是可以忽略不计的。同样,开发者应该将部署步骤标准化以简化系统操作,并在这些服务上保持一致。为了做到这一点,开发者需要做到两点:其一,将微服务部署的人为操作标准化;其二,实现持续交付的流水线。

我们已经听说过可靠的部署是很“单调”的。“单调”的意思不是说乏味无聊,而是说没有事故发生。遗憾的是,我们看到太多团队的情况恰恰相反:软件部署是一个压力很大的操作,而且病态地需要全员出动来完成。一个服务这样就已经很糟糕了,如果开发者要部署非常多的服务,随之而来的焦虑会让他们疯掉的。下面我们看一下如何通过这些步骤实现稳定可靠的微服务部署。

1.微服务部署的人为操作标准化

通常,每一门语言和框架都有自己的部署工具。Python有Fabric,Ruby有Capistrano,Elixir有exrm,等等。此外,它们自己的部署环境也很复杂,具体表现如下。

(1)应用部署在什么服务器上?

(2)应用有哪些其他工具的依赖?

(3)如何启动这个应用?

在运行环境层面,应用依赖(图1.9)是很广的,这其中可能包括类库、二进制和操作系统包(如ImageMagick和libc)以及操作系统进程(如cron和fluentd)。

从技术角度来说,服务自治一个非常大的好处就是差异化。但是差异化并不会让服务部署变得更加容易。没有一致性,开发者就不能把生产环境的服务部署方法标准化,进而会增加部署管理和引入新技术的成本。最差的情况就是,每个团队重复“造轮子”,每个团队都有自己与众不同的依赖管理、打包、部署和应用运维的方法。

经验表明,完成这项工作最好的工具就是容器。容器是一种操作系统层面的虚拟化方法,它支持在同一台主机上运行多个独立的系统——每个系统共享同一个内核,但是都有自己的网络和进程空间。与虚拟机数分钟的构建和启动时间相比,容器的构建和启动速度都要快很多,能够在秒级完成。开发者可以在同一台机器上运行多个容器,这样不但可以简化本地开发的复杂度,而且能够有助于在云环境中优化资源利用率。

容器将应用的打包过程、运行接口进行了标准化,并且为操作环境和代码提供了不可变(immutability)的特性。这使得它们成了在更高层次进行组合的强有力的构件。通过使用容器,开发者可以定义任何服务的完整执行环境并将它们相互隔离。

图1.9 应用对外暴露了一个运维API,这个应用有多种类型的依赖,包括类库、二进制依赖和辅助进程

虽然可以使用容器技术的许多实现方案和概念(除了Linux,还有FreeBSD的jails和Solaris的zone),但是到目前为止,我们所使用的最成熟和友好的工具是Docker。我们会在本书后面部分介绍这个工具。

2.实现持续交付流水线

持续交付是一种开发实践方式。通过这种实践方式,开发者可以在任何时间将软件可靠地发布到生产环境中。想象一下工厂的生产线:为了持续交付软件,开发者建立了类似的流水线,将开发者的代码从提交状态变成活生生的操作。图 1.10所示的是一个简单的流水线。可以看到,每个阶段都能够向开发团队反馈代码的正确性。

图1.10 微服务的部署流水线概览

前面我们提到,微服务是持续交付的理想推动者,因为它们体积更小,这意味着开发者可以快速开发这些服务并独立发布。采用微服务的开发方式并不意味着就自动做到了持续交付。为了能够持续地交付软件,开发者需要关注以下两个目标。

(1)制订一组软件必须通过的验证条件。在部署流程的每个环节,开发者都应该能够证明代码的正确性。

(2)代码从提交状态发布到生产环境上的流水线实现自动化。

搭建一套可验证的、正确的部署流水线能够让开发者工作得更加安全,并和他们在服务开发阶段的迭代步调保持一致。在交付新功能时,这种流水线是一种可靠、可重复的流程。理想情况下,开发者应该有能力将流水线中的验证条件和步骤标准化,并在多个服务间进行使用,这样能够进一步降低部署新服务的成本。

持续交付还能降低风险,因为软件的质量和团队的交付变更的敏捷性都能够得到提升。从产品的角度来讲,这可能意味着开发者可以按照一种更精益的方式进行工作——快速验证开发者的假设并进行迭代。


注意 

在第三部分,我们会使用免费的持续集成工具Jenkins的Pipeline功能来搭建一个持续交付的流水线。我们还会研究一些不同的部署模式,比如金丝雀(canaries)部署和蓝绿(blue-green)部署。


在本章中,我们已经讨论了透明性和可观测性。在生产环境中,开发者需要了解系统的运行情况。它的重要性有两点:其一,开发者想要主动发现系统中的薄弱环节并进行重构;其二,开发者需要了解系统的运行方式。

和单体应用相比,在微服务应用中,完全的监控是一件更加困难的事情。因为一个事务可能会涉及多个不同的服务;在微服务中,不同技术开发的服务可能会生成格式相反的数据;运维数据的总规模也要比一个单体应用高很多。但是如果开发者能够理解系统的运行方式,并且能够进行深入观测的话,即便微服务很复杂,开发者还是可以对系统进行高效的修改的。

1.发现潜在的薄弱环节并进行重构

不论是引入了程序错误、运行环境出错、网络发生故障,还是硬件出现了问题,都会导致系统出现故障。久而久之,消除这些未知的缺陷和错误的成本要高于快速和高效地响应所需要的成本。监控和报警系统使得开发者可以对问题进行诊断,并判断是什么问题导致了当次故障。开发者可以通过自动化的机制来响应这些告警,比如在另一个机房创建一个新的容器实例,或者增加服务的运行实例的数量来解决负载问题。

为了将故障的影响最小化并避免在系统内产生连锁反应,开发者需要采用一些支持服务局部降级的方案来设计和调整服务间的依赖。即便一个服务不可用,也不应该导致整个应用垮掉。认真思考应用中可能的故障点,承认故障总是会发生并做相应的准备,这是非常重要的。

2.了解数以百计的服务的行为

为了了解这些服务的行为,开发者需要在设计和实现这些服务时提高“透明性”的优先级。收集日志和一些数据指标,并将它们统一起来用于分析和告警。这样开发者在监控和分析系统的行为时,就可以诉诸于所构建的这个唯一的可信来源(single source of truth)。

我们在1.3.2节中提过,开发者可以标准化和抽象化每个服务提供的特有功能。开发者可以把每个服务看作一个“洋葱”。在“洋葱”的最里面,是这个服务所提供的特有业务功能。它的外面分别是各个工具层——业务指标、应用日志、运维指标和基础设施指标。这些工具可以让业务功能更易于观测。开发者可以在这些层之间跟踪每个请求,之后将从每层收集的数据推送到一个运维数据库用来进行分析和监控,如图1.11所示。

图1.11 一个业务功能的微服务由多个工具层所包围。请求会穿过这些工具层发送给微服务,而返回的结果也会穿过它们发送出去,这个过程中所收集的数据也会存储到一个运维数据库中


注意 

在本书第四部分,我们会讨论如何为微服务搭建一个监控系统、如何收集合适的数据,以及如何用这些数据为一个复杂的微服务应用创建一个实时现场模型。


考察微服务的技术性时,将它与开发这些微服务的工程团队割裂开来进行调查,是一种错误的行为。通过一个个轻量的、独立的服务来构建应用会彻底地改变组织工程化的方式,所以对团队的文化和优先事项进行引导是微服务应用能够成功交付的重要因素。

对于那些已经成功实现了微服务架构的组织来说,很难将原因和结果分清楚。到底是团队的组织结构和表现顺理成章地成就了这种细粒度的服务开发模式,还是细粒度服务的开发经验成就了团队的这种结构和表现?

答案就是,两者都有。长期运行的系统并不仅仅是提出功能需求——然后进行设计、开发——最后把这些功能堆到一起,它还反映了开发者和运维人员的偏好、观点以及目标。康威定律在某种层次上表达了类似的含义:

设计系统的组织……都是受到约束的,其设计出来的方案只是这些组织的沟通结构的翻版。

“受到约束”应该表示沟通结构会限制和约束系统的开发效果。然而事实上,微服务的做法意味着它正好相反:要避免系统开发过程中的冲突和紧张,最重要的方式就是按照开发者要开发的系统的形式和状况来设计组织。

有意识地和组织结构相互依赖实现共生是一种很常见的微服务实践。为了能够从微服务中获益并充分地管理其复杂度,开发者需要制订一些对微服务应用有效的工作原则和做法,而不是继续采用以前开发单体应用时所使用的相同技巧。

(1)微服务既是一种架构风格,也是一系列文化习惯的集合。它以五大核心原则为支撑,它们分别是自治性、可恢复性、透明性、自动化和一致性。

(2)微服务减少了开发冲突,实现了自治性、技术灵活性以及松耦合。

(3)微服务的设计过程是非常有挑战性的,因为它不仅需要丰富的业务领域知识,还需要开发者在团队之间平衡优先级。

(4)服务向其他服务暴露契约。设计良好的契约是简洁的、完整的和可预测的。

(5)在长期运行的软件系统中,复杂性是不可避免的,但是开发者可以通过一些决策来减少冲突和风险,进而持续地在这些系统中交付价值。

(6)自动化和可验证的发布操作能够让部署过程更加可靠和“没有事故发生”,进而降低微服务的风险。

(7)容器技术将运行环境中的服务之间的差异进行抽象化,简化了对类型各异的微服务进行大规模管理的方式。

(8)故障是不可避免的:对团队来说,微服务需要是透明的、易观测的,这样团队才能够主动地管理、了解和真正拥有服务运维;反之亦然。

(9)采用微服务的团队需要在运维方面比较成熟,并且关注于服务的整个生命周期,而不只是关注设计和开发阶段。

[1] 马丁·福勒(Martin Fowler)在他写于2015年6月3日的博文MonolithFirst中解释了“单体先行”这一模式。

[2] 迈特· 汗思(Matt Heath)于2015年5月30日在Medium上发表的A long Journey into a Microservice World

[3] How we ended up with microservices,菲尔·卡尔卡多(Phil Calçado),发表于2015年9月8日。

[4] 见迈特· 汗思(Matt Heath)于2015年5月18日发表的Building microservice architectures in Go

[5] 单词essential complexity和accidental complexity源自于弗雷德·布鲁克(Fred Brook)的《没有银弹》(No Silver Bullet)。本质复杂性(essential complexity)是由待解决的问题所引起的,是问题固有的复杂性。它是与问题相关的复杂性,是无法避免和消除的。比如,某个业务功能要包含A、B、C三个步骤,那么这三步是必不可少的,程序必须完成这三个工作。

[6] 偶然复杂性(accidental complexity)是由工程师制造出来的并能够处理的与问题相关的复杂度。这种复杂性是偶然的,可能是由某些工程师没有进行足够的思考就把某些组件不必要地联系在一起所导致的。


本章主要内容

在第1章中,我们了解了微服务的五大核心原则以及用微服务来实现持续交付软件价值的充分理由,并介绍了一些支持微服务开发的设计和开发实践。在本章中,我们将探讨如何在微服务的新功能开发中应用这些原则和实践。

在本章中,我们会介绍一家虚构的SimpleBank公司。这是一家有着“改变整个投资领域”的宏大愿景的公司,而作为读者的你就是这家公司的一名工程师。SimpleBank公司的工程师团队希望在保证扩展性和稳定性的同时能够快速交付新功能,毕竟他们处理的是人们的真金白银。微服务可能正是他们所需要的。

和开发单体应用相比,开发和运行一个由许多可独立部署的、有自我控制能力的服务所组成的应用是截然不同的一种挑战。我们先思考一下为什么微服务架构非常适合SimpleBank公司,然后带领读者使用微服务来设计一个新功能。最后,我们将明确从概念验证原型到生产级的应用所需要的开发步骤。现在我们开始吧!

SimpleBank团队希望,不管一个人有多少钱,他都能够享受到智能化的金融投资服务。他们相信,不管是购买股票、出售基金还是进行外汇交易,都应该像开储蓄账户那样简单。

这是一项令人激动的使命,但并不容易实现。金融产品有多重的复杂之处:SimpleBank公司需要了解市场规则和错综复杂的法规,同时,它需要与现有的行业系统进行集成,并且要满足严格的精度要求。

在第1章中,我们确认了一些功能:开户、支付管理、下单和风险建模,这些功能或许可以由SimpleBank公司提供给用户。接下来,我们进一步讨论这种可能性,并分析一下这些功能是否适合SimpleBank公司的更大领域范围内的投资工具。这一领域的各个组成部分如图2.1所示。

图2.1 SimpleBank公司要构建的功能模型总览

可以看到,投资工具需要提供的功能远不止那些提供给客户的开户和投资组合管理这样的功能。它还需要管理托管和理财产品设计。这个投资工具需要以客户的名义来持有其资产,并进行转入和转出。它还需要根据客户的需要来制订金融产品策略。

可以想到,这并不是那么简单的事情!读者现在已经可以开始看到一些SimpleBank公司所要实现的业务功能:投资组合管理、市场数据集成、订单管理、基金设计和组合分析。每块业务领域都可能是由多个服务组成的,这些服务会相互协作,或者可能会与其他领域的服务进行协作。

不管是设计哪种系统,这种总览类型的领域模型都是不可或缺的第一步,但是在构建微服务时,这一步就显得至关重要。如果不解业务领域,设计人员就可能在划定服务边界时做出错误的决策。没有人希望自己所构建的服务是贫血的——只是执行些琐碎的增删改查(CRUD)操作。这些贫血的服务通常是导致系统内部耦合严重的源头之一。同时,我们要避免将太多的责任放到一个服务中,低内聚的服务会使得修改软件时效率更低,风险更大——而这恰恰是我们试图避免的。

最后,如果没有这种判断能力,开发者就可能成为过度工程化的牺牲品,他可能是盲目地选择了微服务的架构方案,而并不是以“产品或者业务领域的实际复杂性”为依据。

SimpleBank的工程师们相信微服务是他们最好的选择,相信微服务可以帮助他们解决业务领域的复杂性问题,也相信微服务可以让他们在面对复杂且不断变化的需求时保持灵活。他们期望随着业务规模的扩大,微服务可以降低每次软件变更的风险,从而让产品更加出色,让客户更加满意。

比如,工程师们需要处理每次购买或者出售的交易记录来计算税务结果,但是不同国家的税务规则各不相同,并且这些规则还会频繁地变更。在单体应用中,即便工程师只是想对某一个国家的规则进行调整,都需要整个平台配合在规定的时间内发布。在微服务应用中,工程师可以开发一组自治的税务处理服务(不论是按国家、税收类型,还是账户类型进行划分),然后独立地部署这些修改。

SimpleBank公司的选择是否正确?软件架构设计总是牵涉到现实主义和理想主义的矛盾和冲突——要在产品需要、发展压力、团队能力这些方面进行平衡。错误的选择并不会立刻显现出后果。在选择微服务架构时所要考虑的影响因素如表2.1所示。

表2.1 选择微服务架构时所要考虑的影响因素

影 响 因 素

影  响

业务领域复杂度

客观评估业务领域的复杂度是件很困难的事情,但是微服务能够解决受竞争压力所影响的系统复杂性问题,比如监管需求和市场范围

技术需求

开发者可以使用不同的编程语言(以及对应的技术生态)来开发系统的不同组件。微服务使得技术选型更具多样化

组织成长

快速成长的工程团队能够受益于微服务架构,因为在微服务架构下,对于已有代码库的依赖更小,新工程师可以快速得到提升,工作得更加高效

团队知识

许多工程师缺乏微服务和分布式系统的经验。如果团队缺少自信或者这方面的知识,最好在完全承诺实现之前,先构建一个用于概念验证的微服务

借助这些影响因素,开发者可以评估一下微服务架构是否能够帮开发者在面对越来越高的应用复杂度时持续地交付价值。

我们花一些时间来了解一下SimpleBank公司的竞争对手们开发软件的方式。在技术创新方面,大部分银行并不会走在前端。这其中或多或少有惰性的因素,这也是大型机构的典型特征,毕竟这并不是金融行业所特有的。有两大因素限制了创新和灵活性。

(1)厌恶风险——金融公司都是受到严格监管的,并且倾向于构建一套自上而下的变更控制系统,通过限制软件变更的频率和影响范围来避免风险。

(2)对复杂的遗留系统的依赖——大部分核心银行系统都是20世纪70年代以前开发的。此外,合并、收购和外包也是使得软件系统并没有被很好地集成,存在大量的技术债务。

但是限制变更和依赖于已有的系统并不能阻止软件问题影响到客户或者金融公司自身。2014年,英国的苏格兰皇家银行由于一次故障导致650万顾客支付出现问题,最终被罚款500万英镑。这还并不包括在它每年已经在IT系统上花费的2.5亿英镑之内。[1]

这种方法并不能使产品变得更好。反而一些像Monzo和Transferwise这样的金融技术初创企业,在以许多银行望尘莫及的速度开发着新的功能。

我们可以做得更好吗?不管用什么方式来衡量,银行业都是一个复杂和竞争激烈的领域。尽管银行系统的生命周期都是以几十年来算的,但是仍然需要同时具备可恢复性和敏捷性。单体应用的规模不断增大是有悖于这个目标的。如果银行想要启动一个新产品,之前构建的那些系统不应该成为阻碍[2],新产品也不应该要求通过超标的工作量和投入来避免已有功能的退化。

设计良好的微服务架构可以解决这些问题。正如我们前面确定的那样,微服务架构类型避开了很多单体应用中存在的“会减缓开发速度”的特质。每个团队都可以循着下面的方式更加自信地前进。

(1)解除变更周期与其他团队之间的耦合。

(2)相互协作的组件之间的交互是受规范约束的。

(3)持续交付小的、单独的变更,以控制功能受到破坏的风险。

这些因素能够减少开发复杂系统过程中的摩擦,但又不影响可恢复性。同样,这些因素在不通过官僚主义扼杀创新的情况下能够降低风险。

这不仅是一个短期的解决方案。微服务还能够帮助技术团队在应用的整个生命周期中持续交付价值,而做到这一点所依赖的就是为每个组件的概念和实现复杂性上设定自然边界。

既然我们已经确定微服务是SimpleBank公司的正确选择,接下来看一下如何使用微服务来开发新功能。为了确保团队理解了微服务风格的要求和约束,开发一个最小可行产品(Minimum Viable Product,MVP)是非常重要的第一步。我们会从SimpleBank要开发的一个新功能着手,并研究团队所做出的设计决策。整个生命周期曾在第1章中展示过(图2.2)。

在第1章中,我们曾提及服务如何相互协作来提交一个出售订单。这个过程如图2.3所示。

我们看一下如何开发这个功能。开发者需要回答下列3个问题。

(1)需要开发哪些服务。

(2)这些服务之间彼此如何合作。

(3)如何将功能公开出去。

图2.2 微服务开发生命周期的三大关键迭代阶段——设计、部署和监测

图2.3 SimpleBank公司为账户下单出售股票的流程

在为单体应用设计新功能时,开发者可能也会问自己类似的问题,但是它们的意义并不同。比如,部署一个新服务所需要花费的力气要比创建一个新的模块更大。在划定微服务范围时,开发者需要确保拆分系统所增加的复杂度不会超过所带来的益处。


注意 

随着应用的不断演进,这些问题也会呈现出更多的维度。之后,我们还会问,是将新增的功能添加到已有的服务,还是把这些服务拆分开?我们会在第4章和第5章进一步讨论。


正如我们前面所讨论的那样,每个服务应该只负责一个功能。那么第一步就是确定需要实现的不同业务功能以及它们之间的相互关系。

为了确定所需要的业务功能,开发者需要提高对所开发软件的业务领域的了解程度。这通常是产品发掘或者业务分析中最难的工作:调查研究,原型设计,与客户、同事或其他终端用户进行访谈等。

我们开始研究图2.3所示的下单的例子。所要交付的价值是什么?从上层讲,客户想要能够提交订单。所以一个很明显的业务功能就是存储和管理这些订单状态。这是开发者第一个候选的微服务。

继续深入研究一下这个例子,开发者会发现一些应用需要提供的其他功能。为了能够出售股票,客户首先需要拥有它,所以开发者需要通过某些方式来展示客户当前持有的股份。这些股份数据是由发生在他们账户中的交易记录所产生的。开发者的服务需要向一个代理发送订单——这个应用需要能够和第三方系统进行交互。事实上,发布订单这一功能需要SimpleBank公司的应用支持如下的所有功能。

(1)记录出售订单的状态和历史。

(2)向客户收取下单的手续费。

(3)在客户的账户记录交易信息。

(4)将订单提交到市场上。

(5)向客户提供所持股份和订单的价值信息。

并不是要绝对化地将每个功能都映射为单个微服务。开发者需要判断哪些功能关系紧密——它们要放在一起,比如,来自于订单的交易结果和来自于其他事件(如股票支付股息)的交易结果是类似的。将一组功能组合到一起,就形成了一个服务所要提供的能力。

我们来将这些功能映射到业务能力——该业务所做的工作。映射关系如图2.4所示。有些功能会跨多个领域,比如收取手续费的功能。

图2.4 应用功能与SimpleBank公司的业务能力的对应关系

开发者在开始时可以直接将这些能力映射到微服务。每个服务应该体现业务所提供的能力,这样也就能实现体积和职责的平衡。开发者还应该思考一下有哪些推动微服务在未来进行变化的因素——它是不是真的只有单一职责。比如,开发者可能认为市场操作是订单管理的子集,因此不应该分成两个服务。但是市场操作这个领域变化的驱动因素是所支持的市场的功能和范围,而与订单管理关联更紧密的是产品的类型以及进行交易的账户。这两个领域并不会同时变化。将这两块分开后,就区分了变化范围并且能最大限度地提升内聚性(图2.5)。

图2.5 服务应该通过区分修改原因来推动松耦合和单一职责

一些微服务实践者会认为微服务更应该反映单个功能,而不是单个能力。有些人甚至认为微服务是“只能追加”的,他们认为开发新的服务永远好于将功能添加到已有服务中。

我们并不赞同他们的说法。分解过细一方面会导致服务本身缺乏内聚,另一方面也会导致那些关联比较紧密的协作服务之间耦合过紧。同样,部署和监控太多的服务也会超出处于微服务实践初期的工程团队的工作能力。一条有用的经验就是宁可选择较大一些的服务,等功能变得更加特殊或者更加明确属于一个独立的服务时,再将功能从中拆分出去,这样会容易很多。

最后,牢记,了解业务领域并不是一蹴而就的过程!随着时间的推进,需要持续反复地去了解业务领域;用户的需求会变,产品也需要持续演进。随着对业务领域的了解的变化,系统本身也需要改变来满足这些要求。幸运的是,正如我们在第1章讨论的,应对不断变化的需求是微服务方法的一个优势。

我们已经确定了一些候选的微服务。这些服务需要相互协作才能为SimpleBank的客户实现一些有用的功能。

正如读者可能已经了解的,服务协作可以是点到点方式的,也可以是事件驱动方式的。点到点的通信通常是同步的,而事件驱动的通信通常是异步的。许多微服务应用起初使用的都是同步通信方式。之所以这么做,有如下两方面的原因。

(1)同步调用通常要比异步通信更加简单而且更便于排查分析。即便如此,也不要错误地认为它们和本地的进程内的函数调用有同样的特性——跨网络的请求明显要慢很多而且更加不可靠。

(2)即便不是所有编程环境,至少大部分都已经支持一种简单、与语言无关而又在开发者中有广泛认知度的传输机制:HTTP。HTTP主要用于同步调用,但是也可以将其用于异步调用。

考虑一下SimpleBank公司的下单流程。order服务负责记录订单并将订单提交到市场。为此,它需要与market服务、fee服务和account transaction服务进行交互。这些交互协作如图2.6所示。

图2.6 order服务控制其他服务的动作以将订单提交到市场

在前面,我们曾指出微服务应该是自治的,而为了实现自治,服务应该是松耦合的。要做到这一点,一部分要靠对服务的设计,通过“将那些因为相同原因而修改的内容聚合到一起”来尽可能降低对服务的改动需要同时修改上游或下游协作方的可能性。此外,开发者还需要考虑服务契约服务职责

1.服务契约

每个服务所接收的消息以及它返回的响应构成了服务与依赖该服务的上游协作服务之间的契约。契约使得每个服务可以被它的协作方当作黑盒对待:发送一个请求,然后收到返回的某些结果。如果这其中没有错误发生,那么这个服务就是在做它该做的事情。

虽然一个服务的实现会随着时间而变化,但是维持契约层面的兼容性能够保证两件事:这些变化不大会破坏消费者的使用;服务之间的依赖是可明确识别和可管理的。

根据我们的经验,契约通常隐含在微服务实现的早期或者初期。它们通常是通过文档或者惯例来体现的,而非显式地编纂成规范。随着服务数量的不断增多,开发者会意识到以机器可读的格式将服务交互的接口标准化的显著好处,比如,REST API可以使用Swagger/OpenAPI。同样,为每个服务加强一致性测试和发布标准化的契约,能够帮助组织机构中的工程师了解如何使用这些服务。

2.服务职责

开发者在图2.6中会注意到order服务有许多职责。它直接操控下单流程所涉及的每个服务的动作。从概念上讲这很简单,但是也有不利的一面。最差的情况下,那些被调用的服务会变成“贫血式”的服务,大部分傻瓜式的服务被少数的聪明服务所控制,而这些聪明的服务越来越大。

这种方式会导致服务间耦合越来越紧。如果开发者想要在下单流程中引入新的内容——比如想要在下的订单额度比较大时,通知客户的账户经理——开发者就必须将这部分修改部署到order服务中。这增加了修改的代价。理论上,订单服务不需要同步确认这一操作的结果——只要它收到了一个请求——它不需要了解下游的具体操作。

在微服务应用中,服务自然会有不同层次的职责。但是开发者应该在编排(choreography)和编配(orchestration)之间进行平衡。在编排式的系统中,服务不需要直接向其他服务发送命令和触发操作。相反,每个服务拥有特殊的职责,也就是对某些事件进行响应和执行操作。

我们重新看一下之前的设计并做些调整。

(1)当有人创建订单时,可能还没开市,所以开发者需要记下订单的状态:创建成功,还是提交成功。订单提交发布的步骤并不需要同步。

(2)只有订单提交成功,才会收取手续费,所以收取手续费的步骤也不需要同步。实际上,应该是响应market服务进而执行收费操作,而不是由order服务来安排收费。

修改后的设计方案如图2.7所示。事件的增加也相应地增加了架构上要关注的内容:开发者需要采取某些方法来保存这些事件并将它们开放给其他系统。为此,我们建议使用RabbitMQ或SQS这样的消息队列。

在这个设计方案中,我们从order服务中移除了如下职责。

(1)收费——order服务并不知道订单提交到市场以后还需要收费。

(2)下单——order服务并不直接与market服务进行交互。这样,开发者可以很容易地用一种不同的实现方案进行替换,甚至于都可以改成每个市场对应一个服务,而不需要对order服务进行任何修改。

图2.7 通过事件来对各个服务的功能进行编排,弱化order服务的协调者角色。
请注意,某些操作是并行的,比如图中两个编号为3的操作

order服务自身也需要通过订阅market服务发送的OrderPlaced事件来对其他服务的动作进行反应。开发者可以很容易地对其扩展来满足未来的需求,比如order服务可以订阅TradeExecuted事件来记录市场上这笔交易完成的时间,如果这笔交易没能在指定的时间内成交,order服务也可以订阅OrderExpired事件。

这一方案要比之前的同步协作的方案复杂很多,但是,在尽可能的情况下,采用这种编排的方案所开发的服务相互之间都是解耦的,相应地,也就可以更独立地部署这些服务,修改这些服务也更容易。但是有得就有失,我们也要付出一些代价:基础设施的队伍中又多了一个消息队列,我们需要持续地对其进行管理和扩容,并且它会成为一个单点故障源。

我们提出的这一设计方案还可以提高系统的可恢复性,比如,market服务出现的故障与order服务的故障是相互隔离的。如果订单发布失败,则可以晚一些等market服务恢复正常以后重新发送这个事件[3];如果发送的次数过多,则可以直接终止。此外,采用这一方案会使得对系统的整个活动轨迹的跟踪变得困难很多,在考虑如何在生产环境监控这些服务时,工程师要考虑到这一点。

到现在为止,我们已经讨论了如何通过服务协作来实现业务目标。那么,我们如何将这些功能开放给真正的用户应用呢?

SimpleBank公司希望同时开发网页端和移动端两个产品。为此,技术团队决定开发一个API网关,将其作为底层各个服务对外的门面。这个网关会将各种各样的后端问题抽象化,这样,这些前端应用既不再需要了解底层的这些微服务的存在,也不需要了解为了完成各项功能这些微服务相互之间的交互方式。API网关会作为代理接管那些发给底层服务的请求,然后将底层服务的响应结果根据公开API的需要转换或者合并成新的数据格式。

设想一下用于发布订单的用户操作界面,它有以下四大关键功能。

(1)展示客户的账户中当前持有股份的信息,包括数量和价值。

(2)展示市场数据:股票的价格和市场变动情况。

(3)订单录入,包括成本计算。

(4)请求对指定的股票执行这些订单。

API网关提供下单功能以及与底层服务进行协作的过程如图2.8所示。

图2.8 用户界面(如网页端和移动端App)与API网关暴露的REST API进行交互。网关为底层的微服务提供了一个统一的对外门面并将请求代理给对应的后端服务

API网关模式是一种很简洁的方式,但是它也有一些缺点,因为作为众多底层服务唯一的组合对外入口,它会变得越来越大,而且可能会越来越笨重。它会诱使开发者将业务逻辑添加到网关中,而非仅仅将其作为一个代理来对待。它试图成为无所不能的可用于所有应用的服务,却又饱受其苦,毕竟不同应用的需求各有不同:移动客户端应用希望返回的数据体积更小、更精简,网页版的内部管理系统所需要的数据却又多得多。在开发高度内聚的API时,开发者要同时平衡这些相互冲突是非常困难的。


注意 

我们会在第3章对API网关模式进行回顾,并讨论一些其他的可选方案。


现在,开发者为SimpleBank公司设计了一个功能,这个功能涉及多个服务之间的交互、事件队列以及API网关。假设开发者已经将这些服务开发了出来,现在CEO要求开发者将它们发布到生产环境中。

在AWS、Azure或GCE这样的公有云中,一种显而易见的方案就是将每个服务部署到一组虚拟机中。开发者可以使用负载均衡器来将请求均匀地分摊到每个网络服务的实例中,或者开发者可以使用托管的事件队列服务(如AWS的Simple Queue Service)来让各个服务互相分发事件消息。


注意 

深度探讨高效的基础设施管理和自动化不在本书涵盖范畴之内。大部分云服务提供商会通过定制的工具来提供这一功能,如AWS的Formation或Elastic Beanstalk。开发者也可以考虑一些开源工具,如Chef和Terraform。


不管怎样,开发者编译代码、通过FTP将应用上传到虚拟机、启动数据库并正常运行,最后发送一些请求测试一下。等做完这一切,已经好几天过去了。生产环境如图2.9所示。

过了几个星期,服务运行还算正常。开发者修改了一些代码,然后提交了这部分新代码,但是很快就遇到了麻烦。很难说清楚服务运行是否符合预期。更坏的情况是,开发者是SimpleBank公司唯一了解如何发布新版本的人,比这还糟糕的情况是,负责transaction服务的那个同事休了几周的长假,而没有其他人知道如何部署这个服务。那么这些服务的巴士系数(bus factor)就是1——这表明任何团队成员的离开都会导致这些服务无法继续存活。


定义

巴士系数[4]是用来衡量由团队成员之间的知识未被分享所造成的风险大小的指标,源自于“以防他们被巴士撞了”这句话,也被称作货车系数(truck factor)。巴士系数越低,团队的风险越大。


图2.9 在简单的微服务部署方案中,每个服务的请求都通过负载均衡器分发到不同的实例——这些实例运行在不同的虚拟机中。同样,服务的多个实例都订阅了事件队列

这一定是出了什么问题。开发者应该记得自己在上一家GiantBank公司的工作。在上一家公司中,基础设施团队负责发布管理。开发者提交一个申请上线的工单,经过几次来来回回的争论,几个星期后,开发者如愿以偿地上线,或者有时候并不如愿,所以又重新提交了一个工单。这同样看起来并不是正确的方式。事实上,开发者对于微服务方案能让他自己管理部署的工作很欣慰。

稳妥点说,这些服务并没有做好发布到生产环境中的准备。运行微服务需要工程师团队具备一定水平的运维意识和运维成熟度,这是高于单体应用通常所要求的能力水平的。只有在开发者完全自信自己的服务能够处理生产环境的流量压力,才可以说这个服务是生产就绪的。

开发者如何确信服务是值得信任的呢?为了实现生产就绪,开发者需要考虑下列几个问题。

(1)可靠性——服务是否可用且没有错误呢?开发者可以依靠其部署流程来上线新功能而不引入缺陷或者导致服务不可靠吗?

(2)可扩展性——开发者了解服务所需要的资源和容量吗?如何在负载下保持响应能力呢?

(3)透明性——开发者是否可以通过日志或者数据指标来观测运行中的服务呢?

(4)容错性——开发者是否解决了单点故障的风险?如何应对所依赖的其他服务出现的故障?

在微服务生命周期的初期,开发者需要确立这三大基本准则:高质量的自动化部署、可恢复性和透明性。

接下来,我们来调查一下这三大基本准则对开发者解决SimpleBank公司所遇到的问题有何帮助。

如果开发者不能可靠且快速地将微服务发布到生产环境中,就是对采用微服务所提升的开发速度的极大浪费。不稳定的部署所造成的痛苦(如引入严重的错误)会完全抵消速度提高所收获的收益。

传统的组织机构通常会通过引入比较官方的变更控制和审批流程来谋求稳定性。这些流程的目的是管理和限制变更。这并不是盲目的冲动行事:如果这些变更引入的大部分bug[5]动辄会给公司造成成千上万乃至数百万的工程投入或者收入上的损失,那么开发者就应该严格控制这些变更。

在微服务架构中,这种方式就不可行了,因为整个系统处于一个持续演进的状态中——正是这种自由带来了实实在在的创新。但是为了确保这种自由不会导致系统出现错误或者不可用,开发者就需要能够做到充分信任开发流程和部署工作。同样,为了使这种自由处于第一优先级,开发者还需要尽可能地减少发布一个新服务或者修改一个已有服务所需要投入的工作。我们可以通过标准化和自动化来实现稳定性的目标。

(1)将开发过程标准化。开发者应该评审代码的改动、编写对应的测试代码以及维护源代码的版本控制。但愿没有人对此要求表示意外或感到奇怪。

(2)将部署过程标准化和自动化。开发者应该彻底地验证所要提交到生产环境的代码变更,且要保证部署过程不需要工程师的介入,也就是说,要做成部署流水线。

想要确保软件系统在面对故障时是可恢复的,这是一项很复杂的任务。系统之下的基础设施本来就是不可靠的,即便代码是完美无缺的,网络调用也会失败,服务器也会宕机。作为服务设计的一部分,开发者需要思考服务本身以及服务的依赖项会怎样出现故障,然后提前做些工作来避免这些故障场景或者尽可能降低这些故障的影响。

表2.2列出了SimpleBank公司所部署的系统的潜在风险领域。开发者可以注意到,即便是一个相对简单的微服务应用,也会引入不同领域的潜在风险和复杂性。

表2.2 SimpleBank公司的微服务应用的风险领域

领域

可能发生的故障

硬件

主机、数据中心组件、物理网络

服务间通信

网络、防火墙、DNS错误

依赖

超时、外部依赖、内部故障,比如数据库


注意 

我们将在第6章中研究一些提高服务可恢复性的技术。


微服务的行为和状态应该是可观测的。不管在什么时候,开发者都应该能够判断服务是否健康以及处理请求是否符合预期。如果某些内容影响到了某个关键指标——比如,订单提交到市场的时间过长——那么系统应该向工程师团队发送告警——这个告警需要有足够的理由才能发送。

我们以一个例子进行说明。SimpleBank公司曾出现过一次故障。有位客户打电话说她不能提交订单了。经过快速检查之后,工程师发现这个问题已经影响到所有客户了:所有发给order服务进行订单创建的请求都超时了。该服务可能的故障点如图2.10所示。

图2.10 有很多底层原因会导致服务超时:网络问题、服务内部依赖的问题(如数据库) 以及其他服务不正常的操作

显然,开发者有一个很大的操作问题:缺乏用来判断到底是谁出错了以及具体哪里出错了的日志。所以,开发者只能通过手工测试进行验证,最终成功地将问题排查了出来:account transaction服务没有响应。而这时,客户已经好几个小时不能下单了,他们非常生气。

为了避免未来发生这种问题,开发者需要为微服务添加一套全面的工具。收集应用各个层面的活动数据对于了解微服务应用当前以及过去的运行表现是至关重要的。

首先,SimpleBank公司要搭建一套对微服务产生的基础日志进行聚合的基础系统,同时还要将这些数据发送到某个服务便于开发者进行查询和标记[6]。该方案如图2.11所示。有了该方案,下次有服务出现故障时,工程师团队就可以使用这些日志来确定系统哪里出现了故障,并进一步排查出问题发生的具体位置和情况。

图2.11 开发者为每台虚拟机安装一个日志收集的代理应用。这个应用会将应用日志数据传到一个中心化的日志仓库中。开发者可以为日志创建索引、搜索,并能对其做进一步分析

但是,日志记录不充分还不是唯一的问题。令人尴尬的是,SimpleBank公司在客户打电话投诉以后才发现了问题。公司还应该有一套告警方案来确保所有服务都符合响应要求和服务目标。

在这种场景中,最简单的方式就是,开发者应该有一套作用于每个微服务的反复执行的心跳检测机制,一旦服务完全没有响应,心跳检测就可以向团队发送告警。除此之外,团队还应该对每个服务做出服务保证的承诺,比如,对于关键服务,开发者会保证在99.99%的可用性基础上,95%的请求能够在100毫秒内返回。如果没有达到这些阈值,就应该向服务所有者发送告警。

为微服务应用开发完整的监控系统是一项很复杂的任务。开发者所采用的监控深入度会随着服务的数量和复杂度的提升而演化。除了我们所介绍的运维指标和日志,一套成熟的微服务监控方案还会处理业务指标、服务间链路追踪和基础设施指标。如果开发者想要信任自己的服务,就需要不断地研究这些数据的含义。


注意 

在本书第四部分,我们会详细讨论监控方面的内容,还会介绍如何用Prometheus之类的微服务工具来触发告警以及如何搭建一套健康监控仪表盘。


微服务的技术灵活性对开发速度以及系统的有效扩展而言是一种恩赐。但是这种灵活性也给组织机构带来了挑战;从根本上改变大规模的技术团队的工作方式成为一个难题。开发者很快就会遇到两大挑战:技术分歧孤立

设想一下,SimpleBank公司已经开发了一个有1000个服务的大型微服务系统。每个小团队负责一个服务,他们各自使用自己擅长的语言、自己最爱的工具、自己拥有的部署脚本、自己认同的设计原则、自己喜欢的外部类库[7],等等。

维护和支持这么多不同方案所付出的努力是相当巨大和令人恐惧的,我们要对此表示拒绝。虽然微服务使得不同的服务可以选择不同的语言和框架,但是我们也很容易明白,不选择一套合理的标准和限制,系统会杂乱和脆弱得难以想象。

很容易注意到,这种因没有统一规范而导致的挫折在规模较小的系统中就已经出现了。考虑下这两个服务——account transaction服务和order服务——它们分别是由两个不同的团队负责的。account transaction服务会为每个请求会生成结构良好的日志输出,其中包含有用的诊断信息,诸如计时、请求ID以及当前发布的修订ID。

service=api
git_commit=d670460b4b4aece5915caf5c68d12f560a9fe3e4
request_id=55f10e07-ec6c
request_ip=1.2.3.4
request_path=/users
response_status=500
error_id=a323-da321
parameters={ id: 1 }
user_id=123
timing_total_ms=223

第二个服务生成的则是难以解析的纯收文本消息:

Processed /users in 223ms with response 500

开发者会注意到,即便是最简单的日志消息格式,一致性和标准化也能够让在不同服务之间进行问题诊断和请求跟踪更加容易。通过在微服务系统的所有层次上达成一种合理的标准来解决分歧和杂乱扩展的问题是至关重要的。

在第1章中,我们提到了康威法则。在采用微服务方式的组织机构中,逆康威法则也是成立的:公司的结构是由产品的架构决定的。

这表明,开发团队会越来越趋向于微服务:他们会高度专业化地完成一件工作。每个团队只拥有或者负责少数几个关系密切的服务。总的来说,开发者将知道有关系统的所有信息。但是具体到每个开发者,他们只熟悉一个狭窄的专业领域。随着SimpleBank的客户基数以及产品复杂度的增加,这种专业化会进一步加深。

这种配置会成为极大的挑战。微服务本身的价值有限,不能孤立地发挥作用。因此,这些独立的团队必须紧密协作来构建无缝运行的应用程序,即使他们作为一个团队的整体的目标可能只与他们自己负责的更窄领域有关。同样,这种关注的狭窄性会使得团队容易只对他们本地局部的问题和参数设置进行优化,而非考虑整个组织机构的需求。极端情况下,这会导致团队之间发生冲突,进而降低部署速度以及产品的可靠性。

在本章中,我们确认微服务非常适合SimpleBank公司,设计了一个新的功能,并思考了如何让这一功能实现生产就绪。我们希望这个学习示例展示了微服务驱动的应用开发的迷人和挑战之处。

在本书的后续章节中,我们会为开发者介绍一些运行微服务应用所需要的技术和工具。虽然微服务能够提高开发的灵活性和生产力,但是运行多个分布式服务的要求比运行单个应用严格得多。为了避免服务变得不稳定,开发者需要能够设计和部署“生产就绪”的服务,即确保这些服务是透明性、容错的、可靠的和可扩展的。

在第二部分中,我们会关注于服务设计。有效地设计一套由分布式的、独立的服务组成的系统,需要仔细思考系统的业务领域以及这些服务之间的交互方式。能够识别职责间的正确边界——由此开发出高内聚低耦合的服务——是任何微服务从业者最有价值的技能之一。

(1)微服务非常适合于有多维复杂性的系统,比如产品的供应范围、全球部署和监管压力。

(2)在设计微服务时,了解产品业务领域是至关重要的。

(3)服务交互可以是编配型的,也可以是编排型的。后者会增加系统复杂度,但是能够降低系统中服务之间的耦合度。

(4)API网关是一种常见的模式,它将微服务架构的复杂性进行了封装和抽象,所以前端或者外部消费者不需要考虑这部分复杂度。

(5)如果开发者充分信任自己的服务能够处理生产环境上的流量压力,就可以说这个服务是生产就绪的。

(6)如果开发者可以可靠地部署和监控某个服务,就可以对这个服务更有信心。

(7)服务监控应该包括日志聚合以及服务层次的健康检查。

(8)微服务会因硬件、通信以及依赖项等原因而出现故障,并不是只有代码中的缺陷才会导致故障发生。

(9)收集业务指标、日志以及服务间的链路跟踪记录对于了解微服务应用当前和过去的运行表现是至关重要的。

(10)随着微服务以及支持团队的数量的不断增加,技术分歧以及孤立会日渐成为技术团队的挑战。

(11)避免技术分歧和孤立需要在不同团队间采用相似的标准和最佳实践,不管采用何种技术基础。

[1] 详见肖恩·法瑞尔(Sean Farrell)和卡门·斐西维克(Carmen Fishwick)于2017年6月17日在英国《卫报》(The Guardian)上发表的RBS could take until weekend to make 600,000 missing payments after glitch以及查德·布雷(Chad Bray)于2014年11月20日在纽约时报发表的Royal Bank of Scotland Fined $88 Million Over Technology Failure

[2] 情况能有多糟?我曾经见过一家金融软件公司,他们维护了10个庞大的代码库,每个代码库中的代码超过200万行。

[3] 假设队列本身是持久化的。

[4] 巴士系数,团队中有最少多少人同时消失,开发者的项目就注定失败,巴士系数是软件开发中关于软件项目成员之间信息与能力集中、未被共享的衡量指标,也有些人将其称作“货车因子”/“卡车因子”(lottery factor/truck factor)。一个项目至少失去若干关键成员的参与(“被巴士撞了”,指代职业和生活方式变动、婚育、意外伤亡等任意导致缺席的缘由),即导致项目陷入混乱、瘫痪而无法存续时,这些成员的数量即为巴士系数。

[5] “SRE发现大于70%左右的服务不可用都是由于对生产系统的修改导致的”。本杰明· 特雷诺· 斯洛斯(Benjamin Treynor Sloss)所写的Site Reliability Engineering中的第1章。

[6] 行业内有一些用于日志聚合的托管服务,包括Loggly、Splunk和Sumo Logic。开发者还可以使用众所周知的ELK(Elasticsearch、Logstash和Kibana)工具栈来在内网运行这一功能。

[7] 不幸的是,尽管严格的组件边界和显式的服务所有权会加剧这个问题,但这不是只在微服务中才会出现的问题。在我早期的职业生涯中,我见过一个Ruby项目,它使用了6个不同的HTTP客户端库!


相关图书

微服务之道
微服务之道
Istio实战指南
Istio实战指南
微服务实践
微服务实践
Spring微服务实战
Spring微服务实战
Git高手之路
Git高手之路
深入理解Spring Cloud与微服务构建
深入理解Spring Cloud与微服务构建

相关文章

相关课程