微服务架构这个术语在过去几年渐成热门,它把一种特定的软件应用的设计方法描述为能够独立部署的服务的套件。尽管缺乏对这一架构类型的准确定义,但是在业务能力、自动化部署、智能端点、语言和数据的去中心化控制等方面,已经形成了某些普遍特征。
微服务,另一个在软件架构领域津津乐道的新词。尽管我们本能上倾向于对它不屑一顾,然而这一专业术语描述了一种目前越来越吸引人的软件系统的风格。我们已经看到近年来有许多项目使用了此类型,结果很鼓舞人心;因而对于我的诸多同事来说,这也就成为他们构建企业级应用时候的首选。然而,当前并没有很多信息来描述什么是微服务,以及如何使用它。
简而言之,微服务架构把一个应用作为一套微服务来开发,这些微服务能运行自己的进程,采用 HTTP Resource API 这样的轻量级机制进行通信。这些微服务围绕着业务能力构建,能够通过完全自动化的部署体系独立部署。这些服务仅有最低限度的中心化管理,用不同的编程语言写成,使用不同的数据存储技术。
要说明微服务,与一体化应用做比较能有助于理解:一体化应用是被当作一个单元来构建的。企业级应用的构建通常包括三部分:客户端用户界面(由 HTML 页面和运行在用户机器上的浏览器里的 javascript 构成)、数据库(由各种表格构成,这些表格被插入到一个通用的关系数据库管理系统里),以及服务器端应用。服务器端应用处理 HTTP 请求,执行域名逻辑、从数据库提取并更新数据,以及选择并填充要被发送到浏览器的 HTML 视图。这个服务器端应用就是个庞然大物——逻辑单一、可执行。系统有任何修改,都需要重新构建并部署新版本的服务器端应用。
构建这样的系统,一体化服务就是非常适合的方法。所有处理请求的逻辑都运行在单个进程中,能够让你使用语言的基本功能从而把应用划分成不同门类、功能和命名空间。出于某种原因,你能够在开发者的电脑上运行和测试应用,使用部署管道来确保各种修改被正确地测试并部署到生产中。借助负载均衡器,你能够通过运行更多实例来横向扩展这一巨型应用。
一体化应用获得了成功,但是渐渐地,随着更多的应用被部署到云平台,人们的挫败感渐增。更新周期被紧紧绑定——即便是应用中一个很小部分的改变,也需要整个应用的重构和部署。随着时间推移,保持一个良好的模块化架构也日益困难,牵一发而动全身。一旦要进行扩展,就必须整体扩展,而不能仅仅扩展其中到一部分模块,也就需要更多的资源。
图 1:一体化 vs 微服务
这种情况最后进化为微服务架构:把应用作为一组服务来构建。这些服务能够独立部署和扩展,每个服务提供一个坚实的模型边界,甚至可以用不同的程序语言来写这些服务。当然他们也能被不同的团队来管理。
我们无意鼓吹微服务是新事物或者具有创新性,它植根于 Unix 的设计主旨。不过我们相信并没有很多人思考过微服务架构;如果许多软件开发使用了微服务,那它们的境况就会更好。
微服务架构的特征 我们不能给微服务架构下准确的定义,但是我们可以尝试总结一些通用特征。并不是所有的微服务架构完全符合下文列出的这些特征,不过我们可以预计大部分微服务架构能符合大多数。作为这个松散社区的活跃分子,我们(指两位作者)将尝试描绘我们在工作和我们熟悉的团队中看到的情况。
通过服务实现组件化
我们已经在软件行业浸淫多年,一直期望能够通过拼插组件的方式来构建系统,而不是采用我们在物理世界里常见的方式。在过去的几十年里,我们已经见证了多数语言平台的公共库的汇编已经取得了长足的进步。
谈及组件,要给它下定义并非易事。我们认为,一个组件就是软件的一个单元,能够被独立替换和升级。
微服务架构也会用到库,不过组件化软件的方式是把软件分解为服务。我们把库定义为一个程序中互相连接的组件,调用内存函数,同时这些服务是进程外部的组件,通过网页服务请求或者远程调用通信的方式进行通信。(这与面向对象编程中的服务对象的概念并不相同)
把服务当作组件(而非库)来使用的一大原因在于服务能够独立部署。如果你的应用在单个进程中包含多个库,那对其中任何一部分的改动都会导致重新部署整个应用。如果该应用分解为多个服务,那么可以预计,单个服务改变后可能只需要重新部署该服务即可。当然事无绝对,某些改动可能会改变服务接口,需要进行某些协调。但是,好的微服务架构的目标在于通过紧密结合的服务边界和进化机制,尽可能降低这些影响。
把服务当作组件使用的另一成果就是更为明确的组件接口。大多数语言缺乏良好机制去定义一个准确的发布接口。通常只有文档和规则来阻止客户端去中断组件的封装,结果导致组件的过度耦合。通过使用准确的远程调用机制,服务能轻松避免这些问题。
使用服务也有不足之处。远程调用比进程内调用更昂贵,远程 API 也需要粗粒度,这往往更加难以使用。如果你需要更改组件之间的职责分配,那么在跨越进程边界时就很难进行这样的操作。
乍一看,我们能观察到服务映射到运行时的过程,不过也仅仅是个大概。一个服务可能包含多个进程,它们会永远被开发和部署在一起,那么这样的应用进程和数据库也就只能被这一服务使用。
围绕业务能力组织
要把大型应用拆分为零件,管理人员通常聚焦在技术层面,拆分成 UI 组、服务器端逻辑组和数据库团组。当这些组被这样垂直分割,非常简单的改动就会导致跨组项目,而这需要时间和预算批准。聪明的团队会围绕这点进行优化,两害相权取其轻——强化逻辑到任意有访问权限的应用。
任何试图设计一个系统(广义定义)的组织将会衍生出一种设计,其结构正是该组织的通信结构的复刻。
— Melvyn Conway, 1967
图 2: Conway 效应在运行
而微服务采用的方法则大不一样,围绕着业务能力拆分并组合。这些服务使用与业务范围相符的软件而实现广泛的技术栈,包括用户界面、持续存储,以及任意的外部协作。因此这些组之间是跨功能的,包括开发所要求的所有技能:用户体验、数据库和项目管理。
图 3:通过团队界限强化服务界限
www.comparethemarket.com 就采用了这种组织方式。跨功能的团队对构建和运行每个产品负责,每个产品又被拆分为大量的单个服务,这些服务通过信息总线通信。
大型的一体化应用也能够围绕业务能力模块化,然而并不常见。当然我们会敦促这些构建一体化应用的大型团队按照业务线来进行分工。然而问题在于,这些业务线倾向于根据诸多环境进行组织。一旦大型应用横跨许多模块,那么对于团队中的个体而言,很难融入他们的工作记忆中。此外我们也看到,这些模块线需要大量的训练去执行。服务组件所需的更为精细的分割也能让团队边界更清晰。
产品而非项目
大多数我们常见的应用开发会使用项目模式:交付软件的部分然后再考虑组合完整。完成后的软件被交付到维护机构,构建此项目的团队不被解散。
微服务的支持者则易于避免此模式,倾向于「在产品的整个生命周期里,开发团队应该拥有此项目」。这一灵感来自于亚马逊的「你构建,你运行」概念。在亚马逊,开发团队对生产环境中的软件负有全部责任。这让开发者每日都能了解自己的软件如何在生产环境运行,增强与用户的接触,也能承担部分支持职责。
这种产品意识与业务能力紧紧联系。与其把软件看作一套需要完成的功能,不如把它们看作一段进行中的关系,其中的关键问题是软件如何帮助用户增强业务能力。
并没有理由表明这一方法不能用于一体化应用,不过颗粒度更小的服务能够更容易地在服务开发者和用户之间建立起个人关系。
智能终端和哑管道
要在不同进程之间构建通信结构,我们已经见过许多产品和方案,它们强调在通信机制内部注入智能,其中优秀案例如 ESB(企业服务总线)。ESB 产品包含复杂的设施,用于信息路由、编排、转化,以及应用业务规则。
微服务社区则喜欢另一种方案:智能终端和哑管道。使用微服务架构的应用致力于在尽可能地解耦合的同时保持关联性——他们拥有自己的域名逻辑,从经典 Unix 的视角看来更像过滤器——接收请求,恰当地应用逻辑,生成反应。这些编排使用了简单的 REST 协议而非 WS-Choreography 或者 BPEL ,也没有采用使用中心化工具进行编配。
两种广为使用的协议分别是 HTTP 请求-反应与资源 API,以及轻量级消息。对于前者,最佳描述莫过于
成为 web ,不要隐藏其后
—— 伊恩·罗宾逊
微服务团队使用万维网(往大了说,Unix)依赖的原则和协议。开发者或者运维人员能够以很小的代价缓存经常使用的资源。
第二种方法的通常用法是利用轻量级信息总线进行通知。选定的基础设施是典型的 「哑管道」—— 就像 RabbitMQ 或 XeroMQ 这样无需提供稳定的异步组织的简单实施;而智能终端则在服务内生成并消耗消息。
在大型应用里,组件联机执行,它们之间的通信或者采用方法调用,或者调用函数。在大型应用到微服务的转变中,最大的问题就是通信模式的改变。从内存调用函数的本地对话转为 RPC,可能会变成性能不佳的聊天式通信。并且,你还需要用粗粒度的方法代替原来的精细通信。
去中心化治理
中心化治理的一大后果就是单一技术平台的标准化倾向。经验显示这一方式非常狭隘——每个问题各有特色,而「马斯洛的锤子」并非万能。我们更喜欢针对工作使用正确的工具,在特定情境下,一体化应用能够发挥不同语言的优势。这并不常见。
把大型应用的组件拆分为服务,那么当我们构建每个部分时就有选择。想用 Node.js 构建一个单个报告页面?用起来!用 C++ 写一个格外粗糙的近实时组件?没问题。想交换不同数据库类型,从而更好地适应某个组件的阅读习惯?我们也有重构技术。
当然,能做并不等于要那样做——不过这种系统切割方法给了你选择权。
构建微服务的团队也愿意使用别的方法达到标准。与其使用纸面上的现成标准,他们更愿意使开发有用的工具,从而别的开发者也能够用来解决相似问题。这些工具通常通过实施收获成果,以包括内部开源模式在内的方式更广泛的群体中分享。现在 git 和 github 已经成为事实上的版本控制系统,开源实践也在机构内部越来越常见。
Netflix 就是这一理念的最佳践行者。通过库的形式分享有用的、经过时间考验的代码,鼓励别的开发者以相似方法解决相似问题,同时也给别的必要的方法留有机会。共享库关注常用问题,比如数据存储、进程间通信,以及下文将讨论的基础设施自动化。
对微服务社区来说,额外的开销格外讨厌。社区并非不重视服务协议;与之相反,他们使用不同的方式来管理这些协议。Tolerant Reader 和 Consumer-Driven Contracts 这样的模型经常被用于微服务。他们帮助服务协议进行独立进化。通过把执行消费者驱动协议作为构建的一部分,强化了信心,也能就其它微服务是否工作提供快速反馈。我们知道澳大利亚的一支团队就使用消费者驱动协议来推动新服务的构建。他们使用能够定义单个服务协议的简单工具。在新服务的代码写好之前,就成为自动化构建的一部分。服务仅在满足协议时被构建,以优雅的方式避免了「YANGI」( You Aren’t Going To Need It )悖论。这些技巧和工具围绕着它们成长,降低了对中心化协议管理的需求,减少了服务之间的暂时耦合。
或许去中心化治理的最高点是流行于亚马逊的谁构建谁运行的理念。每个团队都为自己构建的应用的所有方面负责,包括 24*7 不间断地运营软件。 这种层次的责任转变显然不同寻常,然而我们看到越来越多的公司给开发团队灌输这种层次的责任心。Netflix 是另一家采用这种理念的公司。不要在每个凌晨三点被寻呼机吵醒,这绝对是开发者关注代码质量的强大动力。这些理念已经与传统的中心化治理模型相去甚远。
去中心化数据管理
去中心化数据管理的方式多种多样。在最为抽象的层级,这意味着各个系统之间关于世界的概念模型大相径庭。在大型企业进行整合时,这一现象很常见。对客户的看法,销售人员的视角与支持人员的视角不同。销售认为可称为「客户」的某些方面,支持人员却并不认同。他们可能只是具有一些在语言描述上差异很细微的不同属性。
这一现象在应用之间也很普通,也会发生在应用内,特别是当应用被拆分为单独的组件时。 领域模型驱动设计(Domain-Driven Design) 的 Bounded Context 概念非常有助于思考这一问题。DDD 将一个大模型分解为几个较小的模型,并且能够投射出它们之间的关系。这一过程对于一体化架构和微服务架构都非常有益。不过在服务和 context 的边界之间存在自然关系,当我们在描述业务能力单元、强化分离时,这一自然关系有助于明朗化。
除了下放有关模型概念的决策,微服务也下放了数据存储的决策。由于一体化应用喜欢为持续性数据采用单一逻辑的数据库,企业也往往在一系列应用中采用单一数据库。这些决策大多数受厂商的授权商业模式驱动。微服务倾向于让每个服务管理自己的数据库,或者不同的数据库系统,即 Polyglot Persistence。你也可以在一体化应用中使用 Polyglot Persistence,不过它更多见于微服务。
图4:一体化设计:单一数据库 vs 微服务:应用数据库
把跨微服务的数据下放影响了对更新的管理。处理更新的常见方式是在更新多个资源时,通过使用事务来保证一致性。这种方式通常也被用于一体化应用内。
使用诸如这样的事务有助于保持一致,但是带来了显著的短时耦合,对跨多个服务产生问题。分布式事务因难以实施而闻名,随之而来,微服务架构强调了服务之间的事务和谐,明确了一致性只可能为最终一致性,各种问题通过补偿运算来解决。
选择通过该方法来管理不一致对于许多开发团队来说是项新的挑战,不过很符合商业惯例。通常商家为了快速响应需求会对不一致进行不同程度的处理,其中会存在一些处理错误导致的逆转。只要修复错误的代价低于因为一致性而导致业务损失的成本,这种权衡就是值得的。
基础设施自动化
基础设施自动化已经在过去的几年里取得了巨大的进步。云特别是 AWS 的进化格外地降低了构建、部署和运行微服务时的复杂度。
许多采用微服务构建的产品或系统是由在持续交付和其前身——持续集成方面经验丰富的团队构建的。使用这种方法构建软件的团队需要大量使用基础设施自动化技术。下图展示了构建流程。
图5:基本的构建流程
考虑到本文并不针对持续交付,我们这里只提及几个关键特性。我们需要大量信心来认可自己开发的软件可行,因此会跑大量的自动化测试。要推广可行的软件到流程之上,意味着我们需要把部署每个新环境自动化。
一体化应用能够非常愉快地在这些环境中构建、测试和推送。事实证明,一旦你给一体化应用投入了自动化路径,那部署更多应用也并不那么可怕了。谨记,持续交付的目的之一就是让部署变得单调,所以不管是部署一个还是三个应用,只要依然单调就没有关系。
另一个常见的使用大规模基础设施自动化的领域就是在生产环境中管理微服务。前文我们认为只要部署一如既往地单调,那在一体化和微服务之间相差不会太大;恰恰与此相反,在运行阶段,二者却是相去甚远。
图6:模块部署经常不同
为故障而生
把服务用作组件的一个结果是应用在设计之初就要能容忍技术故障。任何服务调用可能会由于供应商的不可用而失败,而客户端需要尽可能优雅地做出响应。与一体化设计相比,由于引入了额外的复杂性来处理,这是一大不足。其结果是微服务团队不断反省服务故障如何影响用户体验。 Netflix 的 Simian Army 通过测试应用的弹性和监控,减少了工作日的服务故障,甚至数据中心的故障。
这种生产环境中的自动化测试足以让大多数的运营团队望而生畏,通常后者需要提前一周的时间。这并非说一体化架构风格不能尽兴这种精密的监控设置,只是不常见于我们的经验。
既然服务可能随时发生故障,所以能够快速监测并尽可能地自动恢复服务就非常重要。微服务应用侧重于应用的实时监控,检查架构因素(数据库每秒获得多少请求)和业务相关指标(每分钟收到多少订单)。语义监控也可提供早期预警系统,一旦出错就触发开发团队去跟进和调查。
对于微服务架构来说这尤为重要,因为微服务更偏好编配和事件协作导致的自发行为。尽管许多专家认可偶发价值,事实上意外行为并非好事。监控对于发现糟糕的意外行为至关重要,从而能够尽快修复。
一体化也可以像微服务那样透明构建,事实上,他们也应如此。区别在于你必须要了解运行在不同进程的服务们是何时断开的。考虑到相同进程内的库,这种透明度不太可能有用。
微服务团队希望能为每个单独的服务设置精密的监控和记录,这些服务包括在 dashboard 上显示服务启用/宕机状态,以及各种相关的运营和业务指标。与断路器状态、当前吞吐量和延迟的详情都是我们经常遇到的其它例子。
进化的设计
微服务从业者通常具有进化设计的背景,把服务分解视作一个长远的工具,让应用开发者们能够控制应用内的改动,无需让改动慢下来。改动控制并不一定意味着减少——借助正确的态度和工具,你能够经常快速、有节制地修改软件。
当你试图把一个软件系统分为组件,你要作出如何划分的决定——哪些是我们切分应用时需要遵守的原则?组件的关键属性是独立替换和升级的概念,也就意味着我们要找到一些立足点,当需要重写某个组件时,也不会影响它的协作者。
按照一体化来设计并构建,却演化为微服务,卫报网站给这样的应用树立了典范。网站的核心仍然是一体化,不过他们更愿意通过调用一体化应用的 API 来构建微服务,从而添加新功能。对于体育赛事的特定页面这样注定短暂的功能来说,这样的方法非常方便。网站上的类似部分能够通过快速开发语言而被迅速地组织起来,一旦赛事结束则可以快速移除。我们在一家金融机构也看到了类似办法,增加新服务以对应新的市场机会,几个月甚至几周后就被放弃。
这种对替代性的强调,也是模块化设计通用原则的一个特例,通常推动模块性来实现改变的方式。你可能想保留同一模块内同一时间的改变。系统内发生更改的部分很少在不同的当前大量流失的服务内。如果你发现自己重复在同时修改两个应用,那表明它们应该被合并。
把组件集成到服务,让更精细的发布计划大有可为。采用一体化,任何修改都需要对整个应用进行一次全面的构建和部署。采用微服务后只需要重新部署修改过的服务。这能够简化并加快发布过程。缺点是你得担忧对一个服务的修改可能破坏它的用户。传统的集成方法是采用版本控制来处理错误,而在微服务的世界里,则把版本控制当作最后的补救办法。通过给服务设计得尽可能强的修改宽容度,我们能够避免大量的版本控制。
微服务是未来吗?我们撰写此文的主要目的是解释微服务的主要思路和原则。通过此篇论述,我们认为微服务的架构风格是一个重要概念,值得企业级应用去认真考虑。我们最近已经采用此风格构建了多个系统,也知道有人也使用并赞同此方法。
我们所知的微服务架构的先驱包括亚马逊(Amazon)、网飞(Negflix)、卫报(Guardian)、英国政府数字化服务部门(UK Government Digital Service)、realestate.com.au、Forward 和 comparethemarket.com 等。
2013 年的 The Conference Circuit 大会充满了各种公司案例,他们正在迁移到微服务类别的产品和服务,其中包括 Travis CI。此外也有大量机构一直在做类似微服务的事情,但是并未采用此名称。(通常被标记为 SOA,不过 SOA 以各种矛盾的形态出现)
尽管有这些切实的经验,但是我们并不能坚决肯定微服务就是软件架构未来的发展方向。在微服务方面积累积极经验(与一体化应用相比)的同时,我们仍保持清醒——微服务还没有经过足够长时间的检验,因而还不能做出完整判断。
我们的同事 Sam Newman 2014年的时候花费了大量时间写书,记录了我们构建微服务的经历。如果你想深入研究此主题,那你下一步也应该这么做。
微服务架构决定的实际效果需要多年后才能显现。我们已经看到一些由对模块化有强烈需求的优秀团队构建的一体化架构的项目,在多年后衰退。由于服务边界明确,且难以修补,许多人认为微服务不可能有此衰退。除非我们能看到足够多上年头的系统,否则还是不能完全评价微服务的成熟度。
当然也有其它原因让人们认为微服务不够成熟。在各种组件化的努力中,成功依赖于软件与组件的相符程度。要弄清组件的边界在哪里,这非常难。自我进化的设计认识到了让边界正确的难度,以及由此而来的让重构保持简单的重要性。不过一旦你的组件是需要远程通讯的服务,那重构要比采用联机库的服务更难。跨服务边界的代码迁移也很困难,任何界面变化都需要在参与者之间协调,也要添加对后端兼容的层,测试也会更复杂。
另一个问题就是,假如组件不能干净地组合,那么你所做的不过是把复杂性从组件内部转移成组件之间的联结。这样做不仅仅是复杂性的迁移,同时也变得更不明确,也更难以控制。如果只是查看一个小而简单的组件内部,而忽略服务之间混乱的联结,那你很容易就觉得更好。
最后,团队技能也是一大因素。新技术很容易被技术熟练的团队采用。不过一项对熟练团队来说更有效的技术,可能并不适合稍逊一筹的团队。我们已经见证了很多技术水平稍逊的团队构建的凌乱的一体化架构;采用微服务会发生怎样的混乱,也需要时间观察。水平不佳的团队会一直创建不太好的系统,在这种情况下,很难说微服务是会减少混乱,还是会让情况更糟。
我们听到的一个合理的说法是你不应该一开始就用微服务架构。相反,以一体化开始,保持模块化,一旦一体化变得麻烦,就将其拆分为微服务。(不过这一建议不甚理想,因为一个好的联机接口通常并不是一个好的服务接口)
我们以谨慎乐观的态度写下此文。到目前为止,我们已经足够了解微服务的风格,也认为值得踏上此路。我们不能肯定地说终点何在,不过软件开发中的一大挑战就是你只能基于当下所掌握的不完整的信息做决定。
作者:James Lewis Martin Fowler
来源:51CTO