边界:微服务的粒度

勿行极端,过犹不及

子贡问:“师与商也孰贤?” 子曰:“师也过,商也不及。” 曰:“然则师愈与?” 子曰:“过犹不及。”

子贡问:“颛孙师和卜商谁更贤德?” 孔子说:“颛孙师常常作得有些过头,卜商常常达不到要求。” 子贡说:“如此说来,是不是颛孙师要好一些呢?” 孔子说:“过头和达不到同样不好。”

—— 论语·先进

当今软件业界,对本节的话题“识别微服务的边界”其实已取得了较为一致的观点,也找到了指导具体实践的方法论,即领域驱动设计(Domain-Driven Design,DDD)。囿于主题,在这部文档中甚少涉及该如何抽象业务、分析流程、识别边界、建立模型、映射到服务和代码等偏重理论的务虚话题,即使在这一章中,笔者也尽量规避了 DDD 中需要专门学习才能理解的概念,如界限上下文(Bounded Context)、语境映射(Context Map)、通用语言(Ubiquitous Language)、领域和子域(Domain、Sub Domain)、聚合(Aggregate)、领域事件(Domain Event)等等。并非笔者认为业务流程与设计方法论不重要,而是如果要严谨、深刻地讨论这些话题,其篇幅足以独立地写出一本书。事实上,市场上已经有不少这样的书了,DDD 的发明人 Eric Evans 撰写的同名书籍《领域驱动设计:软件核心复杂性应对之道》便是其中翘楚。笔者个人是更推荐 Chris Richardson 撰写的颇具口碑的入门书《微服务架构设计模式》,其叙述的主线就是在 DDD 指导下,如何将一个单体服务逐步拆分为微服务结构,如果你对这方面感兴趣,不妨一读。这两节中,笔者会从业务之外的其他角度,从非功能性、研发效率等方面来探讨微服务的粒度与拆分。

系统设计是一种创作,而不是应试,不可能每一位架构师设计的服务粒度全都相同,微服务的大小、边界不应该只有唯一正确的答案或绝对的标准,但是应该有个合理的范围,笔者称其为微服务粒度的上下界。我们可以分析如果微服务的粒度太小或者太大会出现哪些问题,从而得出服务上下界应该定在哪里。

可能是受微服务名字中“微”的“蛊惑”,笔者听过不少人提倡过微服务越小越好,最好做到一个 REST Endpoint 就对应于一个微服务,这种极端的理解肯定是错误的,如果将微服务粒度定的过细,会受到以下几个方面的反噬:

  • 从性能角度看,一次进程内的方法调用(仅计算调用,与方法具体内容无关),耗时在零(按方法完全内联的场景来计算)到数百个时钟周期(按最慢的虚方法调用无内联缓存要查虚表的场景来计算)之间;一次跨服务的方法调用里,网络传输、参数序列化和结果反序列化都是不可避免的,耗时要达到毫秒级别,你可以算一下这两者有多少个数量级的差距。 远程服务调用 里已经解释了“透明的分布式通信”是不存在的,因此,服务粒度大小必须考虑到消耗在网络上的时间与方法本身执行时间的比例,避免设计得的过于琐碎,客户端不得不多次调用服务才能完成一项业务操作,譬如,将字符串处理这样的功能设计为一个微服务便是不合适的,这点要求微服务从功能设计上看应该是完备的。
  • 从数据一致性角度看,每个微服务都有自己独立的数据源,如果多个微服务要协同工作,我们可以采用 很多办法 来保证它们处理数据的最终一致性,但如果某些数据必须要求保证强一致性的话,那它们本身就应当聚合在同一个微服务中,而不是强行启用 XA 事务 来实现,因为在参与协作的微服务越多,XA 事务的可用性就越差,这点要求微服务从数据一致性上看应该是内聚(Cohesion)的。
  • 从服务可用性角度看,服务之间是松散耦合的依赖关系,微服务架构中无法也不应该假设被调用的服务具有绝对的可用性,服务可能因为网络分区、软件重启升级、硬件故障等任何原因发生中断。如果两个微服务都必须依赖对方可用才能正常工作,那就应当将其合并到同一个微服务中(注意这里说的是“彼此依赖对方才能工作”,单向的依赖是必定存在的),这条要求微服务从依赖关系上看应该是独立的。

综合以上,我们可以得出第一个结论:微服务粒度的下界是它至少应满足独立——能够独立发布、独立部署、独立运行与独立测试,内聚——强相关的功能与数据在同一个服务中处理,完备——一个服务包含至少一项业务实体与对应的完整操作。

我们再来想想,如果微服务的粒度太大,会出现什么问题?从技术角度讲,并不会有什么问题,每个能正常工作的单体系统都能满足独立、内聚、完备的要求,世界上又有那么多运行良好的单体系统。微服务的上界并非受限于技术,而是受限于人,更准确地说,受限于人与人之间的社交协作。《人月神话》中最反直觉的一个结论是:“为进度给项目增加人力,如同用水去为油锅灭火”(Adding Manpower to A Late Software Project Makes It Later)。为什么?Fred Brooks 给出了简洁而有力的答案:

软件项目中的沟通成本= n×(n-1)/2,n 为参与项目的人数

为了让你能更直观地理解这个答案,笔者已经算好了一组数字:15 人参与的项目,沟通成本大约是 5 个人时的十倍,150 人参与的项目,沟通成本大约是 5 个人时的一千倍。你不妨回想一下自己在公司的工作体验,不可能有 150 人的团队而不划分出独立小组来管理的,除非这些人都从事流水线式的工作,协作时完全不需要沟通。此外,你也不妨回想一下自己的生活体验,我敢断言你的社交上界是不超过 5 个知己好友,15 个可信任的伙伴,35 个普通朋友,150 个说得上话的人。这句话的信心底气源于此观点是人类学家Robin Dunbar在 1992 年给出的科学结论,今天已被普遍认可,被称为“邓巴数”(Dunbar's Number),据说是人脑的新皮质大小限制了人能承受的社交数量,决定了邓巴数这个社交的上界。

有了以上铺垫,你应该能更能理解前面的许多文章中笔者为何采用“2 Pizza Team”作为微服务团队规模的“量词”了,并不是因为制造这个梗的人是Jeff Bezos,是亚马逊 CEO、世界首富。而是因为两个 Pizza 能喂饱的人数大概就是 6-12 人,符合软件开发中团队管理的理想规模。

康威定律约束了软件的架构与组织的架构要保持一致,所以微服务的上界应该与 2 Pizza Team 能够开发的最大程序规模保持一致。2 Pizza Team 能开发多大规模的程序?人员数量固定的前提下,这个答案不仅与开发者的能力水平相关,更是与研发模式和周期相关。如果你的软件产品是瀑布开发,可能需要一个月、两个月迭代一次;如果采用 Scrum,可能会一周、两周完成一次冲刺;如果追求日构建、精益,甚至可能一天、两天就会集成构建出一个小版本,以上不同的研发方法,都会产生相应规模的上界。

综合以上,我们得出了第二个结论:微服务粒度的上界是一个 2 Pizza Team 能够在一个研发周期内完成的全部需求范围。

在上下界范围内,架构师会根据业务和团队的实际情况来灵活划定微服务的具体粒度。譬如下界的完备性要求微服务至少包含一项完整的服务,不超过上界的前提下,这个微服务包含了两项、三项业务操作是否合理,那需要根据这些操作本身是否有合理的逻辑关系来具体讨论。又譬如上界要求单个研发周期内能处理掉一个微服务的全部需求,不超过下界的前提下,一个周期就能完成分属于两个、三个微服务的全部需求时,是缩短研发周期更合理,还是允许这个周期内同时开发几个微服务,也可以根据实际情况具体讨论。