很多人说开发微服务并不难,难点在于如何划分微服务。这个话虽然有点绝对,但反应了服务边界的划分有多么重要。一个设计良好的单体应用远比一个混乱的微服务要好。采用微服务架构的目的是为了让系统变得更具有扩展性、可用性。但在把单体应用变成靠谱的微服务架构之前,单体系统的各个模块的划分应该是合理、清晰地。可以认为微服务就是将单体应用的各个模块分开部署而已。
本文将从多个角度切入,探讨一下微服务的划分方法。
在面向对象的程序设计中,有一条原则想必大家都知道,就是:高内聚、低耦合。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。不管是对单体应用进行模块化,还是对微服务进行划分,都需要以这个基本原则为指导。
在这个抽象原则的指导下,结合软件设计的具体实践,可以找到不同的服务划分方法。
数据库驱动的设计
数据驱动编程的核心出发点是相对于程序逻辑,人类更擅长于处理数据。数据比程序逻辑更容易驾驭,所以我们应该尽可能的将设计的复杂度从程序代码转移至数据。
用前贝尔实验室成员、Unix小组成员Rob Pike的话说:数据压倒一切。如果选择了正确的数据结构并把一切组织的井井有条,正确的算法就不言自明。编程的核心是数据结构,而不是算法。
我经历的大多数项目,都是以数据库为中心。 数据集中存储在不同的table中,且为了满足关系模型理论,这些table的设计还需要符合各种范式和标准。 软件系统的业务逻辑通过一系列针对这些table的增删改查来实现。
领域驱动设计
领域驱动设计(DDD)立足于面向对象思想,从业务出发,通过领域模型的方式反映系统的抽象,从而得到合理的服务划分。
通过利用 DDD 对系统从业务的角度分析,对系统进行抽象后,得到内聚更高的业务模型集合,在 DDD 中一组概念接近、高度内聚并能找到清晰的边界的业务模型被称作限界上下文(Bounded Context)。
限界上下文可以视为逻辑上的微服务,或者单体应用中的一个模块。在电商领域就是订单、商品以及支付等几个在电商领域最为常见的概念;在社交领域就是用户、群组、消息等。
数据驱动是一个自下而上的架构设计方法,数据驱动强调的是数据结构,也就是通过分析需求,确定整体数据结构,根据表之间的关系划分服务。
通常基于数据驱动划分服务的步骤如下:
需求分析。通过领域专家(或者产品经理)确定目标,然后总结User Story,确定核心的业务流程;通过工具呈现比较粗糙的界面,进行内部讨论;不断迭代此环节,直到满意为止。
抽象数据结构。根据需求总结Use Case,协助分析需求,从中抽象数据结构。
划分服务。分析数据结构,识别服务——服务应该满足高内聚、低耦合、单一职责SRP等特征。
确定服务调用关系。先分析出主要流程,根据请求需要调用的服务确定服务调用关系。如果存在问题,则需要回到(1)重新开始。
业务流程验证。重新回到User Story,以服务为粒度实现时序图,注意此阶段重点是验证服务划分是否合适,要关注如下问题。
持续优化。
领域驱动是一个自上而下的架构设计方法,通过和领域专家建立统一的语言,不断交流,确定关键业务场景,逐步确定限界上下文。领域驱动更强调业务实现效果,认为自下而上的设计可能会导致技术人员不能更好地理解业务方向,进而偏离业务目标。
通常基于领域驱动划分服务的步骤如下:
通过模型和领域专家建立统一语言。建立统一语言是为了更深入地理解需求。通用语言尽量以业务语言为主,而非技术语言;通用语言和代码一样,需要不断地重构。
业务分析。确定核心的业务流程,然后逐步扩展到全部。最好通过工具呈现比较粗糙的界面,供内部讨论。
寻找聚合。显式地定义领域模型的边界。最近比较热门的事件风暴,是一种基于领域驱动分析业务、划分服务的方法。事件风暴就是把所有的关键参与者都召集到一个很宽敞的屋子里来开会,并且使用便利贴来描述系统中发生的事情,如下图所示。
确定服务调用关系。先分析出主要流程,根据一次请求需要调用的服务来确定服务调用关系。如果存在水平划分,则需要根据服务依赖原则确定关系。如果存在问题,则需要回到(1)重新开始。
业务流程验证。以服务为粒度实现时序图,注意此阶段重点是要验证服务划分是否合适,主要关注如下问题。
持续优化。
拆分后的维护成本要低于拆分前
这里的维护成本包括:人力、物力、时间。
这里的成本对大部分中小团队来说都是必须要考虑的重要环节,如果投入和收益不能成正比,或者超出领导的预算或者市场窗口,那么先进的技术就是绊脚石,千万不要迷恋技术,所谓工程师思维千万要不得。
拆分不仅仅是架构的调整,组织结构上也要做响应的适应性优化
确保拆分后的服务由相对独立的团队负责维护。
这句话怎么理解呢?传统的团队划分是按照产品部、前端、后端横向划分,微服务化以后的团队可能就会是吃一张披萨饼的人数,产品、前端、后端被归类到服务里面,以服务为中心来分配人数。
按照团队成员规模划分
人多就多分几个,人少就少分几个,确保每个微服务的开发团队规模不会过大或过小。
“三个火枪手”原则
即原则上3个人负责一个微服务。三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。当然考虑到人员备份问题,每个微服务最好都安排 2 个人维护,每个人都可以维护多个微服务。
“两个披萨”原则
两个披萨原则最早是由亚马逊CEO贝索斯提出的,他认为如果两个披萨喂不饱的一个团队,那这个团队可能太大了。因为参与人数过多的团队会议不利于形成决策,而小团队在一起做项目,开会讨论更容易达成共识,并且有利于促进公司内部的创新。
按用户角色划分
按照使用系统的用户角色划分微服务。以电商系统为例,按照买家,卖家,售后服务员,快递员等角色来划分,这是一种非常不可取的方法。不同的角色会使用到相同的业务功能,有经验的开发人员一般不会陷入这中显而易见的误区。
还有一些公司按照渠道来划分了团队,甚至按照 To C (面向于用户)和 To B(面向企业内部)划分的团队,最终设计出来的限界上下文中赫然出现 “C 端文章服务”,“B 端文章服务”。这也是一种令人感到惊讶的架构。
划分粒度越小越好
服务的大小并不是特别重要,可以根据团队规模、代码规模、业务复杂度、技术领域、重要程度、成本等因素综合考虑。关于服务粒度的大小,业界并没有统一标准,也很难衡量,最接近的衡量标准是研发团队规模。粒度小意味着更高的维护成本。后端管理、辅助系统通常粒度较大。
一次性划分服务
从单体应用转变为微服务架构时,将业务代码和数据库同步拆分。根据服务将数据库彻底拆分为多个独立的数据库,每个服务独享数据库服务,服务之间只能通过接口调用。这看起来非常美好,但需要为此做大量的数据迁移。当业务处于初期,需求不是非常确定,开发人员对业务理解不是特别透彻的时候,可能拆分后发现拆分得并不合理,只能再进行合并,又要进行一次数据迁移。相对来说,业务代码拆分成本更低,而数据迁移的成本更高,频繁、大量的数据迁移并不可取。
服务划分一旦完成,不能改变
由于业务的不断变化,以及开发人员对领域知识和其他影响因素的理解等问题,很难一次性做出一个完美的解决方案。通常在划分后会发现,某个问题是不可忍受的,例如划分后导致响应时间降低,增加了更多的成本,有可能需要重新合并服务;由于业务的变化,原本的依赖关系发生了变化,有可能面临需要重新划分服务等类似的问题。
在我看来,基于领域模型的微服务划分是更加理想的方法,但是实际操作起来门槛也更高。它要求团队成员深入理解DDD的概念,并且具有较强的领域建模能力,而据我所知一个经验丰富的建模专家是可遇而不可求的。相对来说,基于数据的划分,虽然有偏离业务本质的风险,但是在团队成员能力有限的情况下,是具有现实意义的。