微服务

——Martin FowlerJames Lewis

原文

微服务

新的架构术语——微服务

在过去的几年兴起了一个新的架构术语——微服务。它用来描述将软件应用程序设计为一组可以独立部署的服务的特定方法。这种架构风格虽然没有明确的定义,但是其从组织,业务功能方面呈现出一些共同特征:自动化部署,端点智能化,语言和数据去中心化管理。

微服务——在纷繁复杂的软件架构领域出现的又一新名词。虽然我们的本能反应是对此类事物不屑一顾。但是,我们发现,微服务描述的软件系统风格越来越具有吸引力。我们看到在过去的几年里许多项目采用了这种风格,并且到目前为止效果是积极的。甚至,对于我许多同事来说,这已经成为构建企业级应用的首选风格。然而遗憾的是,没有太多的关于概述什么是微服务风格以及如何使用微服务的信息。

简单来说,微服务架构风格是一种把单个应用系统划分成一套小的服务来进行开发,每个服务拥有自己的进程, 彼此使用轻量级的机制,如HTTP资源API,进行通信,这些服务围绕业务功能构建,通过全自动化部署设施进行独立部署,对它们尽可能的分散管理,可以为它们选择不同的开发语言,选择不同的数据存储技术存储数据。

和单体风格——使用单个单元构建单体应用的方法——进行比较有助于对微服务风格的说明。企业级应用通常由三个部分构成:客户端UI(由运行在客户端浏览器里的HTML页面和脚本组成)、数据库(有许多插入到通用的,通常是关系型的DBMS的表组成)、服务端应用。服务端应用处理HTTP请求,执行域逻辑,返回或更新数据库数据以及选择并构建HTML视图发送给浏览器。服务端应用是一个单体——一个单一的逻辑执行体。任何对系统的改变都需要为服务端应用编译并部署一个新的版本。

这样的单体服务器是构建系统的自然而然的方法。你处理请求的所有逻辑都运行在单个进程中。同时它允许你依靠编程语言的基本特性把应用程序拆分成类、函数和命名空间等。需要注意的是,你可以在开发机器上运行并测试应用,使用部署管道确保对应用的修改经过适当的测试然后部署到生产环境。你可以在一个负载均衡后面运行多个这样的单体应用实例来实现水平扩展。

单体应用可以成功,但人们也越来越对其感到沮丧,尤其是当更多的应用部署到云上的时候。(应用各部分的的)变更周期被捆绑在一起——对应用一个小的部分的修改都要求整个应用重新构建和部署。随着时间的推移,单体应用通常也很难维持一个好的模块化结构,很难做到一个本来仅影响一个模块的变更只需要修改这个模块就行(注:也就是说:本来一个变更业务上看上去只需要修改某个模块,但事实上,由于随着时间的推移,系统渐渐"腐败",模块间耦合增加,导致需要修改其他的模块。)。扩展也只能就整个应用而不是应用的的某个部分进行,从而导致消耗更多资源。


Figure 1: Monoliths and Microservices

上述单体应用带来的困扰促使微服务架构风格的出现——用一组服务来构建应用系统。每个服务可以独立部署和伸缩,每个服务都提供固定的模块边界,不同的服务甚至允许使用不同的编程语言来编写。而且不同的服务可以由不同的团队来管理。

我们不会说微服务风格是一种新颖的风格或创新,因为它的根源至少可以追溯到Unix设计之道。然而,我们认为没有足够多的人在考虑使用微服务架构。我们认为如果使用微服务架构,软件开发会变得更好。

微服务架构特征

我们不说存在一个对微服务架构风格的正式定义。然而我们可以尝试描述下我们所看到的符合这种架构风格的共同特征。与任何概括共同特征的定义一样,不是所有的微服务架构都具有所有的特征。但是我们希望大多数微服务架构都具有大多数的特征。虽然我们这些作者已经是这个相当宽松的社区的积极分子,但我们的目的还是想描述在我们自己的工作中和我们所了解的团队做出的类似的努力中所看到的东西。不过,我们不会做出什么是符合微服务架构的定义。

组件服务化

在软件行业,我们一直希望通过把组件组装在一起的方式来构建系统。就像我们看到的物理世界许多创造事物的方式一样。在过去几十年里,我们看到了作为大多数语言平台一部分的大量的公共库的显著发展。

当我们谈论组件的时候会碰到一个难于定义的问题,就是组件是什么?我们的定义是,组件是一个可独立替换和升级的软件单元。

微服务架构会使用库,但它实现组件化的主要方式是把系统拆分成服务。我们定义的是可以链接到程序并在内存中被函数调用的组件,而服务则是通过Web服务请求或者RPC之类的机制进行通信的进程外的组件。(这和许多使用OO方法设计的程序里的服务对象是不同的概念)。

使用服务而不是库作为组件的一个主要原因是服务是可独立部署的。如果你有一个由位于单个进程里的多个库组成的应用程序,那么对于任何一个库的修改都会导致整个应用程序的重新部署。然而,如果应用程序被分解成多个服务,那么你就可以做到许多针对单个服务的修改仅需要重新部署该服务本身。这不是绝对的,有些变更会改变服务接口,这会导致一些协调的发生(注:受影响的服务需要调整接口的调用)。但一个好的微服务架构的目标就是通过内聚的服务边界和服务契约机制的演进让影响最小化。

使用服务化组件另外一个结果是组件接口变得更清晰。许多语言没有一个良好的机制来定义一个清晰的可发布接口。 通常我们仅通过文档和规则来防止客户破坏组件的封装,这导致组件间过度紧密耦合。服务间通过明确的RPC机制进行可以更容易避免这种情况发生。

但这也有缺点。RPC比进程内调用昂贵,而且这种RPC只能是粗粒度的,这往往导致使用起来比较笨拙。如果你需要改变组件间的职责分配,那么跨进程边界的行为迁移将更困难。

咋一看,我们发现服务就是对应运行时进程,但这仅仅是表象。一个服务可以由多个总是一起开发和部署的进程组成。比如一个服务有一个应用进程和仅被该服务使用的数据库。

按业务功能划分组织

当要拆分一个大的应用的时候,通常管理层主要从技术层面着手,从而会产生UI团队、服务端逻辑团队和数据库团队。当团队是这样划分的时候,即使一个简单的变更也会导致跨团队进行时间估计和预算审批。"机灵"的团队为了他们自己的便利会两害相权取其轻——强制把逻辑放在他们能访问的应用之中。换句话说,逻辑将无处不在(注:大家为了避免跨团队沟通的麻烦,尽可能的在自己可控的范围内注入业务逻辑,导致“逻辑无处不在”,同时因为不会优先考虑系统模块高内聚和低耦合,导致系统“腐化“)。下面是一个康威定律的例子。

任何设计系统(广义上定义的)的组织设计出来的系统,其结构都将和组织沟通结构一致。

——Melvyn Conway, 1967


Figure 2: Conway's Law in action

微服务拆分的方法是不同的,拆分的服务是按照业务功能(业务面)来划分的。这样的服务需要为其业务功能完成全栈的软件实现,包括UI,持久存储及所有的外部协作。因此,(实现服务的)团队也需要是跨功能的团队,需要拥有开发所需的,如用户体验,数据库和项目管理等完整的技能。


Figure 3: Service boundaries reinforced by team boundaries

www.comparethemarket.com是这种组织的一家公司。跨功能团队负责构建和营运每个产品,每个产品被划分成多个独立的服务,服务间使用消息总线通信。

大型单体应用也总是能够按业务功能进行模块化,虽然这种情况不常见。当然我们会力促一个构建单体应用的大型团队按业务线拆分自己。我们在这里看到的主要问题是:他们往往是围绕太多的上下文来组织的。假如单体应用横跨许多这样的模块化的边界,对团队的个体来说是很难短期记住这些模块的。并且,我们看到那些模块化的边线要求遵守大量的规则。组件服务化所必然要求更明确的分隔,这使得保持清晰的团队边界更容易。

产品而非项目

我们看到,大多数应用开发工作使用项目模式,其目标是发布一些被认为完成的软件。软件完工后被移交给运维部门,然后项目团队解散。

微服务支持者倾向于避免这种模式,而是秉承一种团队应该拥有整个产品的生命周期的理念。对这种理念的共同启发是来自亚马逊的“你构建,你运行”的理念。开发团队负责生产环境的软件,这使得开发人员能够和生产环境里的软件保持日常的联系,同时也增进开发人员和客户之间的联系,因为他们至少要承担一些客户支持的工作。

产品心态与业务功能紧密相连。它与把软件看作一套(需要开发)完成的功能不同,它是一种持续的关系——软件如何帮助用户提高业务能力。

没有理由解释相同的做法(注:产品心态)为什么不能应用到单体应用 。不过更小粒度的服务更易于建立服务开发者和用户之间的人际关系。

智能端点和哑管道

当在构建不同进程间的通信结构的时候,我们看到许多产品和方法强调把重要的处理放到通信机制本身里面。一个很好的例子就是企业服务总线(ESB)。ESB产品通常会包含复杂的设施用于消息路由,编排和转换,以及业务规则应用。

但微服务社区更偏好另外一个方法:智能端点和哑管道。微服务构建应用程序以高内聚低耦合为目标。他们拥有自己的域逻辑,扮演着更像经典Unix风格的过滤器(filter)角色,接受请求,然后执行相应的逻辑,最后产生应答。这是通过简单的RESTful协议进行编排,而不是如WS-Choreography 和BPEL等复杂协议或者中心化工具编排。

使用最广泛的两个协议是HTTP请求/应答资源API和轻量消息。对前者最恰当的描述是:

是web,而不是位于web之后

——Ian Robinson

微服务团队使用万维网(更大程度上是Unix)相关的协议和原则。对开发和运维来说,经常使用的资源可以毫不费力的缓存起来。

第二个普遍方法是使用轻量的消息总线发布消息。所选择的基础架构典型的是哑的(仅仅是在扮演消息路由)——简单的实现,如RabbitMQ或者ZeroMQ,它们仅提供可靠异步管道。其他的处理仍然位于产生或消费消息的服务端点中。

在单体应用中,组件在进程内执行,通过方法或函数调用进行通信。把单体应用改变成微服务最大的问题在于改变通信方式。直接把内存中的方法调用转换成RPC会导致通信的繁琐且无法顺利执行。因此,你需要把这种细粒度的通信(函数调用)替换成粗粒度通信。

(注:智能终端,哑管道是相对类似基于ESB构建起来的SOA中的哑终端,智能管道而言的。随着微服务架构的出现,基于K8s+Service Mesh的服务网格系统,已经演变成了智能平台和聚焦业务逻辑的智能服务。如下图所示:


SOA vs MSA vs CNA

去中心化治理

中心化管治的结果最终都倾向于形成单一技术平台的标准化。经验表明这会造成一些局限性——不是每个问题都是钉子,也不是每个方案都是锤子(注:也就是说单一的方案不能解决所有的问题)。我们更喜欢使用合适的工具来完成相应的工作。虽然单体应用也可以一定程度上获得使用不同开发语言的好处,但这并不普遍。

把单体应用拆分成服务之后,我们就可以选择不同的开发语言来构建这些服务。你想使用Node.js起来一个简单的报表页面吗?去弄吧。你想使用C++写一个特别简单的且近乎实时的组件吗?好,没问题。你想变换不同风格的数据库来更好地适应组件的读取行为吗?我们有那种技术来重建它。

当然,只是因为你能够这样做,但并不意味着你应该这样做——你有这样的选项来分割你的系统。

构建微服务的团队也喜欢使用不同标准化的方法。不像把一套定义好的标准写在纸上,他们更喜欢这样的理念:创造有用的工具,其他的开发者也可以用来解决他们遇到的类似问题。这些工具从实现代码中收集而来,并分享给更广泛的团队。有时会以但不限于内部开源的方式使用。如今git和github已经成为版本控制系统的事实上的选择,在公司内部的开源实践已经变得越来越普遍了。

Netflix是奉行这种哲学的组织的一个好例子。分享有用的,重要的是经过实战验证的代码库会鼓励其他开发者以类似的方式解决相似的问题,然而,如果需要也可以选择不同的方法。这些共享库倾向于解决数据存储和进程间通信等公共问题,也包括下面我们要进一步讨论的基础架构自动化。

对于微服务社区来说,(对服务契约的)日常管理(overhead)尤其不受青睐。这不是说,社区不认为服务契约具有价值。恰恰相反,往往是因为认为它们更具价值,所以,他们在寻找管理这些契约的不同方法。如Tolerant Reader消费者驱动型契约模式经常应用到微服务。这些模式有助于服务契约独立进化。作为构建的一部分,执行消费者驱动型契约,可以增加你的信心,为你的服务是否工作提供快速反馈。实际上,我们知道有一家澳大利亚的团队使用消费者驱动契约来驱动构建新的服务。他们使用简单的工具来定义服务契约。这成为新服务在编码之前的自动构建的一部分。随后服务实现仅需要满足这些契约就可以。这是一种优雅的方法,以此可以避免构建新的软件时遇到“YAGNI”的困境。这些技术和围绕这些技术出现的工具降低了服务间一时的耦合,减少了中心化契约管理的需求。

也许去中心化管治的最高境界就是亚马逊所宣扬的“构建并运行它”的理念。团队对构建的软件的方方面面负责,包括7X24小时运维。这种级别职责的下放绝对不是惯常做法,但我们确实看到越来越多的公司把这些职责赋予开发团队。Netflix是采用这种理念的组织。被自己写的页面凌晨3点喊醒必然促使你关注自己代码的质量。传统中心化管治模式与这种思想缺相去甚远。

去中心化数据管理

去中心化数据管理以许多不同的方式存在。从最高的抽象级别来看,它意味着世界的概念模型在不同的系统之间是不同的。当在大型企业集成时,这是一个常见的问题。客户销售试图将会与(系统)支持视图不同。销售视图中所谓的客户可能根本就不会出现在(系统)支持视图里。即使出现夜可能具有不同的属性,或者(更糟糕的)属性相同,但语义却有细微的差别。

这种情况在不同的应用之间是常见,但也可能出现在一个应用的内部,特别是当应用被拆分成单独的的组件的时候。考虑此问题的一个有用的方式是领域驱动设计(DDD)中的界限上下文概念。DDD将一个复杂的域分解成多个有边界的上下文,并在它们之间建立映射关系。这个过程对建立单体应用和微服务架构应用都是有用的,但服务和使其更清晰的上下文边界之间确实存在自然的关联,而且像我们在业务功能那节讨论的一样这种关联得以增强。(注:服务是按照有上下文边界的业务面来划分的,所以服务边界自然清晰。)

除了概念模型去中心化,微服务的数据存储也可以去中心化。单体应用偏向于使用单一逻辑数据库实现数据持久化,企业通常也偏向于所有应用使用一个单一的数据库——许多这些决定是基于供应商的商业许可模式做出的。微服务则偏好于让每个服务管理它自己的数据库,可以是相同数据库的不同实例,也可以是完全不同的数据库——这种方法叫作“复式持久化”(Polyglot Persistence)。你可以在单体应用中使用这种数据持久化方法,但微服务中使用更多。


跨微服务的数据职责去中心化也意味着数据更新的管理。处理数据更新的常用方法就是要使用事务保证多个资源的一致性。这个方法经常在单体应用中使用。

这样使用事务有助于保持一致性,但产生临时耦合,这在跨多个服务时是有问题的。众所周知,分布式事务是不易于实现的。作为微服务架构的一个结果,就是强调服务间无事务协作。作为一个明确的共识就是,一致性仅仅是最终一致性,中间出现的问题由一些补偿操作来处理。

选择以这种方式来管理不一致性对许多开发团队来说是一个挑战,但它往往和业务实践是相似的。业务常常会运用一定程度的不一致性来快速响应需求,而使用某种逆向过程来应对错误(注:如事务回滚)。只要修正错误的成本小于保持更高一致性情况下业务损失的成本,这种权衡之计就是值得的。

基础架构自动化

基础架构自动化技术在过去的几年取得了极大的进步——随着云计算的发展,尤其是AWS,其从构建、部署到运维微服务的复杂度都得到了降低。

许多使用微服务架构的产品和系统都是由在持续部署以及它的前导持续集成方面经验丰富的团队构建。使用这种方式构建软件的团队广泛使用基础架构自动化技术。下图描述了一个构建管道。


Figure 5: basic build pipeline

由于这里不是介绍持续发布的文章,所以我们只在这里关注下几个关键特性。 我们想获得尽可能多的自信,我们的软件可以工作,为此我们运行许多自动化测试。 把工作的软件送入这个构建管道就意味着自动部署到新的环境。

单体应用将被相当愉快的构建、测试以及通过这些环境(注:如上图所示,集成测试环境、UAT环境和性能测试环境)。事实证明一旦你投资部署单一应用的到生产环境的管道自动化,那么部署更多的应用似乎不再那么恐怖。记住,持续部署的目的之一就是使得部署过程变得无趣,所以,不管一个还是三个应用程序,只要部署过程仍然是无趣,那就无所谓。

另外一个我们看到团队大量使用基础架构自动化的领域是生产环境的微服务管理。我们上面断言只要部署是无聊的,那么单体和微服务部署没什么不同,但相反,各自运维视觉完全不同。


Figure 6: Module deployment often differs


容错设计

使用服务作为组件的一个结果是应用程序需要设计能够容忍服务的失败。任何服务的调用都有可能因为服务无效而失败。客户端需要尽可能优雅的应对此情况。这相比单体设计是一个缺点,因为它为了处理服务调用失败而引入额外的复杂性。这也导致微服务团队不断的考量服务的失败如何影响用户的体验。Netflix的Simian Army在工作日引发服务甚至数据中心故障来测试应用程序弹性和对其监控。

这种生产环境的自动化测试足够让大多数运维团队在开始休假一周前就赶到胆战心惊。这不是说单体架构风格不能做复杂的监控设置,只是在我们所经历的里头比较少见。

因为服务可能在任何时候都会发生故障,所以迅速的发现故障,如果可能,自动恢复服务就显得很重要了。微服务应用非常重视监控,包括架构层面的指标监控(比如,每秒有多少读数据库的请求(rps))和业务相关的指标监控(比如,每分钟收到多少订单)。语义上的监控可以提供一个警报系统,显示什么情况正在变得糟糕,从而驱动开发团队进行跟进和调查。

这对微服务架构尤其重要,因为微服务对编排和“事件协作”的偏好可能会导致意外的行为。虽然许多权威专家盛赞意外收获的价值,但事实上,意外行为有时候可能是坏事。监控对迅速发现意外情况并处理至关重要。

单体应用可以和微服务一样透明的构建——事实上也应该这样。不同的是,你必须要知道运行在不同的进程里的服务什么时候连接断开了。但这种透明对于同一个进程里头的库来说基本没什么意义。

微服务团队期待看到对每个独立的微服务的细致的监控和日志设置,比如显示开关状态和各种运维及业务相关指标的仪表盘。另外像我们日常遇到的例子,如关于断路器,当前吞吐量和延迟的详细信息。

演进式设计

微服务的实践者通常都具有演进式设计的背景,把服务分解看作是一个长远的工具,帮助应用程序开发者在不减缓应用程序的变化速度的同时控制变化。控制变化不是必须抑制变化——有正确的态度,好的工具,你能够使得软件频繁快速的变化,并使其得到良好的控制。

无论你什么时候想把软件系统拆分成组件,你都会面临如何拆分的问题——我们应该遵循什么原则来切分我们的应用? 组件的一个关键属性是关于独立替换和可升级的概念——这意味着,我们要寻找一个点,我们可以想象,在这个点的代码可以重写但不会影响它的协作者。实际上,许多微服务团队考虑的更多,他们明确的期望许多服务可以被废弃而不是长期演进(注:也就是这个服务不仅可以独立重写,替换,甚至可以抛弃而不会影响其他的部分)。

Guadian网站是一个很好的例子,它被设计和构建成了一个单体系统,但在朝微服务方向演进。单体仍然是这个网站的核心。不过他们更喜欢使用微服务增加新的功能,这些微服务调用单体的API. 这种方法对于提供本质上是临时性的功能,比如体育赛事相关的页面尤其便利。作为网站一部分的这种功能可以使用快速开发语言迅速的组织在一起,一旦赛事结束就移除掉。我们在金融机构也看到了类似的处理,一个新的服务因市场机遇被创建加入系统,几个月甚或几周后就撤掉。

强调可替换性是更通用的模块化设计原则的一个特例,通过变化模式驱动模块化(注:Kent Beck在《Implementation Patterns》中提到的变更速率原则——变更速率相同的数据和逻辑放在一块,而不同的要分开)。你要把在同一时间变化的东西放在同一个模块里。很少变化的系统部分应该和当前不断变化的部分处于不同的服务里。假如你发现自己在不断重复的同时修改两个服务,那么意味着这两个服务应该合并。

以服务的方式构建组件使得可以制定更加细粒度的发布计划。单体应用的任何变更要求整个应用的完全重新构建和部署。然而,使用微服务,你只需要重新部署你修改的服务。 这可以简化并加速发布过程。确定就是你不得不关心对服务的变更是否会影响它的使用者。传统的集成使用版本化来应对这个问题。但是在微服务的世界里更偏向于把版本化作为最后的手段。我们可以把服务设计对它所以依赖的服务的变化尽可能的容错来避免大量的版本化。

微服务是未来吗?

撰写该文的主要目的是阐述微服务的主要思想和原则。这同时让我们更清楚的认识到微服务架构风格是一个重要的思想——值得为企业级应用认真考虑。我们目前构建了数个此类风格的系统,也知道一些公司在使用和喜好这种构建服务的方法。

我们了解在某种程度上作为推动这种架构风格的先锋包括:亚马逊、Netflix、Guardian、英国政府数字化服务、realestate.com.au、 前进报(Forward)以及comparethemarket.com。 2013的巡回大会上有很多转向使用类似微服务的如Travis CI的东西的公司的例子。同时,也有许多组织长期在做的事情,我们把它们归类为微服务,只是没有叫这个名字而已(通常会打上SOA的标签——尽管,如我们所说,SOA以很多相互矛盾形式存在)。

然而,尽管有这些积极的经验,但我们并不确定微服务就是未来软件架构的方向。虽然到目前为止我们所遇到的(微服务架构方面的情况)相对于单体应用来说都是正面的,但我们也清楚这样一个事实:那就是我们所以经历的时间还是不够,以至于无法做出处充分的判断。

往往,只有在你做出了架构决策几年后其真正的结果才足以显现出来(注:证明当初决策是对还是错)。我们看到的项目,有一个好的团队,带有很强的模块化意念,可构建的单体架构多年来已经腐朽了(注:模块化边界变得模糊,耦合度升高,内聚度降低)。许多人认为这种腐朽在微服务里头是几乎不可能发生的,应为服务的边界是清晰的,难以遮掩的。然而,除非我们看到足够多经年的系统,否则我们无法真正评估微服务架构成熟度。

当然,人们有理由预料到微服务不会成熟的很好。对于组件化的任何努力,其成功在于如何使得组件很好的适配软件。准确的找出组件的边界应该位于哪里是很困难的。演化式设计意识到正确获取边界的困难和重构的重要性。但是当你的组件服务是使用远程通信时,重构它们会比重构进程内的库更加困难:代码跨服务边界移动是困难的,接口的变更需要参与者调整,需要增加向后兼容层,测试也会变得更加复杂。

另外一个问题就是假如你没有清晰的设计好组件,那么你所做的其实就是把复杂性从组件内部转移到了组件之间的连接上。这不仅仅把复杂性转移了,而且还转移到了模糊及难以控制的地方。当看着小且简单的组件内部,而且组件间也没有繁杂的连接,那么你很容易相信事情会变得更好。

最后,团队技能因素。新的技术易于被技能更好的团队采用。 但在高技能的团队使用的高效技术在低技能团队不一定凑效。我们见过大量的例子,低技能团队构建混乱的单体架构。不过要知道这种混乱发生在微服务架构里会出现什么情还需要时间。糟糕的团队总是创建糟糕的系统——很难判断微服务是否减少了这种情况下的混乱,还是使它变得更糟。

我们听到的一个合理的观点是你一开始不应该使用微服务架构,而是单体架构,同时保持模块化, 然后一旦单体变得有问题时拆分成微服务。(虽然这个建议不是很理想,因为好的进程间接口通常并不一定是好的服务接口。)

所以我们谨慎乐观的说:到目前为止,根据看到的足够多的关于微服务的信息,我们认为微服务是一个值得尝试的方向。我们不能确定最终如何,但软件开发的挑战之一就是你只能仅仅凭你目前能获取的不完善的信息来做出决定。

推荐阅读更多精彩内容