1.1.1 微服务架构的关键特性
Martin Fowler在他的文章中列举了一系列微服务架构的特性,笔者认为下面的几点最具代表性。
1.服务组件化
一直以来,解耦、松散耦合就是软件构建过程中追求的目标。即便是单体应用这种逻辑上是一个整体的结构,其通常也会根据功能不同被拆分为不同的模块(前提是有一个优秀的架构师来设计它)。这种相对独立的功能模块可以被理解为组件。与单体应用不同,微服务应用中的服务就成了应用的组件,并且是进程外组件,它们彼此之间通过某种网络协议进行交互。
将服务作为组件的一个优点是让部署变得相对独立。像单体应用这样单一进程的应用,即便其内部只有某一小部分发生了变化,也需要对其整体打包重新部署,而微服务应用无须面对这样的问题,其间接效应就是,可以在一定程度上提升资源的利用率并节约成本。当然,并不是应用中所有的服务都一定是完全独立的,越复杂的业务逻辑越有可能催生更多的依赖关系。
独立的服务让服务间的调用关系更加明确,因为服务之间必须通过共同约定的契约关系,也就是API进行交互。而进程内模块的调用方式没有这一限制,如果方法或函数的可见性设计得不合适,很可能会被习惯不好或者不了解设计意图的程序员乱用,最终与原本拆分服务时想要追求松散耦合的目标背道而驰。
另外,从进程内调用变为进程外调用,组件之间具有的强黏性也因为网络的介入被破坏了,还引入了一系列的非功能性需求,比如原本的本地事务成了分布式事务,或者为了保证系统的可靠性不得不加入通信超时、重试等机制。
2.基于业务能力构建系统
软件开发大师Chris Richardson在他的《微服务架构设计模式》一书中写道:“微服务的本质是服务的拆分和定义,而不是技术和工具。”服务拆分的合理性决定了服务之间交互的合理性,进而影响了应用构建的效率。笔者认为,基于业务构建应用是最难的,也是最重要的一步。所要开发的应用功能来源于客户需求,即所谓的业务,业务之所以被拆分成不同的部分就是因为它们彼此相对内聚。因此,围绕业务构建应用就成了很自然的一个选择,解耦也会相对容易实现。这也就是为什么领域驱动设计在微服务架构成为主流后又重新开始流行。
但因为康威定律(Conway's Law)的存在,对应用的构建在很大程度上受限于组织结构。我们可以简单地将康威定律理解为“组织决定架构”。如图1-1所示,开发团队会构建出和组织结构一致的系统形态。而一个按不同职能(比如前后端、DBA、运维)划分的组织,大概率会开发出水平细分(分层)的系统。想要构建出基于业务垂直细分的服务,会遇到一些难以跨越的障碍(比如沟通、集成),而重组组织结构通常不太可能,这也就是为什么说服务(业务)拆分是微服务开发中的最大难点之一。
图1-1
我们团队在面对这个问题时使用了一个相对取巧的方式。在服务改造初期,我们对职能划分做了重新安排,原本前端、后端、QA这样的常规职位被取消,工程师需以全栈的角色进行开发。侧翼的支持团队,比如DBA、运维人员,也按业务线不同被划入虚拟团队,以便补全开发团队。
围绕业务去构建系统是一条非常有指导意义的规则。当你在服务的拆分过程中遇到困惑时,仔细思考一下这个理念,也许就有了答案。
3.去中心化治理和数据分治
分散的服务带来的另一个特征就是去中心化,下面我们具体介绍与去中心化治理和数据分治相关的内容。
(1)去中心化治理
一个应用所包含的各个业务需求必然是各不相同的,而不同的业务模型必然有最适合它的技术方案。通过拆分,我们实现不同的服务时在技术方案上可以有不同的选择,这就是所谓的异构。使用合适的方案比使用统一的方案更重要,因为这样效率更高。例如,使用面向对象语言构建有复杂业务模型且适合建模的服务,使用Golang语言构建中间件服务。在数据层面也是如此,对于有级联关系的业务模型,使用MongoDB这种文档化的存储方案更合适,对于大数据离线分析业务,使用列式数据仓库方案更合适。对于团队来讲,这种去中心化的构建方式也更加灵活。
相反,集中化的治理方式会产生统一的技术标准,但这一标准或技术栈并不适合所有的业务团队,他们可能需要做适当的兼容和调整来解决技术方案无法满足需求的问题,而这些都会带来额外的工作量。当然,统一技术栈在一定程度上会降低对技术的依赖和维护成本,但在云原生的大背景下,我们观察到这种简化技术栈的方式正在逐渐变少,因为应用的复杂性和非功能性需求越来越多,技术生态也越来越完善,开发团队引入新的技术或工具并不会产生太多额外的负担。因此,对于微服务而言,这种分而治之的去中心化治理依然是其重要特性之一。
(2)数据分治
数据是业务模型在存储层面的体现,换句话说,数据本质上就是业务。因此,数据分治也同样体现了围绕业务构建服务的思路。通常的表现是,不同的服务使用不同的数据库实例、持有不同的数据库表,或者干脆使用不同的存储方案,即采用混合持久化方案。图1-2显示了微服务架构和单体应用在数据库持有层面的不同形态。
图1-2
传统的单体应用通常(但并不绝对)会使用逻辑上单一的数据库来持久化数据。如果你是一位具有多年开发经验的“老”程序员,这种长期的习惯会让你在迁移到微服务架构的过程中感到不适。比如你得很小心地拆分业务对应的数据库表,以保证它们被正确持有。
数据分治带来的好处依然是灵活性和内聚性,业务在数据层面的变化可以由服务自己处理。但缺点也非常明显,就是引入了分布式事务的问题。业务之间必然会有联系,一个完整的调用链上很难不出现事务操作。而因为CAP理论的存在,分布式事务又没有一个完美的解决方案,开发团队不得不根据应用场景做出权衡。因此,微服务架构在设计上趋向于使服务间进行无事务协作,或者用最终一致性和补偿机制来弥补缺陷。本书第5章会详细介绍我们团队在分布式事务上的最佳实践,可供读者参考。
4.基础设施自动化
从本质上来说,自动化的持续集成(CI)和持续交付(CD)的核心是管道(Pipeline)。通过管道,我们可将代码一步步从开发环境传送到生产环境。随着云平台具有的能力越来越强,构建、部署、运维微服务的操作复杂度在逐渐降低。笔者认为,自动化持续集成和持续交付不一定是构建微服务的必要条件,但不可或缺。想象一下,如果一个应用拥有几百个甚至更多的服务,此时若没有自动化的持续集成和持续交付管道,那么有效部署这些服务将是一个极大的麻烦。另一方面,应用被拆分为服务的一大目的就是希望可以独立部署,提高系统的运转效率和资源利用率。如果因为没有自动化部署而导致运维效率低下,这就违背了设计的初衷。因此,为微服务架构构建一个自动化的持续集成和持续交付管道,特别是在云原生时代,就成了非常重要的一点。
5.面向失败设计和演进式设计
使用微服务架构时,在设计层面上需要注意以下两点。
(1)面向失败设计
正如前文所说的,将业务模块拆分成服务后,因为交互方式的改变,服务间的调用很有可能会因为各种原因而失败,比如网络抖动、上游服务不可用、流量过载、路由出错等。这就要求我们在构建微服务应用的过程中充分考虑这些问题,想办法在失败发生时尽可能降低对用户的影响。很显然,这将增加开发负担,我们需要在应用中添加更多的非功能性需求,或者说控制逻辑,而这应该是微服务这种分布式架构之于单体应用最大的劣势。
为了解决微服务的这一问题,服务治理,以及通过服务网格技术更轻松地管理服务间的通信成了重要的课题。当面对失败时,最重要的就是能够及时检测和发现故障并自动恢复。在云原生技术的加持下,实现面向失败设计变得不再困难,比如我们可以基于基础设施的探针和运行策略完成自动化重启,也可以通过日志、指标和追踪构建完善的服务监控能力。
(2)演进式设计
我们通常戏谑地称:软件开发过程中唯一不变的事情就是改变。这句话有点自嘲但的确是不争的事实。频繁的需求变更,以及为了追随市场而做出策略调整,都要求我们能设计出具有演进能力的应用。当我们将应用拆分成服务后,它就具有了独立变化的能力,我们可以针对变化做出单独的调整而不需要重新构建整个应用。比如通过不断地、分批次地更新不同的服务,逐渐让应用的各个部分持续发展,这就是所谓的演进。我们甚至可以通过抛弃式的方式来开发应用。比如针对某次市场运营活动开发一项功能,它完全可以以一个单独服务的形式存在,等活动结束后删除它,不会对主要的业务部分产生影响。为了让演进流程更顺畅和合理,还需要注意将稳定的部分和易变的部分分开,以保证将变更带来的影响降至最低。
可以说,微服务在一定程度上为设计演进式应用提供了前提,服务化带来的模块可替代特性让演进式设计更容易实现。