实施微服务架构所面临的挑战

参考资料:

本文索引:

为什么需要微服务?

微服务最大的好处,是它提供了长期的敏捷性。微服务可以基于多个可单独测试和部署的服务来创建应用,这些服务通常足够小且拥有独立的生命周期。

微服务解决方案的优点:

  • 每个服务覆盖一个独立的业务区域,与其他区域解耦
  • 每个微服务相对较小,易于管理和改进,特别是:
    • 开发人员很容易理解并快速开始
    • 诸如 Visual Studio 这样的 IDE 可快速加载较小的项目。
    • 每个微服务都可独立于其他微服务进行设计、开发测试和部署,借此可获得敏捷性。
  • 单独扩展应用程序的特定部分: 相对于单体应用必须作为整体来扩展,微服务可以只扩展特定部分,这样就能按需扩展真正需要更多处理资源或网络带宽的功能,而不是一起将本不需要扩展的其他功能区域也进行扩展。使用的硬件更少了,也意味着节约了成本。
  • 多个开发团队可以分开工作
  • 问题的隔离程度更高
  • 方便使用最新技术

微服务解决方案的缺点:

  • 分布式应用: 开发人员设计和构建服务时,分发应用程序的过程变得更复杂。例如,开发人员必须使用诸如 HTTP 或 AMQP 等协议实现服务间通信,这增加了测试和异常处理的复杂性,还增加了系统的延迟。
  • 部署的复杂性: 具有数十个微服务并需要高可扩展性的应用程序(需要能为每个服务创建多个实例,并在多个主机间平衡这些服务),意味着 IT 运维和管理变得更复杂。如果不使用面向微服务的基础架构(如编排引擎和调度器),那么额外的复杂性可能需要比业务应用程序本身更多的开发工作。
  • 原子事务: 多个微服务间的原子事务通常是不可能实现的。必须寻求可行的办法包含多个微服务间的最终一致性。
  • 全局资源需求增加: 在许多情况下,当使用微服务方法替换单体应用程序时,新的微服务应用程序所需的全局资源数量将大于原本的单体应用程序对基础设施的需求。然而,一般来说考虑到资源成本较低,而且在单体应用演进过程中,与长期成本相比,能够将应用程序特定领域的能力扩大等优势,因此资源用量的增加对大规模、长期运行的应用程序来说,通常是一个很好的权衡。
  • 客户端与微服务的直连通信问题: 当应用程序很大,包含很多微服务时,如果应用程序需要客户端与微服务之间进行直接通信,通常会面临挑战和局限。例如客户端的需求和每个微服务暴露的 API 可能不匹配。在某些情况下,客户端应用程序可能需要通过大量单独的请求来组成用户界面,这在互联网上可能是低效的。因此,客户端应用程序对后端系统的请求应尽可能最少。客户端和服务之间的这种直接通信造成的另一个问题: 难以重构。随着时间推移,开发人员可能会修改系统划分的方式。例如,可能会合并两个服务,或将一个服务拆分为两个或更多服务。但是如果客户端直接与服务通信,则执行此类重构可能会破坏与客户端应用程序的契约。
  • 如何分割微服务: 最后,无论为微服务架构采取哪种方法,另一个挑战在于: 如何将应用程序分割成多个微服务。

实施微服务架构所面临的挑战:

尽管采用微服务架构利大于弊,但仍有诸多挑战在真正实施之前需要考虑。它们是:

如何定义每个微服务的边界

定义微服务边界可能是每位想要实施微服务遇到的第一项挑战。每个微服务必须是应用程序的一部分。但是,如何识别这些边界?

首先,需要关注应用程序的逻辑域模型和相关数据,不同上下文中所用的术语和实体可能听起来很相似,但你可能会发现在特定上下文中,某个业务概念在另一个上下文中用于不同目的,甚至名称也不同。例如,用户在「会议管理」上下文中称为「用户」,在「订单和注册」上下文中称为「买家」,在「支付」上下文中则称为「付款方」等等。

多个应用程序上下文(各上下文具有不同域)之间的边界识别方法,也可用于识别各业务微服务及其相关域模型和数据的边界。始终尝试最大程度减少这些微服务之间的耦合度。

如何创建从多个微服务中检索数据的查询

如何实现从多个微服务获取数据的查询,同时避免远程客户端和微服务之间不必要的通信。例如一个移动 App 需要一个页面来展示由购物篮、产品目录和用户身份微服务包含的用户信
息。再比如一个复杂的报表系统涉及到位于多个微服务的多个表。适合的解决方案取决于查询的复杂性。
但无论如何都需要一种方式来聚合信息,以提高系统的通信效率。最流行的解决方案如下:

  • API 网关模式: 对于来自多个微服务(拥有不同数据库)的简单数据聚合,推荐方法是称为 API 网关的聚合微服务。然而使用这种模式时需要当心,它可能成为系统瓶颈,也可能违反微服务自治的原则。为了降低这些可能性,可以使用多个细粒度的 API 网关,每个网关主要面向系统的一个垂直切片业务领域。
  • CQRS 查询/读取表: 另一种聚合多个微服务数据的方案是物化视图模式 Materialized View
    Pattern
    ,这种方案会提前(在实际查询发生前准备好非规范的数据)生成包含多个微服务数据的只读表,并且这种表会使用适合客户端应用需求的格式。
  • 中央数据库的「冷数据」: 对于可能不需要实时数据的复杂报告和查询,常用方法是将「冷数据」作为「热数据」(来自微服务的事务数据)导出到仅用于报告的大型数据库。该中央数据库系统可以是基于大数据的系统(如 Hadoop)、基于 Azure SQL 数据仓库的数据仓库,甚至是仅用于报告的单个 SQL 数据库(如果大小没有问题)。

注意,此集中式数据库仅用于不需要实时数据的查询和报告。作为事实来源的原始更新和事务必须位于微服务数据中。用于同步数据的方法有两种:使用事件驱动的通信,或使用其他数据库基础结构导入/导出工具。

如何跨多个微服务实现一致性

每个微服务拥有的数据是该微服务专有的,并且只能通过其本身的微服务 API 访问。因此,面临的挑战是如何在保持多个微服务的一致性的同时实现端到端的业务逻辑。

「目录微服务」保存所有产品的相关信息,包括它们的库存。「订购微服务」管理订单,并且必须验证新订单是否超过可用目录产品库存。在该应用程序的单片版本中,订购子系统可简单地使用 ACID 事务来检查可用库存、在订单表中创建订单以及更新产品表中的可用库存。

但是,在基于微服务的应用程序中,订单和产品表属于其各自的微服务。如图所示,微服务不应包含其他微服务在其事务或查询中所拥有的数据库。

订购微服务不应直接更新产品表,因为产品表属于目录微服务。要更新目录微服务,订购微服务应只使用异步通信,如集成事件(消息和基于事件的通信)。需要在可用性和 ACID 一致性之间做出选择。大多数基于微服务的方案都需要高可用性和高可伸缩性,而非一致性。重要应用必须保持随时在线,开发人员可通过弱一致性或最终一致性的技术来做到强一致性。 这是大多数基于微服务的体系结构采用的方法。

此外,ACID 风格或两步提交事务不仅违背微服务原则,大多数 NoSQL 数据库(如 Azure Cosmos DB、MongoDB 等)不支持两步提交事务。然而,跨服务维护数据的一致性非常重要,这个挑战关系到当某些数据需要实现冗余时,如何跨微服务执行变更的问题,例如需要更新目录微服务和购物篮微服务中的产品名称或描述时。

如何设计跨微服务边界的通信

假设客户端应用程序对单个微服务(如订购微服务)进行 HTTP API 调用。如果订购微服务在相同的请求/响应周期内转而使用 HTTP 调用其他微服务,这表示正在创建 HTTP 调用链。刚开始时,这可能听起来很合理。但是,如果继续进行,则需要考虑一些重要问题:

  • 阻塞和低性能: 由于 HTTP 的同步本质,最初的请求在所有内部 HTTP 请求全部完成前不会获得响应结果。假设这样的请求量在逐步增长,同时某个中间微服务的 HTTP 调用被阻塞,结果就是性能受到影响,并且整体扩展性由于额外 HTTP 请求的增加遇到几何级增长的影响。
  • 微服务将与 HTTP 耦合: 业务微服务不应与其他业务微服务耦合。理想情况下,它们不应「知道」其他微服务的存在。如果应用程序依赖于如例所示的耦合微服务,那么几乎不可能实现每个微服务的自治。
  • 任何微服务引起的宕机: 如果实现由 HTTP 调用链接的微服务链,那么任一微服务宕机(最终所有微服务都可能宕机),整个微服务链将挂掉。微服务系统应该设计成在部分宕机情况下尽可能地继续正常运行。即使客户端逻辑使用了越来越快和灵敏的重试机制,HTTP 调用链越复杂,实现基于 HTTP 的容错策略过程就越复杂。

生成基于微服务的应用程序时,重要的是集成微服务的方法。理想情况下,应尝试减少内部微服务之间的通信,微服务间的通信越少越好。但在许多情况下,必须以某种方式集成微服务,当需要执行此操作时,关键的规则是微服务间的通信应为「异步」。这并不代表必须使用特定协议(例如,异步消息传送与同步 HTTP)。这仅代表微服务之间的通信「应该只通过异步传播数据来完成,但不要依赖其他内部微服务作为初始服务的 HTTP 请求/响应操作的一部分。」

微服务通信通常分为两个轴:

  1. 同步还是异步
    • 同步协议: HTTP 是同步协议。客户端发送请求并等待服务响应,这与客户端代码执行无关,客户端可能是同步(线程被阻止)或异步的(线程没有被阻止,并且响应最终会到达回调)。重要的是,协议 (HTTP/HTTPS) 是同步的,仅当客户端代码接收到 HTTP 服务器响应时,才可以继续其任务。
    • 异步协议: AMQP 之类的其他协议(许多操作系统和云环境支持的协议)使用异步消息。客户端代码或消息发件人通常不会等待响应。
  2. 单个接收方还是多个接收方
    • 单个接收方: 命令模式,具有单个接收者的基于消息的异步通信意味着存在点到点通信,即,仅向正在从该通道读取数据的一个使用者传递消息,并且该消息仅处理一次。但也有一些特殊情况。例如,在试图从故障中自动恢复的云系统中,可以多次发送相同的消息。由于网络或其他故障,客户端必须能够重试发送消息,并且服务器必须实现幂等操作,以便对特定消息仅处理一次。基于消息的单接收者通信特别适用于将异步命令从一个微服务发送到另一个微服务。
    • 多个接收方: 发布/订阅模式,作为一种更灵活的方法,你可能还需要使用发布/订阅机制,以便其他订阅者微服务或外部应用程序能够收到发送者发送的通信。这样一来,以后无需修改发送者服务也可添加额外的订阅者。使用发布/订阅通信时,你可能会使用事件总线接口向任何订阅者发布事件。

如何对服务实施版本策略?(TBD)

  • HTTP 服务:
    • 使用将版本号嵌入 Url 的方式实现
    • REST 采用 Hypermedia

跨微服务的运行状况管理和诊断

跨越多个独立服务将诊断事件关联在一起,以及处理机器时钟偏差让事件顺序更合理,这些方面有着不小的挑战。如同微服务间的交互需要统一的协议和数据格式,我们也需要通过标准来规定如何记录运行状况和诊断事件,最终保存在事件存储器中供查询查看。微服务方式下,关键在于不同团队必须采用统一的日志格式。应用中也需要通过一致的方式来查看诊断事件。

  • ASP.NET HealthChecks

在编排引擎或集群中通过多个节点运行多个服务的分布式应用中,要把分布的事件关联起来就成了一个挑战。微服务应用不应该尝试自己存储事件输出流或日志,也不需要管理集中存储的事件路由。它应该是透明的,这意味着每个进程只需要将事件流写入到标准输出,运行进程的底层基础执行环境会收集这些信息。例如 Microsoft.Diagnostic.EventFlow 就是这样的一种事件转发器,它会从多个源收集事件流然后发布到输出系统,包括开发环境用到的简单标准输出,或者云上的系统,例如 Application InsightsOMS(本地部署使用)和 Azure 诊断。另外还有大量第三方日志分析平台和工具可提供搜索、报警、报表和监控等功能,甚至可以实时进行,例如 Splunk

总结

微服务实践涉及太多的细节,在后续的篇幅中,希望通过阅读更多的资料并结合自身实践,逐个攻破这些挑战。