微服务

原文:Microservices

翻到 23 的时候看到一篇疑似译文,不过并未注明原作和出处,恩。反正我这是学习笔记。

“微服务”——另一个软件架构的流行词。本来我们应该对这类东西扔去一个蔑视的眼神,不过这个名词所描述的软件系统越来越多。过去几年中我们看到很多这种风格的项目,这甚至成为构建企业应用的缺省方式。然而可惜的是,微服务是什么?如何实现微服务?这两个问题却没有明确的解答。

简单来说,微服务架构是一种开发应用的方法,应用由一系列的小服务构成,每个小服务都拥有各自的进程,并提供轻量级的通讯机制(一般是 HTTP 资源 API)。这些服务围绕业务能力进行构建,并且通过全自动方式进行部署。需要有一个管理中心对这些服务进行管理,这一中心可能用不同语言、使用不同数据存储来进行实现。

微服务这个词最早是在 2011 年 5 月威尼斯一个软件架构会议上讨论时,用于描述与会人员当时所探索的通用架构模式。在 2012 年 5 月,同一组人确定了这个最合理的命名。James 在 2012 年 3 月的举行于 Krakow 的 33rd Degree 会议上,以 [Microservices - Java, the Unix Way] 为题,以案例研究的形式阐述了其中的部分理念;几乎同时,Fred George 发表了题为 MicroService Architecture的演讲。Netflix 的 Adrian Cockcroft 称这种方式为 “细纹理 SOA”,他和文中提到的其他人(Joe Walnes, Dan North, Evan Botcher and Graham Tackley),是这一架构的先驱者。

在解释微服务之前,先比较一下 monolithic( 单一庞大的,整块巨石构成的 )模式:以单个单元构建而成的引用。企业应用通常由三个部分组成:一个客户端的用户界面(包含 HTML 页面以及运行在客户浏览器中的 JavaScript)、一个数据库(通常是一个关系型数据库管理系统,包含很多数据表)以及一个服务端应用。服务端会处理 HTTP 请求,执行业务逻辑,对数据库中的数据进行读写,选择和处理 HTML 视图,并发送给浏览器。这一个服务端应用就是一个单一逻辑的可执行应用,所有对系统的变更都需要构建和发布这一应用的新版本。

巨石 这个称呼似乎不错。

monolith 这个词在 Unix 社区用了一段时间了。出自 The Art of Unix Programming,用来称呼过分庞大的系统。

Monolithic 服务器是一个自然而然的想法。对所有请求的处理都是在同一个进程里面实现,可以使用语言的功能来把应用拆分为类、函数以及命名空间。可以在开发者的笔记本上运行和测试应用,并利用发布管线来确认变更都通过测试并部署到生产环境。应用能够用多实例的方式进行水平扩展。

Monolithic 应用是可以成功的,不过随着人数的增加,可能就让人不满了 —— 尤其是更多应用被部署到云环境之中的情况下。即使是应用中的一小部分的变更,也会要求整个应用重建和部署。项目的模块化结构也越来越难于维护,这又导致变更的影响范围很难被限制在模块之内。只能对整个应用的负载进行伸缩,也浪费了更多的资源。

sketch

图 1 :Monoliths 和 微服务

  • Monolithic 应用把所有的功能放入一个进程,只能以应用的粒度进行集群设置。
  • 微服务架构把每个功能元素放入单独的服务中,可以按需对服务进行集群设置。

这些不满造就了微服务这样的架构风格:以一套服务的方式构建应用。每个服务都可以独立的部署和伸缩,每个服务都有严格的边界,甚至可以用不同的语言来开发不同的服务。每个服务也可以被不同的团队来管理。

我们不认为微服务是个创新,他的根基就是 Unix 的设计原则。然而我们认为,使用这一架构思想的人还不够多,否则软件开发的过程会比现在愉快得多。

微服务架构的特性

微服务架构还没有一个正式的定义,然而我们可以尝试描述一下这一架构的特性。当然,并不是说所有微服务架构都会具有所有这些特性,我们希望多数微服务应用能够体现多数的特性。这一社区很松散,我们尝试描述我们自己的团队以及我们知道的其他团队的相关工作,而不是尝试制定一个需要严格尊重的定义。

服务方式实现的组件化

自从我们进入软件工业开始,就重复做着一个美梦——像物理世界一样,用可插接的零件来构建系统。过去二十年中,我们也看到这方面的巨大进步,多数语言平台都有了颇具规模的常规库。

当说起组件时,我们陷入一个困境——如何定义组件?我们说组件是软件的一个可替换的、可升级的单元。

微服务架构也会使用库,但是这一架构实现组件化的方式是把软件拆分为服务。库一般会被 Link 到程序之中,并在内存中被调用;而服务是进程外的组件,只能通过 Web 服务或者远程调用等方式进行调用(这是一概念和很多面向对象的服务对象概念是不同的)

包括我们自己在内的很多面向对象的设计者,在 领域驱动设计模型 中使用服务对象这个词,用于描述一种没有绑定到实体,但却负载业务过程的对象。这跟本文中使用的“服务”一词是完全不同的概念。很不幸,我们必须忍受这个多义词的困扰。

把服务看成组件(而不是库)的一个主要的原因是,服务是可以独立部署的。如果你有一个应用在单独进程中包含了多个库,对其中任何组件的变更都会导致整个应用的重新部署。如果应用解耦称为多个服务,就可以只对发生变更的服务进行重新部署了。但有些变更会导致服务接口的变更,从而对依赖这一服务的其他服务造成影响,因此独立部署这一特点并不是绝对的。好的微服务架构要通过服务边界的内聚,以及在服务协议中封装升级机制,来减少这种情况的发生。

软件的边界

另外一个用服务做组件的结果就是更直接的组件界面。多数语言并没有一个好的机制来定义一个清晰的公开接口。一般只会有文档和规则来阻止客户端破坏组件的封装,这就会造成组件间的耦合。服务方式则可以比较容易的通过限制远程调用的机制来避免这种情况。

如此使用服务也会有坏处。相比进程内调用,远端调用的成本相对高昂,(为了节省调用成本),就需要把远程 API 设计成为粗粒度的结构,但这样的话,又会难于使用。如果想要在不同组件之间移动功能,这一难度在使用服务(跨越进程)的情况下会大大增加。

服务似乎可以映射为进程,不过只是似乎。一个服务可以包含很多一起开发和部署的进程,例如一个应用进程和一个该进程专属的数据库。

根据业务功能进行组织

在尝试将一个大应用拆分开来的过程中,一般是着眼于技术层面,代领 UI 团队、服务端团队、以及数据库团队协作完成任务。如此的组织方式下,即使是简单的变更,也需要团队间的协作,需要完成时间和预算的相关流程。有的团队会围绕这一情况进行优化,两害相权取其轻——能访问哪个应用就把逻辑放到哪里。换句话说,逻辑到处都是。这是康威定律的具体体现。

一个组织设计出来的系统,其结构取决于该组织的通讯结构。

  • Melvyn Conway, 1967

conway

图 2:康威定律

微服务的拆分规则是不同的,是以业务能力维度来进行拆分的。服务实现某种业务能力的一个方面,包括用户界面、持久存储,以及任何的外部协作。因此,这个团队也要是多功能的,具有这项开发任务的所有必要技能:用户体验、数据库以及项目管理。

服务边界和团队边界

www.comparethemarket.com 采用了这样的组织结构。具有交叉功能的团队负责建设和运维每个产品,每个产品都被拆分称为一系列独立的服务,服务之间使用消息总线进行通信。

大型的 Monolithic 应用也能围绕业务能力进行模块化。当然我们可以建立一个庞大团队来建立一个按业务线拆分的大应用。这一做法的主要问题是,这样的应用会跨越很多模块,相应的就会具有很多的上下文,团队成员的短期记忆很难适应任务的切换。而且这里面还有一个问题就是,要制定很多的规则,来保证模块的边界。

产品不是项目

我们所见的多数应用开发都是使用项目模型:把完成的软件交付出去。交付之后,完成软件的团队就解散了。

微服务的倡导者们建议避开这种模型,而建议由同一个团队掌握产品的整个生命周期。一个例子就是 Amazon 的 “谁开发谁运行”(You build, you run it),开发团队要承担软件在生产环境的运行情况。这让开发者每天都会关注到产品的运行情况,并增进同用户的沟通,至少会分担一部分支持的责任。

旧有模式把系统看成一堆待开发完成的功能,而产品心态则更进一步——关注软件是否能够帮助用户增强其业务能力。

Monolithic 应用也可以采用这一目标,但是小粒度的服务更容易在用户和开发者之间产生联系。

智能端点和哑管道

当在不同进城之间建立通信结构时,我们会看到很多产品和方法都试图把智能嵌入到通信机制当中去。一个明显的例子就是 ESB,ESB 产品经常会包含消息路由、编排、转换以及业务规则等功能。

微服务社区倾向于另外一种方式:*智能端点和哑管道*。微服务方式构建的应用力求高内聚和低耦合, ——每个服务都像 Unix 中的 Filter 一样工作——接受请求,处理,输出响应。使用简单的 REST 风格的协议,而不是 WX 或者 BPEL 之类的复杂协议。

最常见的两个协议是 带有资源 API 的 HTTP 请求-响应以及轻量级消息协议。最好的表达是:

成为 Web,而不是基于 Web
-- Ian Robinson

为了追求伸缩性的极致,有些组织最后会转向 protobufs 这样的二进制协议。使用了这样的协议,仍然是智能端点 + 哑管道的,只是为了性能损失了透明性。对大多数 Web 应用来说是无需做出这样的妥协的——透明本身也是明显的优势。

微服务团队使用的规则和协议,正是互联网的基础。常用的资源可以轻易地由开发或运维人员进行缓存。

另一种常用的方法是轻量级的消息总线。一般会选择哑管道类型(就是说只使用消息路由功能)例如 RabbitMQ 或者 ZeroMQ 这样的产品,除去提供可靠的异步功能,几乎没有其他能力。智能始终存在于生产或消费消息的服务端点。

在 Monolith 应用中,组件在进程内执行,通信是通过方法或者函数的调用来实现的。因此从 Monolith 迁移到微服务的最大问题就是通信方式的改变。简单的从内存方法调用转换为 RPC 并不合适。应该把通信从细粒度转为粗粒度。

去中心化的治理

中心化治理的一个后果就是单技术平台的倾向。经验表明,这种方法有其固有的局限性——不能用一个方案解决所有问题。我们建议用合适的工具对付特定的任务。Monolithic 应用在某些情况下也可以使用多种语言,但这并不是一个常规的情况。

把 Monolith 应用拆分为服务,我们就有了选择每个服务实现方式的机会。用 Node.js 来启动一个简单的报告页面?用 C++ 来完成一些接近实时的任务?为了更好的读取性能而选择其他风格的数据库?每一种问题我们都有办法分开解决了。

当然了,能用/会用什么办法,并不意味着就应该用这种办法。但是经过这样的拆分,你就有了选择。

构建微服务的团队倾向于一种不同的标准化的方法。与纸面规矩不同,我们更愿意制作有用的工具,帮助其他开发者解决类似的问题。这些工具一般来自于项目,分享给更多人,也不排除使用一种内部的开源模型。现在 Git 和 Github 已经成为版本控制系统的事实标准,开源实践也日益盛行。

Netflix 就是一个遵循这种哲学的例子。分享有用的、经过高强度测试的代码,鼓励其他开发者用相似的办法解决相似的问题,对有实际需求的另辟蹊径的解决方法也抱以开放的心态。分享库集中于一些基础问题,包括数据存储、进程间通信以及后面我们会讨论到的——基础设施自动化。

对于微服务社区来说,开销不是个有吸引力的问题。这并不是说社区不重视服务合同(协议),而是因为想要做到更多,Tolerant ReaderConsumer Driven 都是微服务中常见的模式。这些服务协议正在独立发展。在服务中采用 Consumer Driven 方式,能够快速获取服务是否正常工作的反馈信息。澳大利亚的一个团队就是使用这种方式。他们利用简单的工具来为服务定义合同。这一自动过程在服务代码编写之前就得以执行。服务只有在满足合同要求时才能被构建,这样在构建新软件的时候,就优雅的规避了 ‘YAGNI’ 困境。这些技术和工具不断成长,通过解耦服务的方式避免了对中心管制的依赖。

YAGNI 或者说“你不需要他”,是一个 XP 原则,在确定需要之前,不要加入新功能。

可能去中心化治理的最高点就是 Amazon 了。一个团队要负责其开发的软件的包括 247 运维在内的方方面面的工作。放权到这个层次当然不是很常见,不过我们看到越来越多的公司正在把职权推给开发团队。Netflix 是另外一个采用这种办法的公司。每天凌晨三点钟被传呼机叫醒,绝对会提升你编写代码时对质量的警觉度。这种理念同传统的中心治理模型渐行渐远。

Adrian Cockcroft 在 2013 年 11 月的演讲中特别的提到“开发者自助”以及“开发者负责运行”

去中心化的数据管理

数据管理的去中心化有很多的表现形式。在最抽象的层面,他意味着在不同的系统中,世界的模型是不同的。这在大企业的集成过程中很常见,例如在销售视图中一个称为客户的对象,在客服系统中可能完全不同。在不同环境下一个对象可能有不同的属性。

这种情况往往发生在应用之间,不过也可能在应用内部的组件之间发生。领域驱动设计观念中的限界上下文(Bounded Context)是一个有用的思考这一问题的途径。DDD 把一个复杂的域拆分为多个限界上下文,并在中间对关系进行映射。这一过程对 Monolithic 和微服务架构都有效的,不过微服务根据业务能力进行的拆分在这方面更清晰,更接近。

概念模型上的去中心化之外,微服务还对数据存储进行了去中心化。Monolithic 应用倾向于一个单独的逻辑数据库来存储持久化数据,因为数据库厂商的授权方式,企业往往也希望单独的一个数据库为多个应用服务。微服务则更希望每个服务管理自己的数据库,数据库可以是同样的,也可以是完全不同的,这种方式被称为混合持久化( Polyglot Persistence ),当然 Monolithic 应用也可以这样使用,但是这种用法更多的还是出现在微服务架构中。

Polyglot Persistence

微服务之间的数据的去中心化,隐含了更新管理的要求。一般的方法是在更新多个资源的时候,利用事务来保证一致性,这种方式在 Monoliths 应用中最为常见。

事务在解决一致性问题的同时,也造成了时间上的耦合,在多服务应用中实现也是有困难的。而分布式事务的难度可以说是街知巷闻了,因此微服务架构强调服务间的非事务协调,只对最终一致性做出保证,其他的问题通过补偿操作进行处理。

如何管理不一致的数据,也给开发团队带来新的挑战,不过这本来也是业务实践过程中常见的情况。现实世界中的业务经常会容忍一定程度的不一致,以此换取快速响应需求的能力,然后用某些反冲过程来处理不一致造成的问题。相对于保证高度一致性的成本,这种对不一致的妥协方式是行之有效的。

基础设施自动化

近年来,针对基础设施的自动化技术取得了长足的进步。云计算、AWS 带来的进步,成功的降低了构建、部署以及运维微服务应用的复杂性。

很多微服务架构的产品和系统都是由具有持续交付持续集成经验的团队所构建的。这种团队在自动化技术方面有很多应用,下图描述了构建流程:

basic build pipeline

图 5:基础构建流程

  • 编译,单元和功能测试(Build 环境运行)
  • 验收测试(部署到 Build 环境)
  • 集成测试(部署到集成环境)
  • 用户验收测试(部署到用户验收测试环境)
  • 性能测试(部署到性能测试环境)
  • 发布到生产环境

本文的焦点并非持续交付,因此这里只会提到一些关键点。我们要尽可能的保证我们的软件可以运行,所以要运行很多的自动测试。能够运行的软件在流程中的“前进”意味着自动部署到指定环境。

Monolithic 应用也可以被轻松的 Build,测试和推送到制定环境中。事实证明,一旦应用实现了自动化,那么应用的发布就不再惊悚了。需要强调的是,持续交付的目标就是让部署变得无聊,所以不管是一个还是三个应用,只要是无聊的,就是没有问题的。

这里似乎不够坦诚。很明显发布多个服务是比发布单个的 Monolith 应用更复杂的,幸运的是,这种复杂度可以通过工具化来降低。

另外一个需要自动化的场合就是管理正在生产环境运行的微服务应用。上面提到的部署过程区别并不大,然而在运维过程中,自动化就会产生明显的区别了。

module deployment oftern differs

  • Monolith:多个模块运行在同一进程中。
  • 微服务:模块运行在不同的进程中

图 6:模块部署的区别*

面向故障的设计(Desgin for failure)

既然把服务作为组件来使用,那么自然而然的,应用就应该适当的容忍服务的故障。每个服务的提供者都可能出现调用失败的情况,客户端必须尽可能的对此做出正确的应对。相对于 Monolithic 应用来说,微服务架构在这里引入了更多的复杂性,因此这是微服务架构的劣势之一。后果就是微服务团队必须要考虑服务失败对用户体验的影响。Netflix 的 Simian Army 项目 可以模拟服务甚至是数据中心的故障,来检测应用的恢复能力以及监控能力。

服务既然随时可能失败,那么对服务的快速检测,并尽可能的恢复服务,就很重要了。实时监控对于微服务应用来说是至关重要的,监控内容包括架构级别的要素(数据库的每秒请求数量),以及业务相关的维度(例如每分钟订单数)。监控系统应该能够在系统不正常的时候,警告开发人员介入调查。

这对微服务架构来说是很重要的,有了服务编排和事件协作,微服务架构表现了一定的自发性。虽然很多人认为自发行为是一大优势,然而自发行为也有失控的可能。所以监控在这里就至关重要了,当错误的自发行为产生时,能够快速的进行修复。

Monoliths 也可以像微服务一样的透明化——其实本该如此。不同之处在于,微服务架构中,必须知道断开的功能是属于哪个服务哪个进程的。在同一个进程中的话,这一过程就没什么用处了。

微服务团队期望有一个承载日志和监控的仪表盘,来展示服务状态以及业务信息。

迭代设计

微服务的实践者一般都有迭代式设计的背景,他们看到了服务分解这一特性,让开发者能够在控制应用的变更。变更控制并不意味着减少变更,而是说用正确的态度和工具来频繁、快速,可控的对软件进行变更。

尝试把软件系统拆分为组件时,会面对一个决策:怎么拆?一个独立的组件,应该是可以独立升级,并可整体被替换的——也就是说我们可以在不影响相关的其他组件的情况下,重写一个组件。事实上很多微服务组织更进一步,宁愿过分细碎,也不愿参杂不清。

Dan North 更愿意称之为可替换组件架构,而不是微服务。这似乎是我们后面要谈到的特性的一个子集。

Guardian 网站是一个很好的 Monolith 应用的例子,但是也已经像微服务方向发展了。Monolith 仍然是网站的主干,但是他们利用 Monolith 的 API,建立微服务,来实现新的功能。这种办法对一些短期的内部功能非常有效,例如一个体育赛事的专题页面。这种方式搭建的部分页面能够用快速开发的语言迅速上线,然后在赛事结束之后立刻停掉。我们已经在一些金融机构见到类似的做法:为一个市场机会、活动建立新的服务,结束后再将服务下线。

模块化设计的规则中,可替换原则,是一个让设计能够经得住变更的考验的重要部分。要保证同一个模块内的变化能够同步进行。系统中较少改变的部分应该沉淀到稳定的经过实践检验的服务中期。重复的同时修改两个服务的情况是一个应当做出服务合并决策的象征。

Kent Beck 以此作为 实施模式 中的一个设计规则。

把组件转换为服务,对于进度的保证也是颇有助益的。在 Monolith 应用中,所有的变更都需要完整的构建和部署。在微服务应用中,只需要重新部署发生变更的服务。这样就简化了发布流程,提高了发布效率。随之而来也有一个问题就是,服务的变更可能会破坏该服务的下游应用。传统的对付这一问题的方法是版本控制,不过在微服务世界中,这种办法只是权宜之计,我们要想方设法通过对下游服务的容错设计来应对上有服务的变化。

微服务就是未来?

本文的主要目的在于阐述微服务的思路和规则。通过这一过程,我们明确了微服务架构是值得认真对待的企业应用构建方式。我们以及其他一些团队最近用这种方式实施了很多项目;其中包括 Amazon,Netflix,The Guardian英国政府数字服务realestate.com.au 以及 comparethemarket.com。2013 年的会议中充满了转向微服务架构的例子——其中包括 Travis CI。另外,还有很多组织正跃跃欲试——虽然可能不叫微服务(经常还是会被称为 SOA,我们之前也说过,SOA 有很多相互矛盾的表达)

SOA 也不是这一形式的源头。我记得当 SOA 团队在本世纪初出现的时候就有人说——我们几年前就开始做这个了。有一个争论点就是,这种风格很像早期企业中使用的 COBOL 程序,他们利用数据文件来进行通信。另外也有人说微服务跟 Erlang 的编程模型很相近,只不过是套上了企业应用的外衣。

尽管有很多肯定的经验,我们也不希望做出微服务代表软件架构未来的结论。虽然对比 Monolith 有很多优点,然而这一方式资历尚浅,仍需时间检验。

架构方面的决策后果往往要多年以后才能被证明。我们见过具有强烈模块化愿望的优秀团队,开发出的 Monolithic 架构在多年以后轰然坍塌。很多人微服务有清晰的边界,不易被污染,因此这种后果应该不会出现在微服务应用上。然而我们还是需要看到更多的系统,运行更长的时间,才能做出微服务架构已经成熟的判断。

还有影响微服务成熟的因素是,组件化的成功与否,要看软件和组件的契合程度,组件的边界也是很难清楚界定的。虽然通过软件设计和重构能够降低对组件切分的难度,然而组件服务化之后,对进程间通信进行重构的难度是高于原有的进程内调用的。不同服务之间的代码移动非常困难,任何接口的变化都需要在参与方之间进行协调,需要进行更多的兼容考虑,测试也变得更为复杂。

另外一个问题是如果组件的边界设计有问题的话,会让一些本应属于组件内的方法变成服务间的调用,这样的情况增加的不只是复杂性,而且还模糊了功能边界,降低了控制能力。大家都认为小的简单的组件能降低难度,却容易忽略服务之间错综复杂的连接问题。

最后,还需要考虑团队的技能水准。高水准的团队才能更好地掌握新技术。对高水平团队来说很有效的技术可能在低水平团队来说完全无法应用。水平低下的团队是无法做好系统的,不论 Monolith 还是微服务架构,都无法改变这一事实。

还有一个经常被讨论的问题就是,不应该直接进入微服务架构,而是应该以 Monolish 架构开始,然后完成组件化过程,如果 Monolith 无法胜任,才需要迁移到微服务架构。(这种建议并不理想,因为进程内的接口并不一定适合转向服务接口。)

所以我们的建议是谨慎乐观。然而我们毕竟看到了,微服务模式值得一试。用于决策的信息永远都是不完整的,这正是是软件最大的挑战之一,微服务也是一样,现在下结论还为时尚早。

comments powered by Disqus