Dapr 被设计用来支持云原生应用。在这一章中,我们将首先回顾 “云原生”(Cloud Native)的含义,然后讨论 Dapr 如何帮助你将你的企业内部应用迁移到云中。之后,我们将在更广泛的背景下考察 Dapr,如工具集成、系统集成应用和边缘计算。

云原生应用

珠穆朗玛峰是地球上最高的山峰。它的海拔高度为 29029 英尺(8848 米),通过冰冷的温度、高太阳辐射、危险的地形和空气中的低氧含量,给登山者带来极端的挑战。大多数登山者需要补充氧气和大量的专业登山装备来帮助他们应对恶劣的环境。然而,即使在这样的极端条件下,我们仍然可以找到一些本地的幸存者,如雪豹、喜马拉雅黑熊和喜马拉雅泰尔。虽然它们现在是濒危物种(因为人类的影响,而不是环境本身),但它们在山上漫游的时间已经比人类多了几千年。

当我们谈论云原生应用时,我们指的是专门为云环境设计的应用。当你把一个应用提升并转移到云端时,你必须确保它拥有生存所需的东西,就像攀登珠穆朗玛峰所需的氧气罐。基础设施即服务(IaaS)为应用程序提供了一个模拟的企业内部环境。它允许你的应用程序在云(缺氧的环境)中运行,就像它在企业内部(富氧)环境中一样。然而,为了充分利用云的优势 —— 如按秒计费、动态扩展和无缝故障转移 —— 你的应用程序需要能够适应云环境,这样它就可以在没有生命支持的情况下漫游和发展。

要了解如何设计一个云原生应用程序,首先需要了解云环境与企业内部环境的不同之处。

云环境

云平台管理服务器,而你的企业内部数据中心管理服务器。这两种情况下的任务都是为应用程序保持基础设施的可用性。那么,云环境与企业内部的环境有什么不同?云环境在两个方面是不同的:拥抱错误和横向规模。

拥抱错误

在本地数据中心,服务器崩溃对 IT 人员来说是糟糕的一天,但对开发人员来说是好的一天 —— 这是一个意外的工作休息。由于故障服务器的平均恢复时间(MTTR)通常相当高,IT 人员对服务器进行严格控制,并实施铁的政策以确保其稳定性和保持高可用性。

云是一个相当不同的环境。一个普通的云数据中心通常拥有数十万台物理服务器,承载着更多的虚拟化服务器。即使是 1% 的错误率也意味着在任何一天都会有成百上千的服务器出现故障。而你的工作负载可能就在这些故障的服务器中。云计算处理这些故障的方式是相当不同的 —— 它不是当场修复故障服务器,而是简单地从其庞大的服务器池中为你分配另一台健康的服务器,并期望你能像往常一样继续开展业务。在这种情况下,你的应用程序的 MTTR 几乎完全取决于你能多快地将你的应用程序迁移到新的服务器上并成功启动它。

即使服务器没有出现故障,云平台也会选择主动将你的应用程序重新部署到不同的服务器上,以优化资源利用率。例如,当一个服务器被认为太忙时,它的部分工作负载可能被驱逐以减少其负载。这意味着你的应用程序必须准备好在任何时候被移动。

为了适应这样的环境,你的应用程序必须能够在没有人类互动的情况下被持续部署。一致性是这里的关键,在过去的几十年里,已经开发了许多技术来实现这一点 —— 从安装脚本到安装包,再到虚拟化,最终是容器。

容器提供了一种轻量级的方式,将应用程序的所有依赖性与应用程序本身打包在一起。这使得应用程序可以快速迁移到不同的服务器上,并以一种可预测的、一致的方式启动,而不会丢失任何依赖。因此,容器在云时代的成功并不是偶然的,它们为应用程序在云环境中保持可用性提供了关键技术。

物理学告诉我们,移动和安装一个较小的软件包比处理一个较大的软件包要快。这启发了微服务架构,在这种架构中,一个应用程序被分割成更小的、独立的、松散耦合的组件,因此各个部分可以更容易地被移动。

然而,一个组件的移动不应该破坏与其他组件的通信。服务网等技术允许在一个受控的计算平面内进行这种组件的移动。诸如松散耦合的架构选择允许更大的灵活性,通过一个强大的消息传递骨干网,用消息传递取代组件之间的直接通信。

移动状态甚至更具挑战性。人们常说,数据有它的引力,把应用程序拉到它周围。Dapr 一般假设数据被保存在外部数据存储中。换句话说,Dapr 允许应用程序在这些数据引力井周围移动,这些引力井大概是稳定的,很难移动。我们将在本章后面讨论一些状态可用性的模式。

水平伸缩

当一个应用程序在内部数据中心需要更多的处理能力时,需要升级托管服务器,为应用程序提供更多的内存、更多的磁盘空间或更多的 CPU 能力,或者需要将应用程序迁移到一个更有能力的服务器上并重新启动。

你可以在 IaaS 上进行类似的升级,因为云平台正试图提供更多、更强大的机器选项,以满足更大的应用程序的需求。然而,在这样做的时候,你并没有充分利用云的弹性。弹性是云计算最大的价值主张之一,它允许你为你所消耗的计算资源的数量付费,精确到秒。当你需要更多的处理能力时,你可以招募你所能负担得起的服务器。云将你的应用程序的一个实例放在每个服务器上,并将服务器连接到一个负载平衡器后面,将用户流量分配到所有连接的服务器。通过这种安排,理论上你可以根据你的需要扩展到多少个服务器实例,当你的应用程序没有太多的(或任何)流量时,可以扩展到一个甚至零个实例。

扩展需要你的应用程序能够在多个服务器上稳定地部署和配置,所以我们所讨论的关于拥抱错误的内容也适用于此 —— 我们需要能够快速和稳定地部署应用程序的多个运行实例。

只要你能获得运行你的应用程序所需的资源,你就不应该太在意它们是如何提供的,在哪里提供的。换句话说,你不希望承担任何不必要的负担来管理底层基础设施。这个想法是整个无服务(Serverless)趋势的根源所在。

现在你已经对云环境与传统数据中心的不同之处有了充分的了解,我们可以总结一下设计一个云原生应用程序需要做什么。

云原生设计

云原生应用的设计应能包容错误,并能横向扩展。经验告诉我们,具有以下特点的应用程序能更好地适应云环境:

由独立的、松散耦合的组件组成

为了减少因服务器迁移而可能造成的停机时间,你的应用程序应该被分解成可以单独部署的小块。由于通常没有保证组件启动的顺序,每个组件都需要自给自足。如果它所依赖的某些组件不可用,它不应该崩溃。相反,它应该等待,并在所有依赖性恢复后恢复正常运行。

基于消息的整合

当你的应用程序的组件运行在一个受控的计算平面上,它们可以承担连接,它们可以根据需要漫游。这适用于单个集群或通过私有网络或服务网连接的多个集群。如果这样的假设不成立,你的应用程序的组件可以使用一个消息传递骨干,通过消息相互连接。正如前面几章所介绍的,基于消息的集成和事件驱动的设计有很多好处。在许多情况下,基于消息的设计可以帮助消除那些可能成为系统瓶颈的中心件。例如,你可以让一个作业发布者将作业发布到一个队列中,你可以根据需要启动尽可能多的作业处理器来耗尽队列中的作业,而不是使用一个中央作业调度器来调度作业给不同的接收者。

可持续的部署

你的应用程序组件应该被打包成可以稳定部署的包格式。包应该是自成一体的,除了预期的包运行时间,如容器运行时间,没有任何假定的外部依赖。通常需要一个包库来促进包的分发,但其他方式,如文件共享,也可用于简单的情况。

可观察

因为你的应用程序是由云中的移动组件组成的,所以当出现故障时,往往很难跟踪发生了什么。因此,最好是每个组件都能向中央数据收集器报告遥测和跟踪,这样你就能观察到整个系统。同样重要的是,你可以将来自分布式来源的数据关联起来,以便你可以追踪完整的调用链。

与基础设施隔绝

虽然你不必把你的应用程序设计成与云无关的,但在你的应用程序和底层基础设施之间建立一个缓冲区通常是可取的。这意味着不要直接依赖平台上的特定服务,如状态存储、秘密存储和负载平衡器。经验告诉我们,应用程序的要求总是不断变化的。你的应用在三年后的样子可能与你今天的设想完全不同。你也可能面临不同的规模和延迟需求,并需要解决非功能性的需求变化,例如,如果你的公司与一个新的服务提供商建立了伙伴关系。通过在你的应用程序和底层基础设施之间建立一个缓冲,你可以为应用程序创造更多的灵活性来适应这些变化。缓冲区还允许你使你的应用程序适应不同的托管需求,如在基于云的服务器不可用的情况下,托管在企业内部的数据中心。

可横向扩展

你的应用程序中的组件应该是可横向扩展的。这意味着一个组件实例不应假定它是世界上的一个单子。如果状态是跨实例共享的,你的应用程序必须考虑到潜在的读/写竞赛条件,并准备好解决可能的冲突。一个更好的设计是确保一个实例不维护本地状态或将状态封装在自己身上。这种设计最大限度地提高了组件的移动性,因此组件可以很容易地被重新安置到其他地方。

明确的 API

在一个复杂的分布式系统中,许多组件是由不同的团队设计、实现和管理的。明确地将你的组件的界面定义为 API 是一个很好的做法,你应该将该 API 作为你的组件和该组件的任何消费者之间的一个有约束力的合同。API 的定义不仅包括方法,还包括数据合同。你的 API 应该有明确的版本,以避免在你发展 API 设计时客户可能产生的混淆。你应该要求你的同行遵循同样的 API 设计原则,这样你的代码就不会因为别人不稳定的 API 表面而被意外地破坏。

Dapr 的设计已经考虑到了实现这些特性。下一节讨论了 Dapr 协助开发非常适合云计算的应用的几个场景。

使用 Dapr 的云原生应用

Dapr 是一个分布式应用程序运行时间,旨在帮助你建立云原生应用。然而,Dapr 的独特之处在于,它并不要求你从第一天起就采取微服务的方法。相反,你可以按照自己的节奏发展你的应用程序,并在需要时从 Dapr 获得帮助。

本节首先介绍了你如何使用 Dapr 为云计算进化一个传统的应用程序。然后,它介绍了在使用 Dapr 从头开始设计云原生应用程序时可以使用的一些模式。

演进一个单体应用

让我们从一个单体应用开始,它的所有逻辑都在一个代码包里,并作为一个单体运行。它使用一个外部数据库来保存状态,但它也在内存中保留了相当多的状态。它使用微软活动目录(AD)进行认证。图 6-1 显示了该应用的架构,当它在现有系统中运行时。
image.png

图 6-1. 一个单体应用

我们要做的第一件事是引入一个 Dapr 边车来接管内存状态的访问。因为 Dapr 可以被配置为使用内存中的 Redis 状态存储,这样做只在延迟方面引入了最小的开销。

图 6-2 展示了包含 Dapr 边车的更新架构。这个更新并不影响现有的访问外部状态存储的应用程序代码。Dapr 假设状态访问是基于键/值对的;它并不管理相关的数据库,而这正是这里可能使用的东西。这表明了 Dapr 的非侵入性 —— 你可以选择只使用你需要的功能,而保持你的其他代码不受影响。

这是一个小而重要的变化 —— 应用程序代码本身变得无状态。这意味着代码可以被云平台轻松地重新定位。作为奖励,现在的应用程序代码可以被容器化,以进一步提高代码的流动性。当你将新的容器化应用部署到 PaaS 平台时,也许是建立在管理的 Kubernetes 集群之上,当容器镜像在集群节点上被缓存时,你会得到自动故障转移和潜在的更快部署时间。
image.png

图 6-2. 通过 Dapr 分离内存中的状态

如果你仔细看这张图,你会发现我们在旁边偷偷地做了一点改变。我们将活动目录更新为 Azure 活动目录(AAD)。然而,实际的转换比这个随意的变化所显示的要复杂一些。虽然 AAD 可以连接到 AD 并与之同步,但你不能再依赖 Kerberos 等协议;你必须调整你的应用程序,以使用更适合互联网的协议,如 SAML 2.0 和 OAuth 2.0。这只是你从企业内部环境切换到云端时必须做的事情,除非你把你的应用部署在与现有 AD 域连接的虚拟机上(这也是一种可能性)。

我们还没有触及核心应用的代码。如前所述,应用程序以单例形式运行,这意味着它只能作为一个单一的实例运行。一个单例程序通常有以下特点。

  • 单独访问状态:在单例模式下,应用程序拥有整个状态集,并且对数据存储有独占的读/写权限。例如,在一个单例汽车预订系统中,应用程序可以查询数据库,找到一个未预订的汽车,并为客户预订。它不需要担心在返回查询结果和完成预订操作之间会有其他人来抢车。当然,一个罪恶的应用程序可以支持多个用户。在这种情况下,应用程序通常依靠数据库事务隔离来按顺序处理用户事务。
  • 内存中状态的因果使用:一个单例程序可以保持内存中的状态以获得更好的性能。例如,一个投票应用程序可以保留一个领先候选人的内存列表。因为单例程序是唯一一个读写内存列表的程序,它可以安全地使用内存数据结构来提供快速访问。当我们把内存中的状态移到 Dapr 后面时,我们可能已经小心翼翼地让 Dapr 使用强一致性和最后写入的策略来避免引入意外的行为。然而,这并不重要,因为无论如何,这个应用程序是一个单例。当一个单例程序崩溃并重新启动时,它可以选择在某个恢复点目标下从外部存储恢复其状态,但一些进行中的事务可能会丢失。
  • 全局决策:因为单例程序知道应用范围内的一切,它可以在任何时候做出明智的决定。例如,一个卡车车队调度员可以为车队中的所有卡车计算出最优化的路线,因为它对整个数据集具有完全的可见性。

然而,Dapr 并不是一个可以解决一切问题的灵丹妙药,所以在这个时候,你需要评估你的代码,并决定你是否要脱离单例模式,以便你的应用程序可以水平扩展。保持单例,这是一个很好的设计模式,在某些情况下肯定是一个有效的选择。

为了便于讨论,我们假设在这种情况下,你确实想脱离单子模式。你可以考虑的一件事是将代码中的作业生成部分与作业处理部分分开,并将它们打包成两个独立的包。在一个抽象的层面上,所有的系统都接受一些作业并处理这些作业。接收作业的部分通常涉及用户交互,而处理作业的部分通常可以作为后台进程运行。因此,我们可以将单体应用分为前端和后端。这也是在系统中引入松散耦合的一个好地方 —— 与其让前端直接与后端对话,不如在两者之间使用一个作业队列,如 图 6-3 所示。这种策略在可能的情况下将应用程序分割开来,并逐渐将应用程序的一部分转移到云端。这有时被称为绞杀模式,因为旧的服务会逐渐从单体中分离出来,成为独立的微服务。
image.png

图 6-3. 将应用程序的工作创建和工作处理部分分开

分离后,工作创建者变得无状态,因为它所做的只是通过 Dapr 将工作发布到工作队列中。这与前端很一致,因为用户界面通常是无状态的,可以根据需要进行扩展,为用户提供更好的响应时间。后台可以根据工作负荷独立扩展。当然,如果后端仍然是一个单子,它将需要时间来耗尽作业队列。

图中没有显示的是对用户的反馈。在这种设计下,用户和系统之间的交互是异步的 —— 用户在提交作业后并不能立即得到反馈。一个单独的渠道,例如反向的消息管道,可以用来向用户发送反馈。

这是一个相当长的历程,应用程序正准备进行云计算的扩展。然而,我们可以更进一步。如果我们把作业封装成一个角色,我们可以使作业成为一个独立的元素,可以由作业处理器操纵。我们甚至可以尝试让作业自主化 —— 作业处理器启动一个带有提醒功能的作业角色(见 第 5 章),不断检查作业是否已经完成。在这种情况下,作业处理器只需启动一个作业角色,而所有作业都可以在所有可用的计算节点上并行执行。而在这时,我们可以使用 Dapr 认证中间件(见 第 4 章 )来处理认证。更新后的架构如 图 6-4 所示。
image.png

图 6-4. 使用一个作用行为体

图 6-4 还达到了另一个里程碑 —— 应用程序与底层基础设施相分离。这意味着当应用程序被部署到不同的环境中时,它可以动态地重新配置。这并不总是必要的,但如果你担心被厂商锁定,这是一个很好的功能。在这一节中,我们采用了一个传统的应用程序,并将其 “Daprized”,正如 Mark Russinovich 所说。

接下来,让我们看看如何用 Dapr 设计一个全新的云原生应用。

设计一个云原生应用

在这一部分,我们将在一个预订度假旅游套餐的旅行社网站上工作。该网站有一个面向公众的终端供个人用户使用。它还为企业提供客户化的部署,以提供与他们现有系统的集成体验,如会计和邮件。我们将假设所有的初始需求收集工作已经完成,我们将直接跳到设计我们的第一个最小可行产品(MVP)。我们将无法讨论设计一个新的应用程序所涉及的所有细节,而是将重点放在总体思路上。

:::info 架构师最重要的工作是正确绘制组件的边界。一个有经验的架构师在设计当前版本的组件时,只需为未来的版本留下自然的扩展点。他们也会考虑工程团队的结构和能力,以及企业的长期目标。这需要技术能力和商业眼光,以及一些艺术性的修饰 —— 这也是好的架构师难找的原因之一。许多架构师试图通过过度的工程设计来创造灵活的架构。一个好的架构师知道什么时候应该做减法以保持架构的精简。 :::

当设计一个新的系统时,一些架构师喜欢首先确定系统所管理的实体的类型,包括对这些实体可以采取的行动。然后,他们围绕实体的创建、读取、更新、删除(CRUD)动作设计 API。这是一个很好的方法。但是,这些动作不一定与用户的工作流程相一致。

另一种方法是先确定系统的不同类型的用户,然后围绕他们的工作流程设计你的 API(这基本上是领域驱动设计的思想,或称 DDD)。将 API 按用户分组是很重要的,因为不同的用户可能有不同的进入点、规模因素、认证要求和访问策略。此外,考虑拥有独立的只读 API 和操作实体状态的 API 也是审慎的,因为你通常可以为只读 API 设计额外的操作(如缓存和预填充视图)。

当你用 Dapr 设计应用程序时,你可以采取一种混合方法 —— 你可以将系统中的实体建模为行为体,并根据用户活动定义 API。让我们假设我们已经确定了系统中的以下实体:

  • 度假套餐
    • 航班预订
    • 汽车预订
    • 酒店预订
  • 旅行者
    • 简介,如姓名、地址和旅行预算
    • 付款方式
    • 预订
  • 旅行政策
  • 合作伙伴
    • 简介,如姓名、合同信息和服务类型
    • 库存
    • 特别优惠和促销活动

而我们已经确定了以下用户和他们的典型工作流程:

  • 管理员
    • 管理旅行政策
    • 管理合作伙伴
    • 管理度假套餐
    • 管理旅行者
  • 旅行者
    • 预订度假套餐
    • 取消预订
  • 合作伙伴
    • 更新库存
    • 更新报价

考虑到这些,我们可以提出一个具有 UI 层、API 层和实体层的设计,如 图 6-5 所示。图中显示,大多数实体都被封装成了角色。然而,合作伙伴的库存被留作一个 API,用来与合作伙伴的库存系统集成。你应该注意,图中块的数量不一定反映代码包的数量。例如,你可以在一个代码包中定义所有的行为体类型;另外,许多网络框架都支持将前端(HTML)和后端(API)打包在一起。

这是一个合理的第一步。然而,我们需要更多地考虑预订过程。正如前几章所介绍的,预订一个度假套餐是一个复杂的分布式交易,可能需要几分钟甚至几个小时才能完成。我们需要的是一种方法来驱动多个预订程序并行进行。实现这一目标的方法之一是使用一个度假套餐行为体来表示要预订的套餐。这个角色通过旅行者用户界面被赋予一个期望的状态,它使用一个定时器来检查它的状态,并试图通过调用相关伙伴的 Inventory API 来使它与期望的状态相一致。我们还将引入 图 6-4 中单独的作业创建和处理设计,这样我们就可以把包的生成和包的预订解耦。在这种情况下,这是必要的,因为包裹预订是一个很长的过程,我们不能耽误用户的请求,等待它。然而,在这种情况下,我们不会使用一个消息传递系统。相反,Traveler API 只是在收到请求时启动新的度假套餐虚拟行为体。
image.png

图 6-5. 旅行社网站的初步设计

图 6-6 显示了更新后的架构。在这个更新的设计中,Traveler UI 不再访问 Inventory API 了。相反,调用 Inventory API 变成了 Vacation package 行为体的一个实现细节。Traveler API 直接访问行为体的状态存储,以提供更快的聚合和对现有预订的灵活查询,并且 Inventory API 被更新以纳入合作伙伴的报价。最后,所有相关行为体的盒子都被更新为多个盒子,以表明它们可以被横向扩展。

我们认为这对单个租户来说是一个体面的设计。对于企业用户,我们有三个选择。第一个是为每个企业客户部署整个堆栈。第二种是使管理路径成为多租户。第三种方案是让所有的东西都成为多租户。第一个方案不需要修改代码。然而,当管理多个客户时,它带来了挑战,例如在不同的部署中共享合作伙伴的库存。第二种方案需要有限的代码修改,但你需要单独管理客户的部署。这不一定是坏事,因为不同的客户可能有不同的规模因素和不同的策略更新频率,他们甚至可能想使用不同的托管环境。根据你的客户需求,每个客户有一个单独的堆栈可能是最好的选择。第三种方案需要修改最多的代码,但它最大限度地减少了管理费用,因为你只需要管理一个单一的全局部署。
image.png

图 6-6. 更新后的旅行社网站设计

无论你选择哪种途径,你都应该将代码定制限制在尽可能少的组件中。在我们的方案中,最可能的是,旅行政策和政策强化的方式会因用户而异。为了弥补这一点,你可能想引入一个单独的政策执行者 API 来隐藏用户之间的差异,并向系统的其他部分提供一个标准的 API。

当你使用这种设计时,需要注意的是。如果你的行为体只是封装了状态,而你的 API 没有正确地分割,你就会陷入 Blob 反模式,即一段复杂的代码对许多单独的实体进行操作。在这种特殊的情况下,你希望行为体尽可能地自主,而 API 只是在不同的行为体实例上触发活动。

到目前为止,我们已经讨论了如何利用一个遗留系统并将其演化为云,以及如何利用 Dapr 从头开始设计一个适度复杂的云原生应用。但是,一个应用程序很少孤立地运行。它经常需要与外部系统沟通。我们将在下一节讨论如何使用 Dapr 将你的应用与外部系统集成。

使用 Dapr 进行系统集成

我们在 第 3 章 介绍 Dapr 绑定时讨论了一些基于消息的集成模式。基于消息的集成是整合多个互不相干的系统的一种有效方式,而这些系统之间是互不相干的。事实上,有各种商业产品可以达到这个目的,比如 Microsoft BizTalk、Azure Logic Apps 和 AWS Step Functions。你当然可以利用社区贡献的绑定或创建你自己的自定义绑定来连接这些系统,这不在本书的范围之内。本节的其余部分将讨论一些使用 Dapr 进行系统集成的有用模式。

作为有限状态机的分布式工作流

当我们在 第 3 章 讨论 Saga 模式时,我们采取了以事件为中心的观点 —— 工作流对各种事件作出反应并执行相应的行动。对工作流进行建模的另一种方法是将其视为一个有限状态机(FSM)。简单地说,一个有限状态机有一个有限的可能状态列表,它根据输入和当前状态在各状态之间转换。例如,第 3 章 中的工作流程样本可以用 图 6-7 中的状态机来描述。为了简单起见,它并不包含所有的取消转换。
image.png

图 6-7. 预订工作流程的有限状态机

你可以使用 Dapr 行为体轻松地对这个状态机进行建模,它可以暴露必要的方法来触发相应的转换。与 第 3 章 中的系统的一个关键区别是,行为体实例可以决定是否允许某种转换,相比之下,工作流将状态与已经发生的事情(表示为事件)进行协调。

FSM 在任何成功的状态转换时都会保存其状态。这意味着 FSM 可以在任何时候关闭,并根据需要从其状态恢复。除了要求状态操作是事务性的,这种设计还要求 FSM 只在定义的状态之间转换,而不使用任何可能在 FSM 实例重新启动时造成混乱的中间状态。

对 FSM 更详细的讨论不在本书的范围之内,因为适当的 FSM 实现需要比这里所描述的多得多的东西。我们将把进一步的探索留给读者,作为一种练习。接下来,我们将换个角度来关注有状态应用程序的状态同步问题。

系统同步

系统集成中的一项常见任务是使两个或多个系统保持同步。因为集成的系统往往是由业务流程耦合在一起的,我们需要确保当流程从一个系统转到另一个系统时,两边的数据是同步的,以避免不必要的冲突。本节介绍了几个用于此目的的方法。

共享数据库

拥有一个共享的数据库,听起来像是在两个系统之间引入了一个紧密的耦合。然而,对于两个可以访问同一个数据库的远程系统来说,比如一个云托管的数据库,共享数据库模式提供了一种有效的方式来同步系统间的大量数据。在实施该模式时,你应该考虑为同步的目的专门设置一个数据库或一个表,而不是强迫系统共享相同的数据库模式。这使你在独立发展每个系统时有更大的灵活性。

你可以使用 Dapr 来封装状态访问。你所需要做的就是配置两个系统,使它们用于同步的状态存储指向同一个数据基础位置。如果你想实现一个共享的关系型数据库,你需要编写自定义代码,并使用 Dapr 向系统的其他部分暴露一个同步 API。在这种情况下,使用 Dapr 的好处是,你也可以利用 Dapr 的中间件机制来插入额外的数据处理功能,如正常化、批处理和压缩,以进一步优化数据同步。图 6-8 展示了一个使用 Dapr 来封装同步 API 并使用 Dapr 中间件来定制数据同步管道的示例设置。
image.png

图 6-8. 使用 Dapr 的共享数据库

如果直接的数据库共享不可行,你可以利用现有的数据同步解决方案,支持你选择的数据库平台。然而,在这种情况下,数据只是最终一致,所以你需要确保系统的设计能够应对暂时的不一致。

如果你把共享数据库换成了消息骨干,那么这种模式就会转变为收件箱/发件箱模式,接下来会介绍。

收件箱/发件箱和邮递员

收件箱/发件箱是一种简单而随意的同步机制。在这种模式下,每个系统都维护一个收件箱和可选的一个发件箱。每当一个系统需要与另一个系统同步时,它就向目标系统发送一条消息。该消息包含了要同步的数据,或获取更新数据的指令。这不是一个僵硬的数据同步模式;相反,它允许系统互相通知对方的变化。收件人可以选择响应这些通知或丢弃这些通知。

依靠系统间的直接信息传递,假定所有系统都知道对方,或者至少知道对方的收件箱地址。这是不可取的,原因有二:首先,当一个系统需要与多个系统同步时,它必须向每个系统发送消息;其次,收件箱地址必须在参与的系统上进行交换和配置,这引入了管理开销。一种不同的方法是使用发布/订阅。发布/订阅允许一个系统同时通知多个系统,它消除了一个系统知道目标收件箱地址的需要。它只是将信息发布到一个约定的主题。这种设计的一个注意事项是,由于所有的系统都订阅同一个主题,一个系统需要过滤掉它自己发送的信息。另一种设计是,每个系统向自己的 “外发”(Outgoing)主题发布信息,而接收系统则订阅该主题。图 6-9 说明了三个系统如何通过 (a) 收件箱/发件箱模式,(b) 向一个共同的主题发布/订阅,以及 (c) 通过外发主题发布/订阅来保持同步。
image.png

图 6-9. 收件箱/发件箱模式的变化

有了收件箱/发件箱模式,你还可以选择引入一个邮件管理员。邮递员是一个集中的消息处理程序,在收件箱和发件箱之间移动消息。使用邮件人的主要好处是,你可以通过控制邮件人来集中管理集成拓扑结构和同步策略。你还可以建立和扩展一个专门的基础设施来托管邮件人,以减少单个系统的信息处理负担。

通过消息传递进行同步是非常强大的,因为它允许系统被整合,但同时保持松散的耦合性。下一步,我们将研究一种更严谨的模式,它可用于通过消息传递进行同步。

事件源

事件源模式的要点是将所有的状态突变都视为事件。例如,在管理一个银行账户时,不是直接更新账户余额,而是将所有的取款和存款都记录为事件。为了得到账户余额,你从初始余额开始,这个余额很可能是零,然后回放账户上曾经发生过的所有事件。当然,你会想实现一些优化,比如定期创建余额的检查点,并在检查点之后才应用事件。

事件源模式有很多好处和应用。首先,你可以返回到系统中的任何时间点。当你想纠正一个较早的事务时,这是相当强大的。例如,如果你想纠正一个月前发生的事务,你可以将系统状态倒退到该事务之前,应用更新的事务,然后回放其余记录的事件。第二,该模式允许你将事件传送到不同的环境中,以达到诊断的目的。如果你想调试一个发生在执行环境中的问题,你可以把事件运送到一个单独的环境,回放它们,并分析问题的根源。然后,你可以在生产数据的副本上测试你的解决方案,只有当你确信一切都准备好了,才可以将变化应用到生产中。第三,该模式在数据库上使用仅附加的操作。这个想法是为了避免数据库更新,因为数据库更新通常比插入操作更昂贵,性能更差。当你更新一条记录时,你需要在该记录上加一个锁,以避免冲突的写操作,而锁会导致各种问题,如锁升级、死锁和瓶颈。该模式还可以将更新与读取分开,这样所有的读取操作都不会受到写入的干扰。事件可以被回放并以不同的方式聚合,以生成重新填充的视图,用于快速查询。最后,该模式可用于建立一个备份和恢复系统。你可以将所有的事件归档并回放,将系统还原到任何时间点上。

你可以通过将事件从一个系统运送到另一个系统并在另一个系统中回放来使用事件源模式进行同步。例如,在一个主/辅配置中,主服务器将其所有的事件运送到辅服务器上。当主服务器发生故障时,一个辅助服务器被选为新的主服务器。因为主服务器和辅助服务器之间的同步通常是定期分批进行的,然而,当故障转移发生时,你可能会失去最新的批次。

你可以使用 Dapr 的状态存储接口实现一个只附加的状态存储。基本上,Set 方法将更新事件附加到数据库中,而不是更新现有的记录,Read 方法根据应用程序的需要以一定的频率维护检查点。它从最后一个已知的检查点读取并回放从该点开始的事件。

我们在这里几乎没有触及系统集成的表面。我们希望看到的是 Dapr 社区集体建立起最佳实践和模式,以及系统集成的工具和服务。

接下来,我们将简要介绍 Dapr 如何与现有的工具和框架合作,以实现各种场景。随着 Dapr 的快速发展,我们在这里所涉及的将只是一小部分可能的东西 —— 但我们希望我们能够提供一个视角,说明 Dapr 如何能够帮助不同的开发者社区,并使他们能够实现更多。

更大的生态系统中的 Dapr

Dapr 无疑吸引了许多不同社区的关注。我们很高兴看到越来越多的团队在他们的生态系统中启用 Dapr,并使用它来构建创新的解决方案。本节提供了我们迄今为止看到的一些最有趣的项目的简要调查,从工具开始。

Yeoman Dapr 生成器

Yeoman 是一个流行的脚手架工具,可用于搭建许多不同类型的应用程序。它是开发人员在学习新的语言或框架时用来快速启动和运行的好工具。现在有一个 Yeoman Dapr 生成器,可以帮助你生成 在C#、Go、JavaScript、Python 和 TypeScript 中生成支持 Dapr 的应用程序。生成的代码包含用于方法调用和发布/订阅以及绑定的样本。

要使用生成器,只需在安装了 Yeoman 和生成器后,在终端窗口中输入 yo dapr,然后按照向导的提示创建一个完整的 Dapr 应用程序。

在 Visual Studio Code 中使用 Dapr

Visual Studio Code 是世界上最流行的代码编辑器之一。数以百万计的开发者使用 Visual Studio Code 来处理他们的项目,使用各种编程语言和框架。作为一个语言无关的运行时,Dapr 很适合这个生态系统。Visual Studio Code 团队也看到了 Dapr 的价值,所以他们在 Visual Studio Code 市场上发布了一个 Dapr 插件

学习一个工具的最好方法是尝试使用它(最好有一个有经验的向导,这样你就不会伤害自己)。因此,让我们通过一个完整的例子,使用 Visual Studio Code 扩展创建、运行和调试一个 Dapr 应用程序。

:::info 下面的步骤是基于预览版的扩展。发布的版本中的实际经验可能有所不同。 :::

  1. 安装 Visual Studio Code Dapr 插件;
  2. 创建一个新的文件夹;
  3. 在 Visual Studio Code 中打开该文件夹;
  4. Ctrl-Shift-D 键打开运行面板;
  5. 点击 create a launch.json file 链接,创建一个新的启动配置,选择 Node.js 作为目标环境;
  6. 关闭生成的 launch.json 文件;
  7. Ctrl-Shift-P 键,调出命令控制板;
  8. 启动 Dapr: Scaffold Dapr Tasks 命令;
  9. 选择默认的启动程序配置;
  10. 输入 nodeapp 作为 Dapr ID,然后按 Enter 键继续
  11. 将应用程序的端口保持在默认的 3000,然后按 Enter 键继续;
  12. 创建一个 Node.js 应用程序,你可以重新使用介绍中的 Node.js greeting 服务;
  13. 在 Visual Studio 终端,运行 npm install 来安装必要的 Node.js 包;
  14. 打开 app.js 文件,在 app.post 方法处设置一个断点;
  15. 再次按下 Ctrl-Shift-D
  16. 在运行面板中,确保你选择新生成的带有 Dapr 启动配置文件的启动程序,然后选择 Run Start Debugging 菜单项,你应该观察到,你的应用程序和 Dapr 边车都被启动了;
  17. Ctrl-Shift-P 键,调出命令控制板;
  18. 启动 Dapr: Invoke (POST) Application Method 命令;
  19. 选择 nodeapp 作为目标应用程序,greeting 作为目标方法,然后输入一个简单的 JSON 有效载荷,比如{"a": "b"},然后按回车键发送请求。

你应该看到中断点被击中,如 图 6-10 所示。
image.png

图 6-10. 在 Visual Studio Code 中调试一个 Dapr 应用程序

除了工具支持外,我们还在与社区合作,提供与现有框架的深度集成,如 ASP.NET Core 和 Spring Boot。我们接下来会看一下这方面的一个例子。

在 ASP.NET Core 中使用 Dapr

Dapr.AspNetCore 是一个 NuGet 包,它允许你轻松地将 Dapr 功能(如状态管理和绑定)集成到你的 ASP.NET Core Web 应用程序中。

该 NuGet 包包含 ASP.NET Core 扩展,允许你装饰你的 ASP.NET 控制器以启用各种 Dapr 功能。例如,下面的 代码片段 演示了如何通过从指定的状态存储和键中访问一个值来使控制器具有状态。

  1. [HttpGet("{account}")]
  2. public ActionResult<Account> Get([FromState(StoreName)]StateEntry<Account> account)
  3. {
  4. if (account.Value is null)
  5. {
  6. return this.NotFound();
  7. }
  8. return account.Value;
  9. }

而下面的代码片断显示了如何将控制器方法绑定到消息主题上,以便该方法不仅可以被直接的网络请求触发,还可以被发布到主题上的事件触发。

  1. [Topic("deposit")]
  2. [HttpPost("deposit")]
  3. public async Task<ActionResult<Account>> Deposit(Transaction transaction, [FromServices] DaprClient daprClient)
  4. {
  5. var state = await daprClient.GetStateEntryAsync<Account>(StoreName, transaction.Id);
  6. state.Value ??= new Account() { Id = transaction.Id, };
  7. state.Value.Balance += transaction.Amount;
  8. await state.SaveAsync();
  9. return state.Value;
  10. }

这很强大。通过 Dapr 化你的 ASP.NET Core 控制器,你可以使相同的 API 表面(实际上是相同的 API 代码!)被一个消息系统触发。例如,本章前面提到的收件箱/发件箱模式可以通过这种注释语法轻松实现。如果没有 Dapr,你就必须建立一个消息传递层,并找出如何将该消息传递层与你的 API 层一起托管。有了 Dapr,你所需要做的就是添加 Topic 注解并配置你的事件源。

Dapr 边车是一个单独的微服务的伴侣。一个复杂的系统通常由几十个甚至几百个微服务组成。在许多情况下,能够把所有这些服务作为一个单一的单元 —— 一个应用程序来管理是非常理想的。在下一节中,我们将讨论 Dapr 如何在一个更大的应用程序中使用。

在一个更大应用中的 Dapr

在写这篇文章的时候,Dapr 本身并没有提供一种方法来描述一个多组件的应用程序。这是因为 Dapr 的目标是成为一个编程模型,而不是一个应用模型。编程模型和应用模型的区别在于,编程模型关注的是如何编写处理单元或服务,而应用模型关注的是处理单元的拓扑结构。

正如你已经知道的,Dapr 边车可以作为一个进程或 Docker 容器运行。作为一个标准的容器运行是很好的,因为这意味着你可以使用任何现有的容器工具和清单格式,与 Dapr 边车一起描述你的工作负载。例如,在本书早些时候,你看到了一个例子,说明如何通过在 Kubernetes 部署 Pod 规范中添加 Dapr 注解,使 Dapr 边车被注入你的应用 Pod 中。

  1. annotations:
  2. dapr.io/enabled: "true"
  3. dapr.io/id: "nodeapp"
  4. dapr.io/port: "3000

同样,你可以在 Helm Chart 中描述 Dapr 边车,在 CNAB 包中打包 Dapr 边车,或者使用 Knative 将 Dapr 边车和你的应用程序一起部署。

我们还谈到了 OAM,这是一个开源项目,旨在提供一种对开发者友好的建模语言,使开发者能够以一种平台无关的方式设计和描述他们的应用程序。开发人员可以设计他们应用程序的确切逻辑拓扑结构,包括组件、其规模和连接性、安全范围和入口路线。然后,他们可以将应用清单发送给操作人员,他们可以在特定的平台上实现这些描述的意图。例如,运营团队可以选择在将应用部署到 Azure 时使用 Azure 负载均衡器作为入口,而在将同一应用部署到边缘服务器时使用基于容器的 Nginx 代理作为入口。这样的决定可以在没有开发人员参与的情况下做出,开发人员永远不需要关注(甚至不需要知道)任何基础设施的细节。

OAM 应用模型由一个兼容 OAM 的控制平面管理。在 Kubernetes 上,一个 OAM 应用模型通常由几个客户资源组成,由相应的运营商管理。Dapr 与这样的控制平面配合得很好,因为它们通常支持 Kubernetes Pod 注释或类似机制。关于 Dapr 支持的更多细节,请参见你选择的控制平面的文档。

在结束本章的这一部分之前,我们要解决一个常见的问题。

Dapr 和服务网格

我们经常被问到 Dapr 和现有的服务网格解决方案(如 Istio、Linkerd 或 Consul)之间有什么区别。

Dapr 和服务网格实有一些相似之处。因为它们都使用了边车结构,而边车架构最初是由服务网格普及的,所以许多人对 Dapr 的第一反应是怀疑这是否是另一个服务网格的竞争对手。另一方面,随着 Dapr 的发展,它开始提供更多与网络有关的功能,但仍然是应用方面的问题,如双向 TLS 认证。

Dapr 和服务网格的主要区别在于它们在不同的层面上工作。服务网格管理服务之间的网络流量,所以它们在基础设施层面上工作。另一方面,Dapr 为应用程序提供通用的构建模块,如状态管理,所以 Dapr 在应用层面工作。“网络网格” 可能是服务网格的一个更好的名字,然后我们可以把 Dapr 本身称为服务网格。然而,由于 “服务网格” 这个名字已经被使用了,我们有时将 Dapr 称为 “应用网格”,以区别于服务网格技术。

因为 Dapr 在不同的层面上工作,解决不同的问题,所以 Dapr 边框可以与现有的服务网格框架无缝地工作。如果你把它们配置在一起工作,你的应用流量将首先通过 Dapr 边车,然后是服务网格边车,它将在通信的另一端以相反的顺序通过堆栈。是的,在这种情况下有多个跳转,但这是为任何类型的抽象所付出的共同代价。

:::tips 在写这篇文章的时候,Dapr 团队正在编写一份文件,提供 Dapr和服务网格之间更详细的比较。请查阅在线文档以了解更多细节。 :::

在本章的最后,我们将简单讨论一下在边缘侧上使用 Dapr,我们将在 第 7 章 中更多地讨论这个问题。

边缘侧上的 Dapr

从第一天起,Dapr 就被设计成轻量级的,所以它在边缘侧工作得很好。我们一直小心翼翼地不在 Kubernetes 或 Docker 容器上添加任何硬性依赖,以便 Dapr 可以在 Kubernetes 之外的无容器环境中运行。Dapr 是为主要的 CPU 架构交叉编译的,包括 x86_64、ARM64 和 ARMv7,而且人们已经成功地将 Dapr 编译为 WebAssembly 字节码。因此,至少我们可以声称 Dapr 是边缘友好的。

而 Dapr 在有能力的边缘设备(如边缘服务器和 Raspberry Pi)上当然效果很好。

然而,问题是 Dapr 是否适用于容量极其有限且有严格功耗限制的低功耗设备。在撰写本文时,我们认为一个公平的答案是 “不太可能”。Dapr 的边车架构假设应用程序是一个网络服务器,而 Dapr 本身也是作为一个网络服务器运行的。即使我们可以忽略内存和 CPU 的限制,处理两个 Web 服务器的功耗本身就可能是一个挑战。

一个可能的解决方案是将 Dapr 变成一个可以加载到同一工作负载进程中的库。在这种情况下,应用程序利用 Dapr 通过本地、进程内调用提供的功能(如消息和状态管理)。如果应用程序不期望有入站调用,它可以被实现为一个常规的程序,而不是一个网络服务器。另一个可能的解决方案是让 Dapr 边车运行在一个更有能力的设备上,如现场网关。然而,这种设计打破了应用程序和挎包之间预先设定的安全边界,你将不得不考虑如何通过同一个网关支持多个设备。

下一章将介绍一些关于如何进一步扩展 Dapr 以支持物联网场景中的低功耗设备的想法。

总结

Dapr 被设计用来支持云原生应用。它通过边车为分布式应用带来通用功能。Dapr 也被设计用于事件驱动的应用程序。它允许应用程序代码响应来自各种事件源的事件,并通过连接器将事件发送到其他系统。许多分布式系统、面向服务的设计和基于消息的集成模式与 Dapr 自然配合。

你可以使用 Dapr 来迁移遗留系统,或开发新的云原生应用。它独特的、非侵入性的特性使你可以在任何你认为合适的程度上利用 Dapr。这意味着你可以将 Dapr 与其他技术(如服务网格)一起使用,而不会产生冲突。

由于我们伟大社区的贡献,Dapr 正在与许多不同的平台、框架和工具链整合,如 ASP.NET Core 和 Visual Studio Code。我们期待着在未来看到社区的更多贡献。在本书的最后一章,我们将窥探一下 Dapr 可能成为什么。