微服务最难的部分是什么?你的数据

我们探讨在创建和开发微服务时处理数据的挑战。
589 位读者喜欢这篇文章。
putting the pieces together

Opensource.com

在本文中,我将探讨在创建和开发 微服务 时可能最困难的问题:你的数据。使用 Spring Boot/Dropwizard/Docker 并不意味着你正在做微服务。认真审视你的领域和数据将有助于你实现微服务。(要了解更多背景信息,请阅读我的微服务实现系列博客:为什么微服务应该是事件驱动的使你的微服务更具弹性的 3 件事,以及 将 Java EE 单体架构分解为微服务:首选垂直而非分层。)

在我们尝试微服务架构的原因中,最主要的原因是让你的团队能够以不同的速度在系统的不同部分工作,并最大限度地减少团队之间的影响。我们希望团队是自主的,能够决定如何最好地实施和运营他们的服务,并能够根据业务需求快速进行更改。如果我们组织团队这样做,那么我们在系统架构中的反映将开始演变成类似于微服务的东西。

为了获得这种自主性,我们需要摆脱我们的依赖关系,但这说起来容易做起来难。我见过人们部分地、简单地将这个想法称为“每个微服务应该拥有并控制自己的数据库,并且没有两个服务应该共享一个数据库”。这个想法是合理的:不要跨服务共享单个数据库,因为那样你会遇到冲突,例如竞争的读/写模式、数据模型冲突和协调挑战。但是单个数据库确实为我们提供了许多安全性和便利性,包括 ACID 事务、查找的单个位置、它被很好地理解(某种程度上?)、一个管理位置等等。

在构建微服务时,我们如何调和这些安全性和将我们的数据库拆分为多个较小的数据库?

首先,对于构建微服务的企业,我们需要明确以下几点

  • 什么是领域?什么是现实?
  • 事务边界在哪里?
  • 微服务应该如何在边界之间通信?
  • 如果我们只是把数据库翻转过来会怎么样?

什么是领域

这似乎在很多地方都被忽略了,但这对于互联网公司如何实践微服务以及传统企业如何(或可能因为忽略这一点而失败)实施微服务之间存在巨大差异。

在我们构建微服务并推理它使用的数据(产生/消费等)之前,我们必须对该数据代表什么有相当好的理解。例如,在我们能够将关于我们的 Ticket Monster 及其 迁移到微服务 的“预订”信息存储到数据库中之前,我们需要了解什么是预订。与你的领域一样,你可能需要了解什么是帐户,或员工,或索赔等。

要做到这一点,我们需要深入研究现实中是什么?例如:什么是书?思考一下,因为这是一个相当简单的例子。我们如何在数据模型中表达这一点?

书是带有页面的东西吗?报纸是书吗?(它有页面。)也许书有硬封面?或者不是每天发布/出版的东西?如果我写了一本书(我确实写了一本——Java 开发人员的微服务),出版商可能会为我创建一个条目,其中有一行代表我的书。但是书店可能有五本我的书。每本都是书吗?还是复本?我们该如何表示这一点?如果一本书太长,必须分成几卷怎么办?每卷都是一本书吗?还是所有卷加起来才是一本书?如果将许多小的作品组合在一起怎么办?组合是书吗?还是每本单独的作品?基本上,我可以出版一本书,在书店里有很多本它的复本,每本都有多卷。

那么,什么是书?

现实情况是,没有现实。对于关于现实的什么是书没有客观的定义,所以要回答像这样的任何问题,我们必须知道:谁在问问题,以及上下文是什么?

我们人类可以快速地——甚至在无意识的情况下——解决这种理解的歧义,因为我们在头脑中、在环境中以及在问题中都有上下文。但是计算机没有。当我们构建软件和建模数据时,我们需要使这种上下文显式化。使用书来举例说明这一点是简单的。你的领域(一个企业)及其帐户、客户、预订、索赔等等将更加复杂,并且更加冲突/模糊。我们需要边界。

我们在哪里划定边界?领域驱动设计社区 的工作帮助我们处理领域中的这种复杂性。我们在 有界上下文 周围绘制 实体值对象聚合,它们建模我们的领域。换句话说:我们构建和完善一个表示我们领域的模型,并且该模型包含在一个定义我们上下文的边界内。这是显式的。这些边界最终成为我们的微服务,或者边界内的组件最终成为微服务,或者两者都是。在任何情况下,微服务都与边界有关,DDD 也是如此。

f1_posta_500.png

我们的数据模型——我们希望如何在物理数据存储中表示概念(注意这里的显式差异)——是由我们的领域模型驱动的,而不是相反。当我们有这个边界时,我们知道(并且可以断言)我们的模型中什么是正确的,什么是错误的。这些边界也暗示了一定程度的自主性。有界上下文 A的理解可能与有界上下文 B 不同。例如,可能有界上下文 A 是一个搜索服务,它搜索标题,其中单个标题是一本;可能有界上下文 B 是一个结帐服务,它根据你购买的书籍数量(标题 + 复本)处理交易,等等。

你可能会停下来并说,“等一下。Netflix 没有说任何关于领域驱动设计的事情,Twitter 和 LinkedIn 也没有。我为什么要听关于 DDD 的这些?”

这就是原因

“人们试图复制 Netflix,但他们只能复制他们所看到的。他们复制结果,而不是过程。” Adrian Cockcroft,前 Netflix 首席云架构师

微服务之旅就是这样:一次旅程。对于每家公司来说都会有所不同。没有硬性规定,只有权衡。复制一家公司有效的方法,因为它在这个实例中似乎有效,是试图跳过过程/旅程,并且不会奏效。你的企业不是 Netflix。事实上,我认为,无论 Netflix 的领域有多复杂,它都没有你的传统企业那么复杂。搜索和展示电影、发布推文、更新 LinkedIn 个人资料等等都比你的保险索赔处理系统简单得多。这些互联网公司转向微服务是因为上市速度和巨大的容量/规模。(向 Twitter 发布推文很简单。为 5 亿用户发布推文和显示推文流非常复杂。)

企业必须应对领域和规模的复杂性。因此,接受这是一个平衡领域、规模和 组织变革旅程。对于每个组织来说,这段旅程都会有所不同。

事务边界在哪里?

回到故事。我们需要像领域驱动设计这样的东西来帮助我们理解我们将用来实现系统的模型,并在上下文中围绕这些模型绘制边界。我们接受客户、帐户、预订等等对于不同的有界上下文可能意味着不同的事物。我们最终可能会在我们的架构周围分布这些相关的概念,但是当更改发生时,我们需要一种方法来协调这些不同模型之间的更改。我们需要考虑到这一点,但首先,我们必须识别我们的事务边界。

不幸的是,开发人员似乎仍然以错误的方式构建分布式系统:我们通过单个、关系型、ACID 数据库的视角来看待。我们还忽略了异步、不可靠网络的危险。为此,我们做了诸如编写花哨的框架,使我们不必了解任何关于网络的东西(包括 RPC 框架,也忽略网络的数据库抽象),并尝试使用点对点同步调用(REST、SOAP、其他类似 CORBA 的对象序列化 RPC 库等)来实现一切。我们构建系统时没有考虑到 权威性 vs. 自主性,最终尝试使用跨多个独立服务的两阶段提交等方式来解决分布式数据问题。或者我们完全忽略这些问题。这种心态导致构建脆弱的系统,这些系统无法扩展,并且无论你称之为 SOA、微服务、微型服务还是其他什么,都无关紧要。

我所说的事务边界是什么意思?我的意思是关于业务不变量你需要的最小原子性单元。无论你是否使用数据库的 ACID 属性来实现原子性,还是两阶段提交,都无关紧要。重点是使这些事务边界尽可能小(理想情况下是单个对象上的单个事务),以便我们可以扩展。(Vernon Vaughn 有一个 描述使用 DDD 聚合的这种方法的系列文章。)当我们使用 DDD 术语构建我们的领域模型时,我们识别 实体值对象聚合。在这种上下文中,聚合是封装其他实体/值对象并负责执行不变量的对象;在一个有界上下文中可以有多个聚合。

例如,假设我们有以下用例

  • 允许客户搜索航班,
  • 允许客户在特定航班上选择座位,
  • 并允许客户预订航班。

我们可能在这里有三个有界上下文:搜索、预订和票务。(我们还会有其他上下文,例如支付、忠诚度、候补、升舱等等,但让我们关注三个)。搜索 负责显示特定航线和给定时间范围(天数、时间范围等)的行程的航班。预订 将负责启动包含客户信息(姓名、地址、常旅客号等)、座位偏好和付款信息的预订流程。票务 将负责与航空公司结算预订并出票。在每个有界上下文中,我们都希望识别我们可以强制执行约束/不变量的事务边界。我们将不考虑跨有界上下文的原子事务,我将在下一节中讨论。

考虑到我们想要小的事务边界(预订航班的简化版本),我们应该如何建模?也许航班聚合封装了诸如时间、日期、航线等值,以及诸如客户、飞机和预订等实体?这似乎有道理——航班有飞机、座位、客户和预订。航班聚合负责跟踪飞机、座位等,以便创建预订。从数据库内部的数据模型角度(具有约束和外键等的优秀关系模型)或源代码中的优秀对象模型(继承/组合)来看,这可能是有意义的,但让我们看看会发生什么。

f2_posta_500.png

真的需要在所有预订、飞机、航班等之间存在不变量,仅仅是为了创建预订吗?也就是说,如果我们将一架新飞机添加到航班聚合中,我们真的应该将客户和预订包含在该事务中吗?可能不应该。我们这里有一个考虑到组合和数据模型便利性而构建的聚合;但是,事务边界太大了。如果我们对航班、座位、预订等进行大量更改,我们将有很多事务冲突(无论使用乐观锁定还是悲观锁定都无关紧要)。这显然无法扩展——更不用说仅仅因为航班时刻表正在更改而导致订单一直失败是一种糟糕的客户体验。

如果我们将事务边界稍微缩小一点会怎么样。

也许预订、座位可用性和航班是它们自己独立的聚合。预订封装了客户信息、偏好以及可能的付款信息。座位可用性聚合封装了飞机和飞机配置。航班聚合由时刻表、航线等组成,但我们可以在不影响航班时刻表和飞机/座位可用性事务的情况下继续创建预订。从领域的角度来看,我们希望能够做到这一点。我们不需要飞机/航班/预订之间 100% 严格的一致性,但我们确实希望正确记录作为管理员的航班时刻表更改、作为供应商的飞机配置以及来自客户的预订。那么我们如何实现诸如在航班上“选择特定座位”之类的功能呢?

在预订过程中,我们可能会调用座位可用性聚合并要求它预订飞机上的座位。此座位预订将作为单个事务实现——例如,(保留座位 23A)并返回预订 ID。我们可以将此预订 ID 与预订关联起来,并在知道座位在某个时间点“已预订”的情况下提交预订。这些——预订座位和接受预订——都是单独的事务,可以独立进行,无需任何类型的两阶段提交或两阶段锁定。

请注意,这里使用预订是业务需求。我们在这里不做座位分配;我们只是预订座位。这个要求可能需要通过模型的迭代来确定,因为用例的语言最初可能只是说“允许客户选择座位”。开发人员可能会尝试推断该要求意味着“从剩余座位中选择,将其分配给客户,从库存中删除,并且销售的票数不超过座位数”。这将是额外的、不必要的不变量,会给我们的事务模型增加额外的负担,而业务实际上并不将其视为不变量。业务当然可以接受没有完整座位分配甚至超额预售航班的预订。

f3_posta_500.png

这是一个允许真正的领域指导你走向更小、更简化但完全原子化的涉及的各个聚合的事务边界的示例。然而,故事不能到此结束,因为我们现在必须纠正所有这些需要在某个时候汇集在一起的单独事务这一事实。涉及数据的不同部分(即,我创建了预订和座位预订,但是这些与获得登机牌/机票等相关的未结算事务)

微服务应该如何在边界之间通信?

我们希望保持真正业务不变量的完整性。使用 DDD,我们可能会选择将这些不变量建模为聚合,并使用单个事务强制执行它们以实现聚合。在某些情况下,我们可能会在单个事务中更新多聚合(跨单个数据库或多个数据库),但这些场景将是例外。我们仍然需要维护聚合之间(最终在有界上下文之间)的某种形式的一致性,那么我们应该如何做到这一点?

我们应该理解的一件事:分布式系统很挑剔。关于分布式系统中任何事情,我们可以做出的保证很少(如果有的话),在有界时间内——事情会失败,会不确定地变慢或看起来已经失败,系统具有不同步的时间边界等等——那么为什么要与之抗争呢?如果我们接受这一点并将其融入到我们跨领域的一致性模型中会怎么样?如果我们说,在必要的事务边界之间,我们可以容忍数据的其他部分和领域在稍后的时间点进行协调并使其保持一致会怎么样?

对于微服务,我们重视自主性。我们重视能够独立于其他系统(在可用性、协议、格式等方面)进行更改。这种时间和任何关于服务之间任何事物保证的解耦使我们能够实现这种自主性(这并非计算机系统或任何系统所独有)。所以我说,在事务边界之间和有界上下文之间,使用事件来传达一致性。事件是不变结构,用于捕获应该广播给对等方的有趣时间点。对等方将侦听他们感兴趣的事件,并根据该数据做出决策、存储该数据、存储该数据的某些派生数据、根据使用该数据做出的决策更新自己的数据等等。

继续我开始的航班预订示例:当通过 ACID 风格的事务存储预订时,我们最终如何出票?这就是前面提到的票务有界上下文的用武之地。预订有界上下文将发布一个像 NewBookingCreated 这样的事件,票务有界上下文将消费该事件并继续与后端(可能是遗留)票务系统交互。这需要某种集成和数据转换,Apache Camel 在这方面非常出色。

我们如何以原子方式对我们的数据库进行写入发布到队列/消息传递设备?如果我们的事件之间有排序要求/因果关系要求怎么办?以及每个服务一个数据库呢?

理想情况下,我们的聚合将直接使用命令和 领域事件(作为第一类公民——也就是说,任何操作都实现为命令,任何响应都实现为对事件的反应),我们可以更清晰地映射我们在有界上下文内部使用的事件与我们在上下文之间使用的事件。我们可以只将事件(例如,NewBookingCreated)发布到消息队列,然后让侦听器从队列中消费它,并将其幂等地插入到数据库中,而无需使用 XA/2PC 事务,而不是我们自己插入到数据库中。我们可以将事件插入到专用的 事件存储 中,该事件存储充当数据库和消息传递发布-订阅主题(这可能是首选路由)。或者我们可以继续使用 ACID 数据库并将更改流式传输到持久的、复制的日志,例如 Apache Kafka,使用像 Debezium 这样的东西,并使用某种事件处理器/蒸汽处理器来推断事件。无论哪种方式,我们都希望使用不可变的时间点事件在边界之间进行通信。

f4_posta_500.png

这带来了巨大的优势

  • 我们避免了跨边界的昂贵且可能不可能的事务模型。
  • 我们可以对我们的系统进行更改,而不会妨碍系统其他部分的进度(时间和可用性)。
  • 我们可以决定我们希望以多快的速度或多慢的速度看到外部世界的其余部分并最终保持一致。
  • 我们可以使用适合我们服务的技术,以我们喜欢的任何方式将数据存储在我们自己的数据库中。
  • 我们可以随意更改我们的架构/数据库。
  • 我们变得更具可扩展性、容错性和灵活性。
  • 你必须更加关注 CAP 定理以及你选择用来实现你的存储/队列的技术。

值得注意的是,这带来了缺点

  • 它更复杂。
  • 调试很困难。
  • 因为我们在看到事件时会有延迟,所以我们无法对其他系统知道什么做出任何假设(无论如何我们都无法做到这一点,但在这种模型中这更加明显)。
  • 操作化更困难。
  • 你必须更加关注 CAP 定理以及你选择用来实现你的存储/队列的技术。

我将“关注 CAP 等”列在两列中,因为虽然它给我们带来了一点负担,但无论如何这样做都是势在必行的。此外,关注分布式数据系统中不同形式的数据一致性和并发性至关重要。依赖“我们的数据库在 ACID 中”不再可以接受(尤其是当该 ACID 数据库很可能默认为弱一致性时)。

从这种方法中出现的另一个有趣的概念是实现一种称为命令查询职责分离的模式的能力,我们在其中将我们的读取模型和写入模型分离到单独的服务中。请记住,我们感叹互联网公司没有复杂的领域模型。这在他们的写入模型很简单这一事实中很明显(例如,将推文插入到分布式日志中)。然而,他们的读取模型由于其规模而变得异常复杂。CQRS 有助于分离这些关注点。另一方面,在企业中,写入模型可能非常复杂,而读取模型可能只是简单的平面选择查询和平面 DTO 对象。CQRS 是一种强大的关注点分离模式,一旦你有了适当的边界以及在聚合之间和有界上下文之间传播数据更改的好方法,就可以对其进行评估。

如果一个服务只有一个数据库,并且不与任何其他服务共享呢?在这种情况下,我们可能有侦听器订阅事件流,并且可能会将数据插入到主聚合最终可能使用的共享数据库中。这个“共享数据库”完全没问题。请记住,没有规则,只有权衡。在这种情况下,我们可能有多个服务与同一个数据库协同工作,只要我们(我们的团队)拥有所有流程,我们就不会否定我们自主性的任何优势。因此,当你听到有人说“微服务应该有自己的数据库,并且不与其他人共享它”时,你可以回应说“嗯,有点吧”。

如果我们只是把数据库翻转过来会怎么样?

如果我们把上一节中的概念推向逻辑上的极端会怎么样?如果我们只是说我们将对所有事情使用事件/流并且也永久保留这些事件会怎么样?如果我们说数据库/缓存/索引实际上只是过去发生的持久日志/事件流的物化视图,而当前状态是所有这些事件的左折叠会怎么样?

这种方法带来了 更多 好处,你可以添加到通过事件进行通信的好处中(如上所列)

  • 现在你可以将你的数据库视为记录的“当前状态”,而不是真正的记录。
  • 你可以引入新的应用程序并重新读取过去的事件,并根据 将会发生什么 来检查它们的行为。
  • 你可以免费实现完美的审计日志记录。
  • 你可以引入你的应用程序的新版本,并通过重放事件对其执行详尽的测试。
  • 你可以通过将事件重放到新数据库中,更轻松地推理数据库版本控制/升级/架构更改。
  • 你可以迁移到全新的数据库技术(例如,你可能会发现你已经超越了你的关系型数据库,并且想要切换到专门的数据库/索引)。

有关此主题的更多信息,请阅读 Martin Kleppmann 的 “使用 Apache Samza 将数据库由内而外翻转”

f5_posta_500.png

当你在 aa.com、delta.com 或 united.com 上预订航班时,你会看到其中一些概念在起作用。当你选择座位时,你实际上并没有被分配座位——你只是预订了座位。当你预订航班时,你实际上并没有机票——你稍后会收到一封电子邮件告诉你你已确认/出票。你是否曾经遇到过预订更改并被分配了航班的不同座位?或者去过登机口并听到他们要求志愿者放弃座位,因为航班超额预售?这些都是事务边界、最终一致性、补偿事务,甚至工作中道歉的例子。

结论

数据、数据集成、数据边界、企业使用模式、分布式系统理论、时序等等都是微服务的难点(因为微服务实际上只是分布式系统)。我看到太多关于技术的困惑(“如果我使用 Spring Boot 我就是在做微服务我需要在云中解决服务发现、负载均衡,然后才能做微服务”“我必须为每个微服务配备一个数据库”)以及关于微服务的无用规则。

别担心。一旦大型供应商向你出售了所有花哨的产品套件,你仍然需要做我在本文中概述的难点。

标签
User profile image.
Red Hat 首席中间件架构师、开源爱好者、Apache 的提交者、云、集成、Kubernetes、Docker、OpenShift、Fabric8、#blogger

3 条评论

天哪,我刚刚读了微服务时代最好的博客文章之一!

关于微服务的优秀博客文章。非常具有解释性。附有插图。一针见血!
(哈哈。“别担心。一旦大型供应商向你出售了所有花哨的产品套件,你仍然需要做我在本文中概述的难点。”)

很棒的内容,Christian!我特别赞赏关于领域定义的章节,这往往被忽视。

RAPID-ML 是一种领域驱动的 API 建模语言

http://rapid-api.org/rapid-ml

这就是我们在大型组织和复杂生态系统中解决跨 API 的标准化数据表示需求的方式。

Ted Epstein,CEO
RepreZen
http://RepreZen.com

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.