微服务概念解析(下)

在上篇中我们讲到了微服务的几个架构特性,包括通过服务实现组件化、以业务功能为核心进行组织、产品而非项目、智能化端点与傻瓜式流程,在今天的微服务概念解析下篇中,我们将继续讲述微服务的特性,具体分析它的离散化治理、离散化数据管理、基础设施自动化、故障应对设计以及演进设计,并理性思考微服务作为一项新兴的技术成果,是否能够代表未来。 ​

离散化治理

聚合型治理的一大影响在于使得单一技术平台上出现标准化趋势。经验表明这类方案具备收缩特性——意味着各个实际问题并不能够轻松与解决方案对应起来。我们更倾向于使用正确的工具执行正确的任务,而且虽然部分整体应用程序能够发挥不同编程语言的独特优势,但这种情况并不常见。

微服务与SOA

当我们探讨微服务时,经常出现的问题就是其到底是不是我们十年前就听说过的面向服务架构(简称SOA)的另一种表现形式?二者之间确实存在一定联系,因为微服务风格拥有与SOA相似的逻辑主张。然而问题在于,SOA的实际含义太过广泛,而且当我们提到所谓“SOA”时,实际所指的对象往往跟这里提到的微服务概念差之千里——具体来讲,其通常代表那些专注于利用ESB实现的集成化整体应用程序。

值得强调的是,我们也见证了大量表现糟糕的面向服务实现手段——从将复杂性隐藏在ESB当中[7]的作法,到投入多年以及数百万资金却毫无成效的尝试,再到以聚合型治理模式抑制变更,我们几乎看不到面向服务架构能够带来什么显著的积极影响。

诚然,微服务社区当中使用的不少技术成果都源自开发人员在大型企业当中积累到的集成化服务成果。Tolerant Reader模式正是其中的典型代表。对Web的运用确实带来可观回报,而使用简单协议正是经验积累的直接产物——这显然是为了解决标准汇聚所导致的高复杂性难题(无论何时,如果大家需要利用一种实体来管理其它实体,那么就意味着各位已经面临着大麻烦)。

SOA的这些弊端导致一部分微服务布道者很讨厌人们把SOA的标签加在微服务头上——尽管也有一些人认为微服务正是SOA的一种实现方式[8],或者说我们可以将微服务称为“面向服务的正确实现”。无论如何,事实上SOA含义的宽泛性意味着其更适合作为一种用于定义架构风格的术语,而非具体解决方案。

通过将整体应用程序的各组件拆分成服务,我们能够对各服务进行分别构建。各位可能希望利用Node.js建立一套简单报告页面?照此办理即可。打算利用C++构建特定的近实时组件?没问题。打算利用不同类型的数据库以匹配单一组件的读取行为?目前的技术方案已经能够实现这种独立重构需求。

当然,我们能够实现以上目标,并不代表我们必须这么做——但对系统进行拆分意味着大家能够拥有更多备用选项。

采用微服务架构的团队倾向于以不同的方式实现所谓标准。相较于以往编写一整套定义标准集的作法,他们更乐于开发实用工具并交付给其他开发人员,从而利用其解决自身面临的类似问题。这些工具通常能够在更为广泛的层面得到实现与共享,但同时又不至于转化为排他性内部开源模式。现在git与github都已经成为客观层面的版本控制系统选项,而开源实践也越来越多地成为内部环境中的常见组成部分。

Netflix公司就是个很好的例子,他们遵循的正是这样一种理念。将具备实用性且经过严格考验的代码作为库,并鼓励其他开发人员利用其以类似的方式解决的类似的问题,这就为各团队成员在必要时选择其它工具保留了空间。共享式库专注于数据存储、进程间通信以及我们在后文中将要探讨的基础设施自动化等问题的解决。

对于微服务社区而言,资源成本显然是种不受欢迎的因素。这并不是说该社区不承认服务协议的价值。恰恰相反,这是因为他们希望构建起大量服务协议。他们希望能够采用多种完全不同的方式对这些协议进行管理。像Tolerant Reader以及Consumer-Driven Contacts这样的模式在微服务架构中非常常见。这些服务协议也各自以独立方式不断演进。将消费者驱动型协议作为构建工作组成部分的作法能够显著增强参与者信心,同时快速获取服务功能能否确切实现的反馈意见。事实上,澳大利亚的某个团队就在积极利用消费者驱动型协议进行新服务构建。他们使用的简单工具确保其能够针对单一服务实现协议定义。其甚至在面向新服务的代码被编写出来之前就已经成为自动化构建流程中的一部分。这意味着服务只有在切实满足该协议要求的前提下才能够实现构建——这就有效解决了构建新软件时经常出现的“YAGNI”[9]难题。这些技术与工具成果围绕协议而生,并通过降低不同服务间的耦合性限制了其对中央协议管理机制的依赖。

多种语言,多种选项

JVM作为平台的快速发展已经成为多种语言混成于单一通用平台内的最新明证。这种作法已经成为一类常见实践,旨在充分发挥高级语言在过去数十年中发展所实现的种种高级抽象优势。其甚至以涓滴效应影响到裸机以及通过低级语言编写的性能敏感型代码。然而,众多整体应用程序并不需要这种级别的性能优化效果,亦非常见的DSL与高级别抽象开发成果。相反,整体应用程序往往使用单一语言,这也严重限制了其能够使用的技术手段。[10]

也许离散化治理的人气正是源自Amazon方面提出的“谁构建,谁运行”原则。各团队需要为其构建的软件的各个方面承担责任,包括为软件提供24/7全天候运维支持。这种程度的责任下放当然还没有成为常态,不过我们已经看到越来越多的企业开始将责任交付至开发团队。Netflix公司亦是另一家采取这种理念[11]的企业。为了不至于在凌晨三点被紧急来电叫醒,开发人员们当然会全力以赴提升所编写代码的质量水平。这些思路与传统的集中化治理模式明显相去甚远。

离散化数据管理

数据管理离散化拥有多种不同的表现形式。从最为抽象的级别来看,这意味着全局概念模型将在不同系统之间有所区别。这种问题常见于解决方案在大型企业当中的部署,毕竟销售团队对于客户概念的理解方式必须不同于技术支持团队的理解方式。被销售人员视为客户的对象也许根本不会出现的技术支持团队的视野当中。不同属性甚至是相同属性的不同理解方式都可能在语义层面产生细微的差异。

实践性规范与执行标准

这种态度实际有点二分法的意味:微服务团队倾向于回避由企业架构部门制定的硬性执行标准,但却乐于使用甚至积极推广HTTP、ATOM以及其它微格式开放标准。

二者之间的本质区别在于标准的开发方式以及执行方式。由IETF等组织管理的标准只会在得到广泛采用之后才能真正成为业界规范,而且其往往脱胎自成功的开源项目。

这些标准拥有与商业世界完全不同的立场与定位——事实上,商业标准的制定工作往往由那些几乎不具备编程经验的团队所负责,或者受到具体厂商的过度影响。

这一问题通常出现在不同应用程序之间甚至是应用程序之内,特别是在将应用程序拆分为多个独立组件的情况下。解决问题的一类可行思路在于基于背景边界化的区域驱动型设计(简称DDD)方案。DDD机制将一个复杂的区域拆分成多个具备边界的背景单元,并对各单元之间的关系加以映射。这种方式同时适用于整体与微服务架构,但服务与背景边界间的自然关联性有助于声明我们曾在业务功能章节中提到过的区分效果。

除了对概念模式进行离散化处理,微服务同时也能够拆分数据存储决策。尽管整体性应用程序倾向于使用单一逻辑数据库保存持久性数据,但企业通常更乐于利用单一数据库涵盖一系列应用程序——而且大多数此类决策立足于具体供应商提供的授权商业模式。微服务机制则选择由每项服务管理其自身数据库的方式,而非不同实例基于同一数据库技术或者完全使用多种不同数据库系统——这种方式亦被称为混合持久化。大家可以利用混合持久化方案打理整体应用程序,但其在微服务架构中的亮相频率明显更高一些。

对微服务架构内数据责任关系的离散化处理也影响到了更新管理工作。常见的更新处理方案是在更新多种资源时,利用事务处理机制来保证其一致性。这种方式通常被用于整体性应用程序汉中。

这种事务处理使用方式确实有助于保障一致性,但却会带来显著的临时性耦合效果,而这在跨越多项服务时会带来新的难题。分布式事务处理非常难以实现,因此微服务架构更强调服务之间的事务处理协调性,同时明确强调只需保障最终一致性并通过补偿运算解决其中的冲突问题。

利用这种方式管理一致性问题已经成为众多开发团队的新困境,但其却能够切实匹配业务实践。一般来讲,企业需要保留一定程度的不一致性以实现某种程度的逆转能力,从而利用快速响应处理错误状况。这种权衡有其必要性,只要确定失误成本要低于高一致性条件下可能造成的业务损失成本即可。

基础设施自动化

基础设施自动化技术在过去几年中得到了长足发展——而云与AWS的演进则显著降低了构建、部署及运维微服务架构所带来的复杂性水平。

大部分利用微服务机制构建的产品或者系统都是由具备丰富的持续交付及其前者——持续集成——经验的团队所完成。通过这种方式构建软件的团队能够充分发挥基础设施自动化技术成果的潜在能力。我们可以将整个流程整理成以下图表:

图五:基本构建流程

让正确决定更易于执行

作为一项连带效应,我们发现实现持续交付与部署能够帮助开发人员及运维人员创造出高实用性工具。这类工具能够创建artifact、管理代码库、建立简单服务或者实现标准监控与记录等常见功能。这方面最典型的实例当数Netflix公司发布的一系列开源工具,险些之外Dropwizard等方案亦得到广泛使用。

整体应用程序的构建、测试与推送流程能够在此类环境下顺利完成。事实证明,一旦大家利用自动化流程进行整体应用开发,那么部署更多应用程序也将成为顺理成章的轻松任务。请记住,持续交付的目标之一就是令部署变得无脑化,这意味着无论是一款应用还是三款,其实际部署流程都不会有什么区别[12]。

我们还发现,不少团队在利用这种广泛的基础设施自动化能力管理生产环境下的微服务架构。相较于前面提到的整体与微服务应用在部署层面并没有太大区别,实际运维环境下的具体条件则存在着巨大差异。

图六:模块部署的具体方式往往差别巨大

故障应对设计

将服务作为组件加以使用的结果之一在于,应用程序需要经过针对性设计以确保其具备服务故障容错能力。任何服务调用都有可能因为供应程序不可用而发生问题。在这种情况下,客户端必须要尽可能做出适当的回应。相较于整体应用程序来说,服务即组件机制会增加额外的处理复杂性,这也是微服务架构的一大弊端。在这种情况下,微服务团队需要不断审视服务故障对用户体验造成的影响。Netflix公司的“猴子军团”项目就专门负责在正常运营期间对服务进行破坏,甚至利用数据中心故障来测试应用程序的弹性及监控能力。

断路器与可交代生产环境之代码

断路器模式出现在Amazon的Release It!当中,其中提到的其它模式还包括隔板模式与超时模式等。在加以结合之后,这些模式将在构建通信应用方面发挥巨大作用。Netflix公司发布的一系列博文就很好地解释了他们对这些模式选项的具体使用方式。 这类自动化测试机制往往会令正等待周末下班的运维团队们感到不寒而慄。这并不是说整体架构风格就无法使用高复杂性监控机制——只不过这种情况确实不太常见。

由于服务随时可能发生故障,因此最重要的就是保持对故障的快速检测能力,并在可能的情况下对其进行自动恢复。微服务应用程序高度强调对应用程序的实时监控能力,同时不断对架构元素(数据库每秒钟接收到的请求数量)以及业务相关指标(例如每分钟收到的订单数量)进行记录。语义监控能够通过早期预警系统抢先一步做出警示,并引导开发团队对问题加以跟进与调查。

这一点对于微服务架构尤为重要,因为微服务更倾向于采用由编排及事件协作实现的应急处理方式。尽管很多专家都对应急处理方案偶尔带来的收益表示认同,但其实际上往往也是让事情变糟的罪魁祸首。为了及时阻断糟糕的应急处理并确保其拥有可恢复性,监控系统就变得极为重要。

同步调用殊不可取

无论何时时,一旦在不同服务之间进行多次同步调用,那么可能引发宕机的概率也会以乘法形式增长。简单来讲,系统的总体宕机时间为各单个部件宕机时间的乘积。这时我们就面临着具体选择,到底是以异步方式进行调用,还是以计划方式管理由同步调用带来的宕机时间。英国《卫报》网站在其全新平台上执行了一项简单的规则——每个用户请求对应一次同步调用,而Netflix公司所使用的API则经历重新设计,确保其结构内采用异步调用机制。 整体应用程序的构建方式可与微服务架构同样透明——事实上也本应如此。二者的区别在于,在面对整体应用时我们需要在确切了解其运行在不同进程中的服务何时发生断开。考虑到同一进程当中可能包含多套库,这种透明度水平实际上很难实现。

微服务团队需要利用复杂的监控与记录机制处理各项服务,例如通过仪表板显示上线/下线状态以及一系列运营与业务相关指标。另外,我们还需要面对断路器状态、当前数据吞吐量以及延迟等其它常见的衡量数据。

演进设计

微服务从业者通常都具备演进设计工作背景,并将服务拆分视为一种深入型工具,旨在帮助应用程序开发人员在无需拖慢变更速度的前提下实现面向应用程序的变更控制。变更控制并不一定意味着变更数量削减——配合正确的态度与工具,大家完全可以帮助软件提供快速、频繁且经过良好控制的变更。

当尝试将一套软件系统拆分为多个组件时,我们往往面临着与具体拆分工作相关的决策任务——即我们应该遵循怎样的方针对应用程序进行拆分?而组件中的关键属性则在于其独立替换与可升级特性[13]——这意味着我们要找到确切的平衡点,保证自身能够在不影响其它协作对象的前提下对单一组件进行重写。事实上,很多微服务团队会更进一步,直接清退某些服务而非对其进行长期升级。

英国《卫报》网站就是个很好的例子,其应用程序在设计与构建方面作为整体应用存在,但却在逐步面向微服务架构演进。该网站的核心部分仍然属于整体性项目,但他们更倾向于通过构建微服务利用整体API实现新功能添加。这套方案对于临时性功能的实现非常重要,例如加设专题页面以显示体育赛事报道。网站中的这类组成部分能够通过快速开发语言在短时间内编写完成,并在对应事件结束后立即下线。我们还发现其它一些金融机构亦采取类似的方式公布突发性市场波动,并在数周或者数月之后将其下线。

这也强调了可替换性在模块化设计中的重要地位,其主旨正在于将模块机制贯彻整个变更模式[14]。大家希望只变更其中必须变更的部分,而其它模块则继续保持原样。系统当中那些几乎很少变动的部分应该立足于不同于高变更频率组件的服务。如果大家发现自己经常需要同时对两项服务做出变更,那么明显应该将二者加以合并。

将组件纳入服务也让我们能够以更高的细粒度水平进行规划制定。在整体应用程序当中,任何一项变更都需要对应用整体进行重构与重新部署。但在微服务架构方面,我们只需要重新部署包含对应变更的服务。这能够显著简化并加快发布流程。不过其弊端在于,我们必须考虑针对单一服务的变更是否会影响到其它服务。传统的整体性方案能够通过版本控制解决这类难题,但微服务领域则倾向于将版本控制作为最后一种应急办法。我们可以通过设计保证服务拥有强大的容错能力,从而应对其供应程序中出现的各类代码修改。

微服务是否代表着未来?

我们撰写这篇文章的主要目的在于解释微服务架构的基本思路与原则。而在撰写过程当中,我们明确意识到微服务架构风格确实是一项值得重视的关键成果——企业级应用程序开发人员应当对其加以了解。我们最近利用该架构构建了多套系统,而且了解到亦有其它多家企业将其纳入业务体系。

我们了解到的微服务架构先驱企业包括Amazon、Netflix、英国《卫报》、英国政府数字化服务局、realestate.com.au、Forward以及comparethemarket.com等等。2013年召开的相关会议则公布了更多参与其中的重要厂商。除此之外,另有相当一部分企业一直在使用类似的实现思路——但却并没有使用‘微服务’这样的称谓。(其通常将其冠以SOA标签——不过正如我们之前提到,SOA是一类存在大量矛盾取向的概念组合。[15])

尽管拥有这些积极的经验,但我们仍然无法完全肯定微服务架构就代表着软件未来的发展方向。虽然我们的实际经历证明微服务架构截至目前仍拥有优于整体性应用程序的积极优势,但必须承认只有充分的时间积累才能帮助我们做出真正完整则准确的判断结论。

我们的同事Sam Newman曾于2014年倾尽心力撰写出这本关于我们如何构建微服务架构类应用的论著。如果大家希望进一步探讨这个议题,请千万不要错过。

通常来说,架构决策的实际影响可能需要几年之后才能逐步显现出来。我们已经看到不少优秀的团队带着巨大的热情与愿景而投入工作,但最终却构建起一套陈旧不堪的整体性架构。很多人认为同样的情况不太可能发生在微服务架构身上,因为其服务边界非常明确因此不太可能发生相互影响。但由于时间尚短且系统程度不足,我们目前还无法真正评估微服务架构的成熟度水平。

人们对微服务成熟度抱持的怀疑态度也有其理由。在任何组件化尝试工作当中,最终结果的成功与否都取决于该软件与拆分后组件的契合效果。我们目前仍然很难说明组件边界的选择原则。演进设计导致边界划分变得非常困难,因此最重要的是保证其重构的简易性。但一旦将组件作为服务处理以实现远程通信,那么其重构难度将远远高于进程内库。在不同服务边界之间进行代码移动难度极大,而任何接口变更都需要在不同相关服务间实现,同时添加层的向下兼容能力,这无疑会令测试工作更加复杂。

另一大问题在于,如果相关组件间的关系不够简洁,那么我们就相当于把组件内部的复杂性转移到了不同组件间的连接当中。这样做不仅会导致复杂性扩散,同时亦会导致其明确性缺失且难以控制。立足于小型、简单组件审视问题总是更为直观,而在不同服务间进行纵览则往往会错失关注点。

最后,团队的技能水平也将起到决定性作用。新型技术成果往往要求高水平技术团队加以实施。不过高水平团队能够顺畅利用的技术方案并不一定能够在低水平人员手中发挥作用。我们已经见证了众多低水平团队构建起的如一团乱麻般的整体架构,但仍需要时间来了解微服务架构是否会在同样的情况下引发同样的状况。诚然,糟糕的团队创建出的始终只能是糟糕的系统——但我们不知道微服务架构到底是会缓解这种状况,还是令状况更中惨不忍睹。

目前有一种较为理性的论调,认为我们不应将微服务架构作为起步方案。相反,大家可以从整体性开发风格出发,保证其结合模块化机制,并在整体性特征引发实际问题后逐步将其拆分为微服务形式。(不过这样的建议并非完全理想,因为良好的进程内接口往往并不能成为良好的服务接口。)

因此我们对此抱持谨慎的乐观态度。到目前为止,我们已经了解到关于微服务架构的方方面面,而且其应该能够成为一种极具价值的开发手段。虽然还不能做出最终判断,但软件开发工作的固有挑战之一,正是我们只能根据目前掌握的远称不上完美的信息做出决策。

脚注

7: 虽然无关紧要,但Jim Webber曾经将ESB解释成“Egregious Spaghetti Box”,也就是“恐怖意面盒”。

8: Netflix公司最近将其架构类型称为“细化SOA”。

9: “YAGNI”的全称是“You Aren’t Going To Need It(你根本不需要它)”,这是一项经典的用户体验原则,即不要自作聪明地添加非必要性功能。

10: 我们所宣称的整体型应用只支持单一语言确实有些不尽不实——在当下的Web系统构建过程中,大家可能需要掌握JavaScript、XHTML以及CSS,而在服务器端的语言选项则包括SQL以及某种ORM(即对象关系映射)衍生语言。没错,单一语言肯定玩不转,但我相信大家明白我想要强调的意思。

11: Adrian Cockcroft在2013年11月的Flowcon大会上作出了精彩演讲,并特别提到了“开发者自助服务”与“开发者应亲自运行所编写代码”的观点。

12: 我们在这里的说法并不准确。很明显,在更为复杂的拓扑结构中部署大量服务肯定要比在单一整体型架构内进行部署困难得多。幸运的是,各类模式能够显著降低这种复杂性——当然,在工具方面的投入仍然不可或缺。

13: 事实上,Dan North将这种类型称为“可替代式组件架构”而非微服务架构。由于其强调内容属于微服务架构的一类子集,所以我们更倾向于使用后一种表达方式。

14: Kent Beck将此作为其《实施模式》一文中的设计原则之一。

15: SOA几乎是此类架构的历史起源。我记得当SOA一词在本世纪初刚刚出现时,很多人表示“我们几年前就已经将其引入日常工作了”。也有意见认为这种架构类型似乎最早出现于早期企业计算当中,COBOL程序通过数据文件实现通信的处理机制。而在另一方面,也有人认为微服务架构与Erlang编程模型其实是同一回事,不过后者只被应用在企业应用程序当中。