在微服务的诸多优势中,最重要的动机是业务单位的规模和自主权。然而,我们仍然需要创建一个对最终用户有意义的集成体验。在为微服务之间的交互开发策略时,记住这两个目标是很重要的。这些策略可以成就或毁掉你的努力。
我们如何映射每个微服务决定了它的自主性。由有限上下文[1]或业务功能建模的微服务比基于技术能力建模的微服务更具有自主性。让我们考虑一个银行应用程序的示例。它可以具有的一些边界上下文通常是登录和安全、配置文件管理、事务服务(一种借贷服务,因为它们紧密相连)、支出报告和外部服务,比如信用报告检查或奖励检查。这些上下文可能有许多类似的技术实现:例如,日志记录。但是,如果我们将日志记录创建为自己的服务,那么几乎所有其他服务都将依赖于它。它可以成为一个关键:你把它拿下来,生意就停止了。相反,我们可以将日志实现推入一个库,根据上下文创建服务,并尽可能使用日志库。
用自己的数据库映射垂直业务片中的服务只是开始。我们仍然需要以一种创建内聚体验并在这些服务之间共享数据的方式来集成它们。我们如何在保持自主性的同时做到这一点?在研究如何进行集成之前,我们必须首先评估将影响集成决策的各个服务之间的无数交互。
为了确保自主性和可伸缩性,各个服务应该具有高度的内聚性(对类似功能进行分组)和松散耦合的[2]。计算机科学中的“耦合”描述了模块[3]之间的相互依赖关系。松散耦合的系统以消息的形式共享定义良好的数据,仅此而已。它们不关心状态、正常运行时间、性能水平或技术实现。
从我们的银行示例中可以看出,如果信贷和借贷服务是分开的,它们就会变得非常依赖对方,因为它们往往会影响相同的数据部分:您的账户余额。如果显示的余额不一致,哪个是对的?这些服务必须非常一致,这会导致大量的网络通信。相反,我们可以将这两个功能合并到一个内聚服务中,从而避免复杂性。
过多依赖于其他服务的数据、实现和uptimes可能是错误或过时的业务边界的征兆。业务总是在变化,这就是为什么我们需要周期性地回顾边界假设。这确保我们没有创建太细粒度的服务,即nanoservices[4]。这些毫微服务往往有支离破碎的逻辑和较差的性能。它们增加了很多维护开销。基于技术实现而不是业务边界的水平服务落入了这个陷阱。信贷和借贷功能划分到它们自己的服务中也符合这一点。没有必要打破这种凝聚力,在他们之间引入一个网络。
如果有可能发生故障,我们需要有一个计划来处理它,这是一个很好的工程实践。网络通信是[5]的一个主要例子。跨越业务边界的服务通过网络相互连接。我们需要了解其影响,因为在业务边界之外,维护团队的控制范围非常小。因此,我们应该尽可能减少网络通信。例如,在银行应用程序中,我们需要让支出报告服务知道借方交易。不正确的实现会调用该服务,询问是否可能进行这种操作,或者可能对输入参数执行验证,然后报告余额更改。这两个可以很容易地合并成一个,可以将颤振减少一半。这个想法是基于告诉,不要问[6]原则。所有这些看起来都很小,但是这些小事情加起来很快。理解将api放在HTTP上的含义是很重要的。
一直考虑API的消费者是很重要的,无论我们决定采用哪种集成。考虑到服务使用者而编写的代码具有更好的封装性,并很好地隐藏了实现细节。在这方面,测试驱动开发可能会有帮助。有了TDD,我们可以先编写消费者契约,然后编写代码来满足这些契约。PACT[7]可以帮助我们在服务之间共享这些合同。
在没有以这种方式编写的代码中划分界限变得很困难,例如,基于CRUD或基于存储库模式的api。它们与数据库实体有关。它们跨越产生更紧密耦合的业务功能。在这一点上,首先重新设计它们是一个更好的主意。
分布式系统的主要目标是更好地进行扩展。在理想的情况下,松散耦合服务共享的数据可以毫不费力地进行复制。这将需要最佳的一致性、可用性和分区公差,这意味着1)每个阅读器都获得最新的写入,2)每个请求都收到一个无错误的响应,3)由于网络分隔了微服务,它们必须处理任意数量的被删除的消息。然而,这受到CAP定理[8]的限制,它说明在任何系统中,这三个条件中只有两个是最优满足的。
因为可用性和分区公差在分布式世界中至关重要;我们必须处理较弱的一致性,如下所示:
然而,一致性本身有很多层次。Azure Cosmos DB等分布式数据库技术支持其中的五种[9]。另一方面,谷歌云扳手技术通过声称提供高一致性以及可用性和分区公差[10]来挑战CAP定理。在决定系统的数据库技术时,我们需要记住这些条件。
跨多个服务的分布式事务很难得到正确的处理,因为在提交数据[11]之前,它们要经历多个阶段。它们需要编排,这使得系统非常脆弱。所有这些麻烦都是为了到达一个不能很好地进行扩展的地方,并且不同服务之间的数据库选择可能不同。现在怎么办呢?
相反,我们可以让Cosmos DB或Cloud Spanner等新的数据库技术处理幕后的复杂性。如果这不是一个选项,我们可以在服务边界内支持事务性保证,并使用Outbox模式生成事件,供其他人使用[12]。使用我们的银行示例,当用户更改配置文件中的电话号码时,我们可以在用户配置文件服务自己的数据存储中提交该信息,并生成事件供其他系统使用。成功使用该消息后,我们的通知服务可以通知用户帐户更改,如下图所示:
在提供服务之前,我们必须考虑到网络的局限性。跨服务的同步调用通常是通过HTTP进行的,如果HTTP服务宕机会发生什么,管理起来就会变得非常棘手。我们怎么知道它下降了?我们如何处理失败?如何同步回滚应用的更改?缓存位于何处?将管理多少类型的缓存?每一个消费者?每一个电话吗?所有这些复杂性都可能导致复杂的体系结构,每个人都相互调用。
同步服务对响应时间有更高的期望,这使得它们在扩展和维护方面更具挑战性。少即是多。同步API调用通常会导致更协调的解决方案。有时,我们需要物理障碍来防止不正确的用法潜入系统[13]。编配有什么问题?我们能做些什么呢?
编制(orchestration)和编排(choreography)是常用于描述“合成Web服务的两种方式”的术语。虽然它们有共同之处,但还是有些区别的。Web服务编制(Web Services Orchestration,WSO)指为业务流程(business processes)而进行Web服务合成,而Web服务编排(Web Services Choreography,WSC)指为业务协作(business collaborations)而进行Web服务合成。
任何需要大量集中管理的系统或任何扮演这类角色的服务都可能成为问题。它们变得太重要而不能下降。一切都通过它们,增加了系统中的耦合。这种高度协调的方法称为编排。相反,编排好的方法让服务决定在事件发生时做什么。这些服务不需要中央经理的支持。回到银行应用程序示例:在从您的帐户中扣款时,transaction服务可以调用Rewards服务,后者可以调用Credit Score服务,并以通知结束它。在本例中,事务服务处于扮演交警的一切中间。相反,它可以只创建一个“帐户余额更改”事件,并让其他服务订阅这些事件并独立完成它们的操作。后者是一种更加松散的方法——即使信用评分服务宕机,仍然可以发送通知。
经过编排的方法可以区分部分宕机和完全宕机,从而使服务本身更加健壮。
考虑到您的服务结构和它们所继承的复杂的交互网络,这是构建健壮的微服务体系结构的第一步。软件工程中没有灵丹妙药,但是这些原则都是构建对服务之间交互的充分理解的基础。
要了解更多关于在分布式系统中处理网络计算谬误的知识,请查看Headspring的首席架构师Jimmy Bogard的视频:Building Distributed Systems.