关于微服务

微服务的特征和需求

2014 年 Martin Fowler 发表了一篇关于微服务的文章《Microservices》

福勒提出,微服务的风格并不是创新的,对于其根源可以追溯至 Unix 的设计原则。并且微服务没有正式的定义,但是我们可以从众多符合微服务思想的架构中提取其特点。

切分服务来实现组件化

人们在理解或构建复杂系统时,习惯于将其拆分成对应的 组件/模块/部分。福勒给 组件 的定义是:

A unit of software that is independently replaceable and upgradeable.
一个 独立、可替换、可升级 的 软件单元

正如我们在开发中,会依赖大量的库,无论是自己实现的或是引用第三方的,都可以视为一种组件化。只不过这种组件是内部的,而抽象的看,微服务仅仅是改变了调用的组件的位置。

微服务一个明确的优点是:和传统的单体应用相比,微服务可以独立部署。也就意味着对一个正在运行的应用的部分进行更新,可以不需要重新打包部署整个应用,只要对其中对应的微服务进行单独升级部署。

另一个优点在于,微服务定义了明确的组件接口,这其实应该是得益于使用网络来实现的松耦合,每个微服务不受限于开发语言和技术。接口也可以限制模块之间的耦合程度变紧密的风险。

同样的,微服务的模式也有缺点。正如其思想早在 Unix 时代就存在,却直到 2012 年后才开始广泛使用,一个较为关键的因素就是网络。近年来网络通信成本的降低和其效率的提升,为微服务落地提供了环境。但是不得不承认,微服务的开销还是会大于单点应用,并且远程调用方法的粒度也会比使用库更大。

围绕业务进行组织

微服务很困扰我的一点是,我在习惯了传统应用开发之后接触微服务,对于如何切分微服务非常迷茫。在讨论这种迷茫之前,我或许应该弄清楚自己的项目组织结构到底适不适合使用微服务。

福勒提到了康威定律:

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.

任何组织设计一个广义的系统时,会创造一个结构类似于组织通信结构的设计的副本。

—— Melvin Conway, 1968

简单的来讲,项目组的组织结构会影响项目的结构。

举个例子,假设我们的项目组包括前端,后端和数据库三个大组,那么自然而然的会去选择使用 MVC 的结构,后端组内易于沟通,更倾向于做一个大型单体后端应用,然后以大组为单位向前端组和数据库组对接。

如果我们将大组打散,构建多个小组,每个小组分配几个前端、后端、数据库人员,这样来匹配微服务架构就会非常自然,每个小组只需要维护其自己服务对外的接口。

在传统的软件项目流程中,开发团队完成项目,交付后转接给运维团队进行运营。福勒鼓励开发团队长期维护并支持用户。当然,这一点单体应用同样可以实现,但是由于微服务粒度更低,更容易达成这样的目标。

智能(强)端点和呆(弱)管道

原为 Smart Endpoint and Dumb Pipes,一开始翻译成哑管道,再三斟酌,我觉得应该称为呆(弱)管道更为贴切。

原文里有一句话比较关键:

We've seen many products and approaches that stress putting significant smarts into the communication mechanism itself.
我们已经看到,很多产品和方法强调将智能处理部分融入通信机制本身。

端点比较好理解,你可以看作微服务主要的处理逻辑。

管道的思想源于 Unix (Linux)。你可能用到过如下命令:

ps aux | grep process_name

ps 的输出会通过呆(弱)管道(’|’)传递到 grep 中进一步筛选输出。你会发现它除了传递之外什么事都没做,这就是我愿意称之为 “呆” 的原因。

再回过头来看,就能更好理解福勒想表达的意味:

尽量让逻辑处理在端点中进行,让通信过程变得简单纯粹。

将端点和管道替换成单个微服务和消息传递方式,我们就能理解他选择并推荐的两种通信方式 —— HTTP 请求-响应 和 轻量级消息传递。以至于在后面,选用轻量级消息总线时,会推荐选择 RabbitMQ 和 ZeroMQ —— 因为它们是 “呆” 的,除了提供一个可靠的异步结构之外没有做更多的事情。

这里用他引用的一句话作为总结:

Be of the web, not behind the web.
做网络该做的事。

—— Ian Robinson

去中心化

微服务的去中心化体现在两个方面:技术管理和数据管理。

技术管理的去中心化体现在微服务带来技术选择的高自由度,每一个微服务可以根据每一个具体服务的需求选择对应的技术、结构甚至是语言。”手里拿着锤子,看什么都像钉子”,当单体项目敲定了架构,对什么需求的处理可能都会陷入用现有工具如何实现的问题里。

数据管理的去中心化又可以分成概念模型和数据存储决策。

数据管理和 DDD 的关联更密切——首先,微服务的拆分有助于使领域驱动设计中的上下文边界更加清晰。

其次,微服务不但可以分离领域概念模型,也分离了数据存储。微服务更倾向于每个服务自己管理数据库,包括拥有相同数据库技术的独立存储实例 或 直接使用完全不同的数据库系统(混合持久化 – Polyglot Persistence)。

当然,分离数据存储必然会带来问题。单体应用中我们倾向于用事务来保证一致性,但是事务会带来明显的时序耦合(Temporal Coupling)。

Temporal Coupling, which occurs when there's an implicit relationship between two, or more, members of a class requiring clients to invoke one member before the other.
当两个或多个成员之间存在隐式关系,要求客户端先调用另一个成员时,就会发生时序耦合。

微服务架构强调服务之间的无事务协调 —— 明确一致性可能只能是最终一致性。

实现最终一致性的方式大致有三种:可靠事件模式、业务补偿模式、TCC 模式。文中福勒也提到使用补偿操作来实现最终一致性,这里简述一下补偿操作:

所谓补偿类似一个反向操作。假设一个商品的订购流程可以切分成多个事务,第一个订单事务会创建一个订单,并继续推进发送消息给库存事务,库存事务将商品的库存扣除,并发送确认消息给订单事务,然后继续往下推进。如果下一个事务失败,则推送消息触发库存服务的补偿事务,补充库存,之后再返回推进给订单事务的补偿事务,取消订单。

即,沿着经过的路径原路返回,并进行补偿操作。因为补偿过程也可能出现错误,补偿过程也需要实现最终一致性(正向和补偿都需要拥有幂等性)。补偿的思路和难点都在于构建一整个服务链。

管理和监控

同样重要的是,将单体应用拆分成微服务,在开发和部署中如何更好的管理到位,其中可能包括:

  • 单元/功能/验收测试
  • 集成测试(SIT)
  • 用户验收环境测试(UAT)
  • 高仿真环境测试(SIM)
  • 正式环境(PROD)

目前已经有比较成熟的方案来支持项目实现持续交付,比如 Jenkins。

在微服务运行中统一进行监控、问题定位是必要的 —— 服务之间的调用会随着业务功能的增加越来越复杂,定位问题的难度会极大提升。目前也已经有成熟的解决方案,例如使用 Sleuth,Zipkin 进行链路追踪。

微服务的不足

至此,你可能会发现,如何切分微服务这个令人困扰的点正是整个微服务架构最难实现的部分。

In any effort at componentization, success depends on how well the software fits into components. It's hard to figure out exactly where the component boundaries should lie.
在组件化的任何努力中,成功取决于软件与组件的匹配程度。很难弄清楚组件边界应该在哪里。

换而言之,微服务架构的成功与否就有一部分依赖于服务的拆分,这也是众多微服务架构提到 DDD 的原因。

由于组件转变为拥有远程调用通信的服务,重构就比内部库来的困难得多。跨服务边界移动代码会出现很多阻力,任何接口更改都需要在参与者之间协调,需要添加向后兼容层,并且测试会变得更加复杂。

另一个问题是如果组件的组合不合理,那么你所做的就是将复杂性从组件(服务)内部转移到组件(服务)之间的连接上。这转移了复杂性,并且将其转移到一个模糊且难以控制的地方。当你查看一个小的、简单的组件(服务)内部时,很容易被迷惑,认为服务拆分合理,而忽略服务之间的混乱连接。通俗的讲 —— 服务的粒度难以掌握,这本质还是拆分的问题。

福勒认为合理的论点是,不要一开始就选择微服务架构,而是从一个单体应用开始。保持模块化,一旦单体成为问题,就将其拆分为微服务。不合理处在于,单体服务的模块划分并不适用于直接拆分为微服务,重构需要重新进行拆分设计,不过由于有了单体服务的积累,开发团队或许会对业务有更深度的理解。