“微服务架构”一词在过去几年逐渐流行起来,用于描述一种特定的软件应用设计方式:将应用构建为一组可独立部署的小型服务。简单来说,微服务架构是一种围绕业务能力拆分系统、让各个服务独立开发、独立部署、独立扩展的软件架构风格。虽然这种架构风格并没有一个精确统一的定义,但它通常具有若干共同特征,例如围绕业务能力组织服务、自动化部署、智能端点,以及在编程语言和数据管理方面实行去中心化控制。
“微服务”是软件架构领域的又一个新词。我们通常会对这类新术语保持警惕,但它所描述的软件系统风格,确实越来越值得关注。过去几年里,我们看到许多项目采用了这种风格,并取得了相当不错的效果,以至于不少同行已经将其视为构建企业级应用的默认方式。然而遗憾的是,目前系统阐述微服务风格究竟包含哪些内容,以及如何真正落地这种风格的资料仍然不多。

简而言之,微服务架构风格¹是一种将单个应用开发为一组小型服务的方法。每个服务都运行在自己的进程中,并通过轻量级机制进行通信,通常是基于 HTTP 的资源 API。这些服务围绕业务能力构建,并可通过全自动部署机制独立部署。服务只需要极少的集中式管理,可以使用不同的编程语言编写,也可以采用不同的数据存储技术。
微服务架构与单体架构的区别
要解释微服务架构,可以先将它与单体架构进行比较。单体应用通常作为一个整体构建。典型的企业应用一般由三个主要部分组成:客户端用户界面,通常由运行在用户浏览器中的 HTML 页面和 JavaScript 构成;数据库,通常由存放在同一个数据库管理系统中的多张表构成,多数情况下是关系型数据库;以及服务器端应用。服务器端应用负责处理 HTTP 请求、执行领域逻辑、从数据库中读取和更新数据,并选择、填充要发送到浏览器的 HTML 视图。这个服务器端应用就是一个单体应用,也就是一个单一的逻辑可执行体²。对系统的任何修改,通常都意味着必须构建并部署服务器端应用的新版本。
这种单体服务器架构,是构建此类系统时很自然的选择。所有处理请求的逻辑都运行在同一个进程中,因此可以利用编程语言的基本机制,将应用划分为类、函数和命名空间。只要稍加注意,开发人员就可以在自己的笔记本电脑上运行和测试应用,也可以使用部署流水线,确保变更经过充分测试后再发布到生产环境。单体服务器还可以通过在负载均衡器后运行多个实例来实现横向扩展。
单体应用当然可以成功运行,但越来越多的人开始对它们感到不满,尤其是在越来越多应用被部署到云端之后。单体应用的变更周期是捆绑在一起的:哪怕只是修改应用中的一小部分,也需要重新构建并部署整个单体应用。随着时间推移,保持良好的模块化结构会变得越来越困难,本应只影响单个模块的变更,也会逐渐变得难以控制。扩展时同样如此,往往必须扩展整个应用,而不能只扩展真正需要更多资源的部分。

图 1:单体架构和微服务架构
这些痛点催生了微服务架构风格:将应用构建为一组服务。除了能够独立部署和独立扩展之外,每个服务还提供了清晰的模块边界,甚至允许不同服务使用不同的编程语言编写。它们也可以由不同团队分别管理。
我们并不认为微服务架构是全新的发明或创新,它的思想根源至少可以追溯到 Unix 的设计原则。但我们认为,认真考虑微服务架构的人仍然不够多;许多软件开发项目如果采用这种架构方式,可能会获得更好的结果。
微服务架构的核心特点
我们无法说微服务架构风格存在一个正式定义,但可以尝试描述那些通常被归入这一标签的架构所具有的共同特征。与任何基于共同特征的定义一样,并非所有微服务架构都具备全部特征,但我们预期大多数微服务架构会体现其中的大部分。
虽然我们一直是这个较为松散的社区中的活跃成员,但本文的目的并不是制定一个必须遵循的定义,而是尝试描述我们在自身工作中,以及在其他团队类似实践中所观察到的现象。
通过服务实现组件化
自从我们进入软件行业以来,人们就一直希望能够像在现实世界中组装产品那样,通过拼装组件来构建系统。过去二十年里,通用库迅速发展,并成为大多数语言平台的重要组成部分。
谈到组件时,我们会遇到一个棘手的问题:究竟什么才算组件?在本文中,我们将组件定义为:可以独立替换和升级的软件单元。
微服务架构也会使用库,但它实现软件组件化的主要方式,是将软件拆分为服务。我们把库定义为链接到程序中,并通过内存中的函数调用来调用的组件;而服务则是进程外组件,它们通过 Web 服务请求、远程过程调用等机制进行通信。这与许多面向对象程序中“服务对象”的概念不同³。
使用服务而不是库作为组件,一个主要原因是服务可以独立部署。如果一个应用包含多个库,并且这些库运行在同一个进程中,那么对任何一个组件的修改都会导致整个应用需要重新部署。相反,如果将应用拆分为多个服务,那么许多针对单个服务的修改只需要重新部署该服务即可。当然,这并非绝对。某些修改会改变服务接口,因此仍然需要一定的协调。但优秀的微服务架构会通过统一的服务边界,以及服务契约中的演进机制,尽可能减少这种协调成本。
将服务作为组件的另一个好处,是组件接口更加明确。大多数编程语言并没有足够完善的机制来定义清晰的发布接口。通常只能依靠文档和规范,防止调用方破坏组件封装,从而导致组件之间产生过强耦合。而服务由于使用明确的远程调用机制,往往更容易避免这类问题。
当然,使用这类服务也存在缺点。远程调用比进程内调用成本更高,因此远程 API 往往需要设计得更粗粒度,而这通常会让使用体验变得更麻烦。如果需要重新划分组件之间的职责,跨越进程边界进行这种行为调整也会更加困难。
初步来看,我们可以把服务理解为运行时进程,但这只是一个近似说法。一个服务可能包含多个进程,这些进程总是一起开发和部署,例如一个应用进程,以及一个仅供该服务使用的数据库。
围绕业务能力组织微服务
当需要将大型应用拆分为多个部分时,管理层往往会从技术层面进行划分,例如组建用户界面团队、服务器端逻辑团队和数据库团队。如果团队按照这种方式组织,即便是一个简单改动,也可能演变为跨团队项目,并消耗大量时间和预算审批成本。聪明的团队通常会绕过这些问题,选择“两害相权取其轻”的方式:把逻辑塞进自己能够访问的应用部分中。换句话说,逻辑最终会散落到各处。这正是康威定律的体现。
任何设计系统(广义上定义)的组织,其设计的结构都会是该组织自身沟通结构的复制品。
——梅尔文·康威,1968年

图 2:康威定律的实际应用
微服务架构采用了不同的划分方式:它围绕业务能力来拆分服务。每个服务都面向特定业务领域,包含完整的软件实现,包括用户界面、持久化存储以及必要的外部协作。因此,团队通常是跨职能的,覆盖开发所需的全部技能,包括用户体验、数据库和项目管理等。在实际落地时,这类跨职能协作也需要清晰的任务、文档、目标和排期管理,像 Worktile 这类通用项目协作系统,就可以帮助团队在任务、项目、文档、日历、甘特图和审批等方面保持协同。

图 3:服务边界由团队边界强化
海外某些公司正是采用这种组织方式:跨职能团队负责构建并运营每个产品,而每个产品又被拆分为若干个独立服务,这些服务通过消息总线进行通信。
大型单体应用也可以围绕业务能力进行模块化,尽管这种情况并不常见。我们当然也建议构建单体应用的团队按照业务线来划分组织。我们观察到的主要问题是,单体应用往往跨越过多上下文。如果单体应用跨越了许多模块边界,团队成员就很难在短期记忆中始终保持对这些边界的清晰理解。此外,我们发现,要在单体应用中有效执行模块划分,需要非常严格的纪律。相比之下,服务组件要求更加明确的边界,因此也更容易让团队边界保持清晰。
产品而非项目
我们看到的大多数应用开发工作都采用项目模式:目标是交付一段软件,一旦交付完成,软件就被视为“已完成”。随后它会被移交给维护组织,而开发该软件的项目团队则随之解散。
微服务架构的倡导者通常会避免这种模式。他们更倾向于认为,团队应当在产品的整个生命周期内持续负责。海外某些企业提出的“谁构建,谁运行”理念,是这种模式的重要灵感来源:开发团队要对运行在生产环境中的软件承担全部责任。这使开发人员能够持续了解软件在生产环境中的真实运行情况,也会增加他们与用户之间的互动,因为他们至少需要承担一部分支持工作。
产品思维与业务能力紧密相关。它不是把软件视为一组需要完成的功能,而是把软件视为一种持续关系:软件如何持续帮助用户提升其业务能力。对于研发团队来说,这也意味着从目标制定、客户反馈、需求梳理、评审排期,到开发、测试、发布和知识沉淀,都需要形成连续闭环。比如 PingCode 这类智能化研发管理工具,覆盖研发全生命周期管理,并能打通研发过程中使用的其他工具,让需求、项目、测试、发布和知识数据更顺畅地流转。
对于单体应用来说,采用同样的方法当然也并非不可行。但服务粒度更小,通常更容易在服务开发者和用户之间建立直接联系。
智能端点与哑管道
在构建不同进程之间的通信结构时,我们看到许多产品和方法都强调在通信机制本身中加入大量智能能力。企业服务总线(ESB)就是一个典型例子。ESB 产品通常提供复杂功能,用于消息路由、编排、转换以及业务规则应用。
微服务社区倾向于另一种方法:智能端点与哑管道。基于微服务构建的应用会力求尽可能解耦,并保持内聚。它们拥有自己的领域逻辑,更像经典 Unix 意义上的过滤器:接收请求,应用相应逻辑,并生成响应。这些操作通常使用简单的 REST 协议进行协调,而不是使用 WS-Choreography 或 BPEL 这类复杂协议,也不是通过中心化工具进行编排。
最常用的两种通信方式,是基于资源 API 的 HTTP 请求—响应机制,以及轻量级消息传递机制⁷。第一种方式可以用某位技术专家的话来概括:
要融入网络,而不是躲在网络背后。
微服务团队采用的是构建万维网的原则和协议,在很大程度上也继承了 Unix 的思想。借助这些原则和协议,开发人员或运维人员只需付出很少努力,就可以缓存常用资源。
第二种常见方式是使用轻量级消息总线进行消息传递。所选基础设施通常是“哑”的,也就是说它只充当消息路由器。像 RabbitMQ 或 ZeroMQ 这样的简单实现,除了提供可靠的异步结构之外,并不会做太多事情。智能逻辑仍然存在于生成和消费消息的端点中,也就是服务本身。
在单体架构中,组件在同一个进程内执行,彼此之间通过方法调用或函数调用通信。将单体架构迁移到微服务架构时,最大的挑战之一就是通信模式的变化。简单地把内存中的方法调用转换成 RPC,会导致通信过于频繁,从而带来糟糕的性能。因此,必须用更粗粒度的通信方式来替代原本细粒度的调用方式。
去中心化治理
集中式治理的后果之一,是容易倾向于采用单一技术平台。经验表明,这种做法存在明显局限:并非所有问题都是钉子,也并非所有解决方案都是锤子。我们更倾向于选择合适的工具来完成合适的工作。虽然单体应用在一定程度上也可以使用不同的编程语言,但这并不常见。
一旦将单体应用拆分为服务,在构建每个服务时就会有更多选择。想用 Node.js 搭建一个简单的报表页面?没问题。想用 C++ 开发一个特别复杂的近实时组件?没问题。想为某个组件的读取行为更换一种更合适的数据库?我们也有能力这样重构。
当然,能做某件事并不意味着就应该做。但以这种方式划分系统,至少意味着你拥有选择权。
构建微服务的团队也倾向于采用不同的标准化方式。他们通常不会依赖一套写在纸面上的固定标准,而更倾向于开发实用工具,帮助其他开发者解决类似问题。这些工具通常从已有实现中提炼出来,并在更大的群体中共享,有时采用内部开源模式。如今,Git 和各类代码托管平台已经成为事实上的主流协作工具,开源实践也越来越多地进入企业内部。
海外某些技术公司是践行这种理念的典型代表。将实用且经过生产验证的代码以库的形式共享,既能鼓励其他开发者以类似方式解决类似问题,也为必要时选择不同方法保留了空间。共享库通常专注于数据存储、进程间通信,以及下文将进一步讨论的基础设施自动化等常见问题。
对于微服务社区来说,额外开销尤其令人反感。但这并不意味着社区不重视服务契约。恰恰相反,微服务社区往往拥有更多服务契约,只是他们在寻找不同的契约管理方式。“容错读取器”(Tolerant Reader)和“消费者驱动契约”(Consumer-Driven Contracts)等模式,通常会被应用于微服务实践中。这些模式有助于服务契约独立演进。在构建过程中执行消费者驱动契约,可以增强信心,并快速反馈服务是否仍然正常工作。
事实上,我们了解过一个海外团队,他们使用消费者驱动契约来驱动新服务的构建。他们使用简单工具定义服务契约;在编写新服务代码之前,这些契约就已经成为自动化构建的一部分。随后,服务只需要构建到满足契约为止。这是一种巧妙的方法,可以避免在构建新软件时陷入 YAGNI(You Aren’t Gonna Need It,即“你不会需要它”)困境。这些技术以及围绕它们发展出的工具,通过降低服务之间的时间耦合,减少了对集中式契约管理的需求。
或许,去中心化治理的极致体现,就是海外某些企业推广的“构建/运行”理念。团队负责自己所构建软件的方方面面,包括全天候运行。这种程度的责任下放当然并不普遍,但我们确实看到越来越多公司正在将责任下放给开发团队。海外某些技术公司也采用了类似理念¹⁰。凌晨三点被寻呼机叫醒,无疑会激励你在编写代码时更加关注质量。这些理念与传统的集中式治理模式截然不同。
去中心化数据管理
数据管理的去中心化体现在多个层面。最抽象的层面是,不同系统对世界的概念模型可能并不相同。这在大型企业集成中是一个常见问题。例如,销售部门眼中的客户,与支持部门眼中的客户可能并不是同一个概念。某些在销售视角中被视为客户的实体,在支持视角中可能根本不会出现。即使出现,它们也可能具有不同属性;更糟糕的是,它们可能拥有同名属性,但语义却略有不同。
这个问题在应用之间很常见,但也可能出现在应用内部,尤其是在应用被拆分为多个独立组件之后。一个有用的思考方式,是理解领域驱动设计(DDD)中的“限界上下文”概念。DDD 将复杂领域划分为多个限界上下文,并描绘它们之间的关系。这个过程对单体架构和微服务架构都适用。但服务边界与上下文边界之间存在天然联系,这有助于明确这些分离,正如我们在“围绕业务能力组织”一节中所讨论的那样。
除了分散概念模型的决策权之外,微服务还分散了数据存储的决策权。单体应用倾向于使用单一逻辑数据库来持久化数据,而企业也常常倾向于让多个应用共享同一个数据库。这些决策在很大程度上受供应商许可模式影响。微服务则倾向于让每个服务管理自己的数据库:可以是同一种数据库技术的不同实例,也可以是完全不同的数据库系统。这种方法被称为多语言持久化(Polyglot Persistence)。虽然单体应用也可以使用多语言持久化,但它在微服务架构中更为常见。

将数据责任分散到各个微服务中,会对更新管理产生影响。处理更新的常见方式,是使用事务来保证多个资源更新时的一致性。这种方式在单体架构中很常见。
事务有助于保持一致性,但也会造成显著的时间耦合。当这种耦合跨越多个服务时,就会带来问题。分布式事务的实现难度众所周知,因此微服务架构通常强调服务之间的无事务协调,并明确接受一致性可能只是最终一致性,而相关问题则通过补偿操作来处理。
以这种方式管理不一致性,对许多开发团队而言都是一个全新的挑战,但它往往与真实业务实践相符。企业通常会容忍一定程度的不一致性,以便更快响应需求,同时建立某种纠正流程来处理错误。只要修复错误的成本低于为了追求更高一致性而造成的业务损失,这种权衡就是值得的。
基础设施自动化
过去几年,基础设施自动化技术取得了巨大进展。尤其是云计算和海外某些云服务平台的发展,降低了构建、部署和运行微服务的运维复杂度。
许多采用微服务架构构建的产品或系统,都由具备丰富持续交付经验的团队开发。这些团队也通常经历过持续集成的长期实践。它们会广泛运用基础设施自动化技术来构建软件。下图所示的构建流水线便体现了这一点。

图 5:基本构建流程
由于本文并不是关于持续交付的文章,我们只重点介绍几个关键特性。为了确保软件能够正常运行,我们会执行大量自动化测试。将运行良好的软件逐步推进到流水线中的后续环境,意味着我们已经实现了对每个新环境的自动化部署。
单体应用同样可以在这些环境中顺利完成构建、测试和部署。事实证明,一旦你投入资源实现了单体应用生产部署流程的自动化,部署更多应用就不再那么令人恐惧。持续交付的目标之一,就是让部署变得平淡、可靠,甚至“无聊”。因此,无论部署的是一个应用还是三个应用,只要部署过程依然足够无聊,就没有本质区别。
我们还看到,团队在生产环境中管理微服务时,也会广泛使用基础设施自动化技术。与前面关于部署的观点不同——即只要部署足够简单,单体架构和微服务架构之间差异并不大——二者在运行环境中的差异可能会非常明显。

图 6:模块部署方式通常有所不同
面向失败设计
将服务作为组件的一个后果是,应用程序必须被设计为能够容忍服务故障。任何服务调用都可能因为服务提供方不可用而失败,客户端必须尽可能优雅地应对这种情况。与单体架构相比,这是一个劣势,因为它引入了额外的故障处理复杂性。因此,微服务团队需要持续思考服务故障对用户体验的影响。海外某些技术公司的故障演练工具集,会在工作日人为制造服务甚至数据中心故障,以测试应用的弹性和监控能力。
这种在生产环境中进行的自动化测试,足以让大多数运维团队感到不寒而栗,甚至恨不得因此休假一周。这并不是说单体架构无法实现复杂的监控设置,只是根据我们的经验,这种情况相对少见。
由于服务随时可能发生故障,因此快速检测故障,并尽可能自动恢复服务,就变得至关重要。微服务应用非常重视实时监控,既包括架构层面的指标,例如数据库每秒接收的请求数,也包括业务相关指标,例如每分钟接收的订单数。语义监控可以作为早期预警系统,及时发现问题,并触发开发团队进行跟进和调查。
这一点对微服务架构尤其重要,因为微服务倾向于通过编排和事件协作进行工作,这可能导致涌现行为。虽然许多专家赞赏涌现带来的价值,但现实是,涌现行为有时也可能产生负面结果。监控至关重要,因为它可以帮助我们快速发现不良的涌现行为,并及时修复。
单体架构也可以构建得像微服务架构一样透明,事实上也应该如此。区别在于,对于运行在不同进程中的服务,你必须清楚知道它们何时断开连接;而对于运行在同一进程中的库来说,这种透明性的重要性相对较低。
微服务团队通常期望为每个服务配备完善的监控和日志记录系统,例如展示服务运行或宕机状态,以及各类运营指标和业务指标的仪表盘。熔断器状态、当前吞吐量和延迟等细节,也是我们在实际应用中经常见到的监控内容。
演进式设计
微服务实践者通常具有演进式设计背景。他们将服务拆分视为一种额外工具,帮助应用开发人员在不降低开发速度的前提下控制应用变更。控制变更并不意味着减少变更;只要拥有正确的态度和工具,就可以频繁、快速且可控地修改软件。
每当尝试将软件系统拆分为组件时,都会面临一个问题:应该如何划分这些组件?我们依据什么原则来拆分应用?组件的关键属性是可独立替换和可升级¹²。这意味着我们要寻找那些可以重写某个组件,而不影响其他组件的边界。事实上,许多微服务团队更进一步:他们明确预期许多服务在长期演进过程中会被废弃,而不是一直演进下去。
海外某新闻媒体网站就是一个很好的例子。它最初是作为单体应用设计和构建的,但后来逐渐向微服务架构演进。单体应用仍然是网站的核心,但他们更倾向于通过构建使用单体应用 API 的微服务来添加新功能。这种方法尤其适用于那些本质上具有临时性的功能,例如为体育赛事制作的专题页面。网站的这部分功能可以使用适合快速开发的语言迅速搭建,并在赛事结束后直接移除。我们也曾在一家海外金融机构看到类似做法:他们会根据市场需求添加新服务,并在几个月甚至几周后将其废弃。
这种对可替换性的强调,是模块化设计更普遍原则的一个特例:根据变更模式来驱动模块化¹³。应该将会同时发生变化的内容放在同一个模块中。系统中很少变化的部分,应该与当前频繁变化的部分放在不同服务中。如果你发现自己反复同时修改两个服务,那通常表明它们应该被合并。
将组件放入服务中,可以实现更细粒度的发布计划。在单体架构中,任何变更都需要重新构建并部署整个应用。而采用微服务架构时,通常只需要重新部署被修改的服务。这可以简化并加快发布流程。缺点是,你必须担心某个服务的变更会影响它的其他使用方。传统集成方式通常试图通过版本控制解决这个问题,但在微服务领域,最好只有在不得已时才使用版本控制。我们可以通过合理设计服务,使其尽可能容忍服务提供方的变化,从而避免大量使用版本控制。
微服务架构是未来趋势吗?
本文的主要目的,是阐述微服务架构的核心思想和原则。我们认为,微服务架构风格非常重要,值得企业应用认真考虑。我们近期已经运用这种风格构建了多个系统,也了解其他一些正在使用并青睐这种方法的团队。
据我们所知,在某些方面引领这种架构风格的,包括海外某些电商企业、流媒体公司、新闻媒体网站、政府数字服务机构、房地产平台、技术公司和保险比价平台等。2013 年的技术会议中,也出现了许多公司正在向微服务架构转型的案例,其中包括海外某些持续集成服务商。此外,还有许多组织长期以来一直在实践我们所说的微服务架构,只是从未正式使用过这个名称。通常,这种架构会被称为 SOA,尽管正如我们所说,SOA 本身也存在许多相互矛盾的形式¹⁴。
尽管有这些积极经验,我们并不认为微服务就是软件架构的必然未来。虽然与单体应用相比,我们目前的体验是积极的,但我们也意识到,实践时间仍然太短,还不足以做出全面判断。
架构决策的真正后果往往要在几年后才会显现。我们见过一些项目:优秀团队虽然非常重视模块化,却仍然构建了单体架构,而这些架构随着时间推移逐渐衰败。许多人认为,微服务架构不太可能出现这种衰败,因为服务边界清晰明确,难以绕过。然而,只有当我们看到足够多的系统运行足够长时间之后,才能真正评估微服务架构的成熟度。
微服务架构也可能因为许多原因而发展缓慢。任何组件化尝试能否成功,都取决于软件与组件划分方式之间的契合程度。准确划分组件边界并不容易。演进式设计认识到边界确定的困难,因此强调易于重构的重要性。但当组件变成通过远程通信协作的服务时,重构难度会远高于进程内库。跨服务边界移动代码非常困难,任何接口变更都需要相关参与方协调,需要增加多层向后兼容机制,测试也会变得更加复杂。
另一个问题是,如果组件之间无法清晰组合,那么你所做的可能只是把复杂性从组件内部转移到了组件之间的连接上。这不仅只是转移了复杂性,而且是把复杂性转移到了一个更难控制、更不明确的地方。当你只关注一个小型、简单组件的内部时,很容易误以为一切都很理想,却忽略了服务之间错综复杂的连接。
最后,团队能力也是一个重要因素。技术能力更强的团队往往更愿意采用新技术。但对高水平团队有效的技术,对能力较弱的团队未必同样适用。我们已经见过许多能力较弱的团队构建出混乱的单体架构;如果这种混乱出现在微服务架构中,其后果如何还需要时间观察。糟糕的团队总是会构建出糟糕的系统。在这种情况下,很难判断微服务究竟会减少混乱,还是让局面变得更糟。
我们听到过一种合理观点:不应该一开始就采用微服务架构,而应该先构建单体应用,并保持良好的模块化;等到单体应用真正出现问题时,再将其拆分为微服务。尽管这种建议也并不完美,因为优秀的进程内接口通常并不等同于优秀的服务接口。
因此,我们是带着谨慎乐观的态度写下这篇文章的。到目前为止,我们对微服务架构的了解已经足够让我们相信:这是一条值得探索的道路。我们无法确定它最终会走向何方,但软件开发的一大挑战正在于此:你只能基于当前掌握的不完整信息做出决策。
文章包含AI辅助创作:微服务架构是什么?定义、特点、优缺点与实践原则,发布者:su,转载请注明出处:https://worktile.com/kb/p/3975150
微信扫一扫
支付宝扫一扫