微服务实战
上QQ阅读APP看书,第一时间看更新

1.2 微服务的挑战

我们进一步深究和分析一下设计和运行微服务系统的代价和复杂度。微服务并不是唯一通过分解和分布式实现“涅槃”(解决一切麻烦)的架构模式,但是过去的一些尝试(如SOA)已经被大家认为是不成功的。没有哪一种技术是“银弹”,比如,我们提到的微服务架构,就极大地增加了系统中运行的模块的数量。在将功能和数据所有权分发到多个自治的服务上的同时,开发者也将整个应用的稳定性和安全操作的责任分配到了这些服务上。

在设计和运行微服务应用时,开发者会遇到很多挑战,如下所示。

(1)识别和划定微服务范围需要大量专业的业务领域知识。

(2)正确识别服务间的边界和契约是很困难的,而且一旦确定,是很难对它们进行改动的。

(3)微服务是分布式系统,所以需要对状态、一致性和网络可靠性这些内容做出不同的假设。

(4)跨网络分发系统组件以及不断增长的技术差异性,会导致微服务出现新的故障形式

(5)越来越难以了解和验证在正常运行过程中会发生什么事情。

这些挑战是如何影响微服务开发的设计和运行阶段的呢?我们前面介绍了微服务开发的五大核心原则。首当其冲的就是自治性。为了让服务实现自治,开发者需要将它们设计为:从整体看,它们是松耦合的;而单独看每一个服务的话,它们内部封装了高度内聚的功能单元。这是一个不断演进的过程。服务的功能范围可能会随着时间而发生变化,开发者未来也可能会经常从现有的甚至可能将要下线的服务中剥离出新的功能来。

做出这些选择是很困难的——尤其是在应用开发的初期!服务解耦的主要驱动力就是开发者所确定的服务边界,如果这一步出错的话,服务将难以修改,整体而言,也就会导致应用不够灵活、不易于扩展。

1.划定微服务范围需要业务领域知识

每个微服务都只负责一个功能。识别这些功能需要丰富的业务领域知识。在应用生命周期的初期,开发者的领域知识充其量是不够完整的,而最糟糕的情况下,开发者了解的这些知识可能是错误的。

对问题领域理解不充分可能会导致错误的设计决定。和单体应用中的模块相比,微服务应用的服务边界更加僵化。这也就意味着,如果范围划定出错,可能给下游造成更高的代价(图1.5):开发者可能需要在多个不同的代码库上进行重构;可能需要将数据从一个服务的数据库迁移到另一个服务中;可能没有发现服务间的隐式依赖,导致在部署阶段出现错误或者不兼容的问题。

但是,基于并不充分的业务领域知识做出设计决策的事情并不是微服务所独有的问题。区别只在于这些决策所造成的影响。

注意

在第2章和第4章中,我们将通过一个示例来讨论服务识别和范围划定的最佳实践。

2.服务契约的维护

每个微服务都应该独立于其他服务的实现方式。这样才能实现技术多样性和自治性。为了做到这一点,每个微服务应该对外暴露一个契约(类比于面向对象设计中的接口)——用于定义它所期望接受和返回的消息。一个良好的契约应该具有以下特点。

(1)完整:定义了交互所需的全部内容。

(2)简洁:除了必需的信息,没有多余的内容,这样消费者就能在合理的范围内组装消息。

(3)可预测:准确反映了所有实现的真实表现。

任何设计过API的人可能都知道实现这些要求是多么困难。契约会成为服务之间的黏合剂。随着时间的推移,开发者会对契约逐渐做出调整,但是还需要保持对现有协作方的向后兼容性。稳定性和变化这两个矛盾体之间的紧张关系是很难把握分寸的。

图1.5 错误地划分服务范围可能导致跨多个服务边界进行复杂且代价巨大的重构

3.微服务应用是多个团队设计的

在规模大一些的组织中,通常是由多个团队来开发和运行微服务应用的。每个团队负责不同的微服务,他们有自己的目标、工作方式和交付周期。如果开发者还需要和其他的独立团队协调时间表和优先级,就很难设计出一个内聚的系统。因此,要协调任何庞大的微服务应用的开发,都需要跨多个团队在优先级和实践层面达成一致。

4.微服务应用是分布式系统

设计微服务应用也就意味着设计分布式系统。关于分布式系统的设计,有许多谬论,其中包括网络是可靠的、网络延迟为0、带宽是无限的以及数据传输成本为0。

显然,开发者在非分布式系统中可以做出的那些假设(如方法调用的速度和可靠性)都不再合适,基于这些假设实现的系统会非常糟糕和不稳定。开发者必须考虑到延迟性、可靠性以及应用中的状态一致性。

一旦应用成为一个分布式应用,应用的状态数据就会分布到许多地方—— 一致性就会成为难题。开发者不再能保证操作的顺序。在多个服务上进行操作时,开发者也不再能像ACID这样继续保证事务。这还会影响到应用层面的设计:开发者需要考虑服务如何在不一致的状态下进行操作以及如何在事务失败的情况下进行回滚。

微服务方案本身会使系统中可能出现的故障点增多。为了说明这一点,我们回到前面介绍过的“投资工具”那里。应用中可能出现的故障点如图1.6所示。可以看到,有些类型的故障可能会在多处发生。这些故障都会影响订单的正常处理流程。

图1.6 提交出售订单时可能的故障点

在生产环境中运行应用时,开发者可能需要回答下面几个问题,请考虑一下。

(1)如果用户不能提交订单出现故障,如何判断是哪里发生了故障?

(2)如何在不影响下单操作的情况下部署一个新版本的服务?

(3)如何知道要调用哪些服务?

(4)如何在多个服务间测试应用是否正常工作?

(5)如果某个服务不可用,会发生什么事情?

微服务并不能消除风险,而是将这个成本移到了系统生命周期的后半阶段:降低了开发过程中的冲突,但是增加了运维阶段系统部署、验证以及监控的复杂度。

微服务方案推荐在系统设计中采用演进式的方案,这样开发者可以在不修改现有服务的情况下开独立开发新的功能,就能将变更的风险和代价最小化。

但是在不断变化的解耦系统中,清楚地了解整体的情况可能变得极度困难,这又使得问题诊断和支持变得更具有挑战性。当出现故障时,开发者需要通过一系列的方式来跟踪系统实际发生的行为(调用了哪个服务、顺序是什么以及输出是什么),但是还需要一些途径来了解系统应该发生的行为。

最后,工程师会面对微服务的两大运维挑战:可观测性和多点故障。下面我们来一一展开介绍。

1.难以实现的可观测性

我们在1.1.2节中介绍了透明性的重要性。但是为什么在微服务应用中,透明性会变得更困难呢?这是因为开发者需要对整体有所了解。开发者需要将许许多多的碎片拼接起来形成整体的蓝图,所以需要将每个服务所生产的数据关联并连接到一起,进而在了解了交付商业价值整体的来龙去脉之后理解每个服务所做的工作。每个服务的日志提供了系统运行的部分视图,这是很有用的,但是开发者需要同时从微观细节和宏观整体两方面来更加全面地理解这个系统。

同样,开发者现在运行了多个应用,根据所选择的部署方式,基础设施指标(像内存和CPU利用率)和应用之间的相关性可能不再那么明显了。这些指标依旧有用,但是不再像在单体应用中那么重要了。

2.不断增加的服务使得故障点增多

如果我们说“任何可能出现故障的东西最终肯定会出现故障”,这并不是说我们太悲观。这非常重要,从现在开始,开发者就要牢牢地把这句话记在自己的脑海里:如果开发者提前认定构成系统的这些微服务是有缺陷的和脆弱的,那么就能够更好地提醒自己如何对系统进行设计、部署和监控,而不会等到出现故障时才大吃一惊。开发者需要考虑如何让系统能够在单个组件出现问题的情况下继续运行。这意味着,每个服务都需要更具鲁棒性(考虑到错误检查、故障切换、恢复),同样,整个系统也应该运行更加可靠,即便单个组件做不到100%的可靠。