05 | 微服务反模式与陷阱:沙粒缺陷

译者简介:ASCE1885,《Android 高级进阶》《Android 高级进阶(源码剖析篇)》作者。
本文首发于Source Code Chain开发者社区,欢迎使用我的专属邀请链接加入一起交流。

Photo by ToT(https://www.ssyer.com/author/25644)

本系列是对《Microservices AntiPatterns and Pitfalls》一书的翻译,仅用于学习交流使用,谢谢!

架构师和开发人员在使用微服务架构创建应用时可能会遇到的最大挑战之一是服务粒度的问题。一个服务的粒度应该为多大合适?为多小合适?为服务选择一个合适的粒度是任何微服务系统能否成功的关键所在。服务粒度可以影响到性能,健壮性,可用性,变更控制,可测试性,甚至是部署。

当架构师或者开发人员创建的微服务粒度太细时,就会引入“沙粒缺陷”。你可能会有疑问,难道这不就是当初为什么称这种架构为微服务架构的原因吗?单词“微”表示服务应该很小的,但具体应该是多小呢?

之所以会引入这种缺陷的一个主要原因是开发人员经常会混淆服务与类。我看到过很多开发团队在创建微服务时想当然的认为他们编写的实现类就是服务本身,没有什么比事实更能说明这个问题的了。

一个服务应该总是被当作一个服务组件来对待。服务组件是在系统中完成某个特定功能的架构组件。服务组件应该在系统中具备清晰准确的角色和职责声明,同时拥有良好定义的操作集合。服务组件如何实现以及这个服务需要多少实现类,这都是开发人员所需要决定的

如图 5-1 所示,服务组件通过一个或多个模块(例如 Java 类)实现。使用模块和服务一对一的方式来实现服务组件,不仅会导致组件粒度太细,它同时也是一种很差的编程实践。一个服务如果只由一个单一的类来实现,往往会导致类太大,通过该类会承载太多职责,导致很难维护和测试。

Figure 5-1. Relationship between modules and a service

当然,服务中实现类的个数并不能用来作为判定服务粒度的决定性特征。某些服务可能只需要一个单一的类文件来实现所有业务功能,而其他服务可能需要六个或者更多的类来实现。

如果服务中实现类的个数对服务粒度没有影响,那是什么呢?幸运的是,我们可以使用以下三个基本的测试来确定服务的粒度级别:

  • 服务范围和功能
  • 是否需要数据库事务
  • 服务编排级别

服务范围和功能的分析

确定服务是否拥有正确的粒度的第一种方法是分析服务的范围和功能。服务是做什么的?它有哪些操作?把服务范围和功能写下来是确定服务是否职责太多的好方法。如果发现类似“和”以及“此外”之类的单词的话,那往往是服务功能太多的一个特征。

内聚性在服务范围和功能方面也起着一定的作用。内聚性被定义为服务操作之间相关性的程度和方式。一般来说,我们总是希望服务内聚性强,例如,假设我们有一个客户服务,它包含如下操作:

  • add_customer
  • update_customer
  • get_customer
  • notify_customer
  • record_customer_comments
  • get_customer_comments

在这个例子中,前三个操作是相互关联的,因为它们都涉及维护和检索用户信息的操作。然而,后面三个操作(notify_customer,record_customer_comments 和 get_customer_comments)跟用户基础数据的 CRUD 操作无关。在分析该服务中操作的内聚性级别时,很明显,这个服务应该被拆分为三个独立的服务(客户维护服务,客户通知服务,客户评论服务)。

图 5-2 说明了这一点,通常来说,当分析一个服务的范围和功能时,我们很可能发现这个服务粒度太粗,从而将它拆分为更细粒度的服务。

Figure 5-2. Impact of analyzing service functionality and scope

Sam Newman 在这方面提供了一些很好的建议,一个服务开始时粒度较粗没有关系,随着我们对这个服务的了解越深,再将它进一步拆分为细粒度的多个服务即可。遵循这个建议将帮助我们开始定义服务组件,而不必马上担心粒度的问题。

分析服务的范围和功能是一个好的开始,但我们不希望只停留在这个阶段,在分析完服务范围后,我们需要接着分析数据库事务的需求。

数据库事务的分析

验证服务粒度的另一个测试是确定是否需要数据库事务。数据库事务更正式的称呼是 ACID (原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability))事务。ACID 事务将多条数据库操作合并为一次提交。这些数据库操作要么被作为一个整体提交,要么在出现错误时统一被回滚。

因为微服务体系架构中服务是作为独立的应用程序来分布和部署的,因此,要在两个或者多个远程服务之间进行 ACID 事务操作是非常困难的。出于这个原因,微服务架构通常依赖一种被称为 BASE(基本可用(Basic availability),软状态,状态可以有一段时间不同步(soft state) 和最终一致,最终数据是一致的就可以了,而不是时时保持强一致(eventual consistency))事务的技术。无论如何,通常你会遇到需要保持 ACID 事务的一些业务操作。如果你发现经常会陷入关于某个具体场景
到底是使用 ACID 还是 BASE 的争论时,同时你需要协调多个更新操作,很可能这时的问题是你的服务粒度太细了。

当分析服务是否需要数据库事务时,如果发现服务不能接受最终一致性,这时通常会将原来细粒度的多个服务合并成一个粗粒度的服务,从而保持在单个服务上下文中来协调多个数据库操作,如图 5-3 所示:

Figure 5-3. Impact of analyzing database transactions

需要注意的是,从 ACID 事务的观点来看,我们将服务合并后,是否同时将数据库合并为一个,或者继续保持多个独立的数据库,这一点并不重要。一般来说,我们还是会把数据库也进行合并,但这并不是遵循 ACID 事务所必需的(前提是假设我们使用的数据库和事务管理器支持 XA 事务,例如两阶段提交)。

一旦我们分析完服务的数据库事务需求,那么就可以接着进行第三个测试,即服务编排。

服务编排的分析

可以用来验证服务粒度级别的第三个测试是服务编排。服务编排是指服务之间的通信,通常也被称为服务间通信。在微服务架构中服务编排通常是需要特别关注的。首先,它降低了应用程序的整体性能,因为每次对另一个服务的调用都是远程调用。例如,假设对另一个服务进行 REST 调用需要耗时 100 毫秒,如果调用五个远程服务,那么光是远程服务调用的耗时累加起来就需要半秒钟了。

太多的服务编排引入的另一个问题是它会影响系统的整体可靠性和健壮性。对于单个业务请求来说,远程服务调用越多,其中某个服务调用失败或者超时的可能性就越高。

如果你发现完成单个业务请求需要太多的远程服务通信,那么可能是服务拆分的粒度太细导致的。在分析服务编排的级别时,一般会从细粒度服务逐渐进化到粒度更粗的服务,如图 5-4 所示:

Figure 5-4. Impact of analyzing service choreography

通过合并服务为粒度更粗的服务,我们可以提高应用程序的性能和整体的可靠性和健壮性。同时还可以移除服务之间的依赖关系,允许更好的变更控制,测试和部署。

处理服务编排从而克服系统性能和可靠性问题的另一个方法是利用异步并行处理结合响应式架构技术来进行错误处理。同时执行多个请求可以提高系统整体的响应能力,使得可以快速协调单个业务请求中涉及的多个远程服务调用。这里的关键点是理解和分析与服务编排相关的权衡(trade-off),以确保对用户而言系统有足够的响应性和整体可靠性。

猜你喜欢

转载自blog.csdn.net/ACE1985/article/details/80953534
05