微服务设计模式之-独享数据库(Database per Microservice)


原文:https://dev.to/eugene-zimin/database-per-service-as-a-design-pattern-44gi

在现代软件开发领域,微服务架构已成为构建可扩展且可维护应用程序的流行方法。此架构将单片应用程序分解为较小的独立服务,这些服务可以独立开发、部署和扩展。

然而,如果有多种方法可以分解服务功能,那么如何拆分数据以及是否有必要拆分数据仍然是一个问题。这就引出了“每个服务一个数据库”模式的核心及其在现代架构中的作用。

历史回顾

软件架构和数据管理的发展历程是复杂性不断提高和分布性不断增强的过程之一。要了解我们目前在“每个服务一个数据库”等模式方面所处的位置,追溯引领我们走到这一步的路径将大有裨益。这一历程始于传统的单片架构,其中应用程序被构建为单个、紧密集成的单元,共享一个公共数据库。

传统单体架构

在深入研究这种设计模式之前,了解传统方法以及问题出现的原因非常重要。

在单体应用时代,不存在如何访问数据的问题,因为没有定义多个应用程序并发访问数据。单体架构具有单一共享数据库,提供了一种简单的数据管理方法。应用程序的所有组件都可以直接访问整个数据库,数据一致性主要通过数据库事务和锁定机制进行管理。

在这里插入图片描述
整体式应用程序访问图 1. 应用程序 - 数据库通信模型

然而,这种简单性是有代价的。随着应用程序规模和复杂性的增长,单片方法的局限性变得越来越明显。应用程序组件和数据库之间的紧密耦合使得在不影响整个系统的情况下发展数据模型变得困难。架构更改通常需要在应用程序的多个部分之间进行协调更新,从而导致复杂的部署过程和潜在的停机时间。

此外,缺乏数据封装意味着应用程序的任何部分都有可能访问和修改任何数据,从而增加了意外副作用和数据损坏的风险。随着团队规模的扩大以及不同开发人员团队在同一块单体应用中开发不同功能,这个问题变得尤为严重。

面向服务架构以及随后出现的微服务的出现,使这些数据访问和管理挑战成为焦点。突然之间,架构师和开发人员面临着在单片世界中不存在的问题:我们如何确保每项服务都能访问其所需的数据,而不会产生紧密耦合?当事务跨越多个服务时,我们如何保持数据一致性?我们如何针对具有不同性能要求的服务独立扩展数据访问模式?

N 层架构

回答这个问题的第一次尝试是 N 层架构风格。这种方法试图在应用程序内引入不同关注点之间的一定程度的分离,或者通过将代码组织到不同的层(通常是表示层、业务逻辑层和数据访问层)中,甚至更进一步 - 将该功能提取到单独的应用程序中。每一层都应该有特定的职责,并通过定义明确的接口与相邻层进行通信。
在这里插入图片描述达拉斯图 2. 具有外部 DAL 的分层系统示例

在数据管理的背景下,N 层架构通常涉及创建专用的数据访问层 (DAL)。该层封装了与数据库的所有交互,提供了一组方法或服务,业务逻辑层可以使用这些方法或服务来检索或操作数据。其理念是抽象出数据存储和检索的复杂性,使更改底层数据库技术或结构变得更加容易,而不会影响应用程序的其余部分。

虽然 N 层架构带来了一些好处,例如改进了代码组织和数据访问组件的可重用性,但它并没有完全解决分布式系统的挑战。数据访问层虽然是独立的,但通常仍与单个共享数据库紧密耦合。这意味着虽然各个服务或模块可能有自己的业务逻辑,但它们仍然依赖于中央数据存储,这可能会成为瓶颈和单点故障。

此外,N 层方法本身并不能解决数据所有权和自主权问题。不同的服务或模块可能需要以不同的速度发展其数据模型,或者具有不同的性能和扩展要求。共享数据访问层和数据库使得满足这些不同的需求变得困难,因为需要复杂的协调和潜在的中断。

所有这些问题促使系统和软件架构师进入下一步——创建一种称为“共享数据集” 的设计模式。

共享数据库
随着系统复杂性不断增加,对真正解耦的需求也变得越来越迫切,架构师开始将目光从分层架构转向更加分布式的模式。这促使他们考虑采用“每个服务一个数据库”模式等方法,该模式将分离的概念更进一步,不仅为每个服务提供自己的数据访问代码,还为每个服务提供自己的专用数据库。

从 N 层架构过渡到“共享数据库”等模式,反映了分布式系统中数据管理思维的根本转变。现代方法不再将数据访问视为需要抽象出来的共同关注点,而是通常将数据视为每个服务域的一个组成部分,尽可能自主地拥有和管理。每个实例具有 DB 的微服务应用程序图在这里插入图片描述
3. 共享数据库设计模式的实际应用

然而,这种演变并非没有挑战。虽然 N 层架构努力解决代码组织和可重用性问题,但共享数据库模式必须应对数据一致性、重复和服务间通信问题。数据存储的解耦提供了更大的灵活性和弹性,但也需要仔细考虑如何在整个系统中保持一致的数据视图。

随着我们继续探索“共享数据库”模式,我们将看到它如何借鉴早期架构风格(如 N 层)的经验教训,同时引入更适合现代分布式应用程序现实的新范例。目标始终不变:创建可扩展、可维护且响应变化的系统。但实现这一目标的方法已经发生了变化,反映了我们构建的软件的复杂性和规模不断增加。

共享采用数据库模式的好处
我们需要明白,共享数据库模式并不是灵丹妙药。然而,即使它本身也存在一系列挑战,但它提供了一些引人注目的好处,使它成为微服务架构中越来越受欢迎的选择。这些优势源于解耦的基本原则,不仅在应用程序级别,而且在数据级别。

增强的可扩展性
采用微服务以及扩展共享数据库模式的主要驱动因素之一是可扩展性。考虑两个不同的例子,第一个是具有共享数据库的传统单片应用程序。整体式扩展

在这里插入图片描述
图 4. 在整体式应用程序内加载地图

采用这种方法,扩展通常意味着扩展整个系统,即使只有一个组件负载过重。这可能导致资源利用率低下和成本增加。让我们来算一下。

假设我们的应用程序在具有 4 个 CPU 和 16GB RAM 的虚拟机上运行。这是一个简单的 Web 应用程序,提供根据不同用户的订阅存储和读取消息的功能,就像在 Twitter 中一样。应用程序构建为单体式应用程序,能够消耗不超过 25% 的虚拟机资源,从而为扩展和运营开销留出一些空间。

现在,为了简单起见,我们假设每个组件消耗大约 300Mb 的 RAM,而大约 100Mb 的 RAM 是整体的集成开销。在这种情况下,我们注意到我们的应用程序缺乏资源,我们想创建另一个实例。这是一个好主意吗?不。我们有选择吗?没有冗余扩展
在这里插入图片描述

图 5. 扩展整体式应用程序

创建整个应用程序的另一个实例,我们必须扩展其中的所有组件,即使是那些完全没有高负载的组件,因为我们无法区分它们。最终,这将导致我们拥有 2 个具有冗余功能的应用程序,消耗内存和 CPU,这会花费我们的金钱。
在这里插入图片描述

微服务扩展图 6. 扩展微服务应用程序

共享都有一个数据库,每个服务及其对应的数据库都可以根据其特定需求独立扩展。例如,处理创建和读取消息的服务可能需要扩展以处理大量请求,而用户注册可能根本不需要扩展。通过允许每个服务扩展自己的数据库,我们可以微调其基础架构,将资源分配到最需要的地方并优化性能,而无需不必要的开销。

更清晰的数据所有权

在大型系统中,有时可能不清楚应用程序的哪个部分“拥有”特定数据,从而导致在修改数据时出现混乱、不一致或意外的副作用。共享数据库模式建立了明确的数据所有权界限。

想象一下消息应用程序的场景。
在这里插入图片描述数据所有权图 7. 消息传递应用程序中的数据所有权

采用数据驱动设计方法,我们可以确定两个具有不同数据职责的主要服务:用户管理服务和消息服务。在检查消息服务的数据模型时,很明显每条消息都必须包含有关其作者(在用户管理服务中注册的用户)的信息。

一个简单的解决方案可能涉及在消息数据库中复制用户和角色数据,该数据库直接链接到消息服务。 原理似乎很简单:每次创建和保存消息时,我们都需要对用户进行身份验证并验证他们对消息生成和存储的权限。

但是,在消息数据库中复制用户和角色信息会导致和User表Roles同时存在于两个单独的数据库中。如果我们将用户管理数据库视为用户相关数据的权威来源,我们就会引入数据同步挑战——确保信息在两个位置保持一致。这种情况实际上违反了 SOLID 模式中的单一责任原则。消息数据库现在不仅在消息数据更改时需要更新,而且在用户信息被修改时也需要更新,从而混淆了其职责。

这样的安排使数据所有权和管理变得复杂。理想情况下,每个服务都应对其特定领域的数据拥有明确的权限,并有明确的界限,以促进可维护性并减少相互依赖。在消息服务中复制用户数据会模糊这些界限,造成数据不一致的可能性,并增加系统范围更新的复杂性。

更稳健的方法将尊重服务之间的自然边界,允许用户管理服务保留与用户相关的数据的唯一所有权。消息服务不会复制这些信息,而是根据需要引用这些信息,仅存储链接或指针,同时依靠用户管理服务获取权威数据。这种策略坚持服务自主性和数据完整性的原则,这是有效微服务架构的基石。
在这里插入图片描述

正确的数据所有权图 8. 正确的数据所有权

现在,每个服务都成为其数据库的唯一写入者,也是其数据域的权威来源。这种清晰度降低了数据管理的复杂性,有助于维护数据完整性,并使推断系统行为变得更加容易。虽然服务可能仍需要共享数据,但这通常是通过定义明确的 API 而不是直接访问数据库或冗余数据来完成的,从而强化了封装和松散耦合的原则。

改进故障隔离
在具有共享数据库的系统中,中断、性能下降或数据损坏等问题可能会产生广泛影响,甚至可能导致整个应用程序崩溃。共享数据库模式提供了更高程度的故障隔离。

如果某项服务的数据库出现问题,影响主要局限于该服务。系统的其他部分可以继续运行,从而提高整体弹性。这种隔离还简化了故障排除和恢复,因为团队可以专注于解决特定服务及其数据库中的问题,而无需协调整个系统。

让我们看一下这个例子。我们将使用相同的消息传递应用程序,现在考虑最坏的情况:应用程序依赖于共享数据库,而该数据库突然不可用。
在这里插入图片描述
图 9. 数据库/DAL 故障 单点故障

这次故障的后果是立竿见影且影响深远的。由于数据库无法访问,整个服务的核心功能实际上陷入停滞。系统内的任何组件都无法检索或操作数据,从最终用户的角度来看,应用程序毫无用处。

这次中断不仅仅是带来不便,它代表着一个单点故障,可能导致整个系统瘫痪。用户身份验证可能会因无法访问帐户数据而失败。消息传递功能是应用程序的核心,无法显示现有对话或允许发送新消息。甚至用户设置或搜索功能等次要功能也受到影响。

连锁反应不仅限于数据检索。写入操作(例如存储新消息、更新用户配置文件或记录系统活动)也会陷入停顿。这不仅会中断当前的用户交互,而且如果系统没有设计强大的排队和重试机制,还会导致数据丢失。

此外,数据库的共享特性意味着,即使是数据需求最少的服务或组件也会受到影响。状态监控工具通常可以使用缓存数据运行,但如果需要定期检查数据库,它可能会失败。管理界面通常对于诊断和解决问题至关重要,但也可能因此无法运行。

这种场景凸显了使用共享数据库的单片架构的一个根本弱点:缺乏隔离。当所有服务都依赖于同一个数据存储时,当该数据存储发生故障时,它们将面临相同的命运。没有优雅的降级,系统的某些部分无法继续运行,而其他部分则受到影响。
在这里插入图片描述
单点故障图 10. 分布式系统中的数据库故障

现在让我们重新想象同样的场景,但在一个采用“共享数据库”模式的系统环境中。当数据库发生故障时,其影响主要局限于与其直接相关的服务,其他服务不受影响,可以正常运行。

在此图中,我们看到链接到评论服务的数据库已脱机。直接后果是评论服务失去了检索或存储数据的能力。用户在尝试发布新评论或查看现有评论时可能会遇到错误消息。然而——这是关键的区别——连锁反应就此停止了。

用户管理服务拥有自己的专用数据库,可以继续对用户进行身份验证并畅通无阻地管理帐户信息。消息服务仍然方便用户之间直接消息的交换。即使是假设的分析服务也可以继续跟踪用户参与度指标,也许会对评论相关事件暂时视而不见。

这种弹性源于“共享数据库”模式固有的解耦。每个服务不仅有自己的代码库和部署管道,还有自己的数据持久层。这种自主性意味着系统某个部分的故障不会自动导致整个系统的中断。相反,应用程序会优雅地降级,即使某些功能暂时不可用,也能保持核心功能。

当然,在设计良好的微服务架构中,服务很少完全孤立存在。它们通常相互依赖以实现某些功能。在我们的示例中,消息传递或用户管理服务可能需要相互交互 - 以执行用户身份验证和授权,因此任何核心服务的故障可能仍然对整体功能至关重要,但其余服务必须分析错误响应并与系统管理员一起通知客户有关故障的信息。此外,他们还可以缩小问题故障点,从而使调试更容易。

服务应构建为妥善处理依赖项的错误或超时。如果消息传递服务尝试获取对话的最近评论并收到来自评论服务的错误响应,它不应该崩溃。相反,它可能会向用户显示一条消息,表示评论暂时不可用,同时仍显示对话历史记录的其余部分。同样,用户管理服务可以将审核操作排队,以便在评论服务恢复在线后进行处理,而不是阻止用户管理任务。

更大的发展自主权

大型软件项目的挑战之一是协调变更,尤其是数据库架构变更。在共享数据库中,应用程序某一部分的架构变更可能需要跨多个组件进行更新和测试,这会减慢开发周期,因为它会直接影响整个组件。

通过为每项服务提供自己的数据库,开发团队获得了更大的自主权。他们可以改进服务的数据模型、优化查询,甚至更改数据库技术,而不会影响其他服务。这种自主权可以显著加快开发和部署周期,使团队能够更加独立地工作并更快地交付价值。
在这里插入图片描述
仅限 API 访问图 11. 仅通过 API 访问数据

此图说明了“共享数据库”模式的基本原则:通过定义良好的 API 进行数据封装。在这里,我们看到对消息数据库模型的任何修改都只会对消息服务产生直接影响。这些更改对于更广泛的系统生态系统中的其他组件而言是不可见的。这种封装是通过遵守严格的规则来实现的:所有数据访问都必须通过服务的 API 进行。

当我们考虑模式演进的影响时,这种方法的强大之处就显而易见了。假设负责消息传递服务的团队决定对其数据模型进行非规范化以提高查询性能,例如将经常访问的用户信息直接嵌入消息记录中。在共享数据库场景中,这样的更改可能会造成严重破坏,破坏跨多个服务的查询和数据访问模式。但是,使用共享数据库模式,更改是可以控制的。只要消息传递服务继续遵守其 API 契约(返回预期的数据结构和字段),其他服务就仍然不会意识到底层数据重组。

这一原则与 SOLID 的开放封闭原则完美契合,该原则规定软件实体应该对扩展开放,但对修改封闭。在我们的上下文中,消息服务的 API 代表“封闭”部分 - 其他服务可以依赖的稳定接口。当出现新需求或需要优化时,服务可以通过改进其内部数据模型或添加新的 API 端点来“扩展”其功能,而不会干扰现有消费者。

例如,如果需要支持更丰富的消息元数据,消息服务团队可能会向其数据库架构添加新字段,并通过新的 API 方法或现有方法上的可选参数公开这些附加信息。不需要这些元数据的服务将继续正常运行,而需要这些元数据的服务可以选择使用增强功能。

这种方法的好处不仅仅是便于更改。通过强制所有数据访问都通过版本化 API,我们引入了一个抽象层,可以消除服务内部数据表示与向外部呈现的数据视图之间的差异。这在增量迁移或支持旧版客户端时尤其有用。

设想这样一个场景:消息服务正在从将时间戳存储为 Unix 纪元转换为 ISO 8601 字符串。服务的 API 可以继续接受并返回 API v1 的纪元,同时在 v2 中提供新格式,而不是强迫所有消费者同时适应(这在大型系统中是一项艰巨的任务)。在内部,服务会处理转换,从而实现分阶段迁移,而不会遗漏系统的任何部分。

灵活的技术选择
这一优势直接源于两种基本模式:独立的数据所有权和封装。当数据隐藏在定义良好的 API 契约之后时,底层存储机制的细节将成为实现细节 — — 不透明,并且在很大程度上与该 API 的消费者无关。消息服务是否将其数据保存在 MySQL、PostgreSQL 中,还是使用 MongoDB 进入 NoSQL 领域,这对系统的其余部分重要吗?只要服务履行其 API 承诺,答案就是肯定的“不”。

这种抽象开辟了一个充满可能性的世界,让团队摆脱了共享单片数据库经常带来的“一刀切”束缚。事实上,并非所有数据都是平等的,单一数据库技术可以最佳地服务于复杂系统中每种类型的数据或访问模式这一概念越来越被认为是一种限制性的神话。

每共享数据库模式打破了这一限制,开创了多语言持久性的时代 — — 每个服务都可以选择一种与其独特数据特征和运营需求完全匹配的数据库技术。这不仅仅是为了多样化而做出选择;而是为了让团队能够做出细致入微的战略决策,这些决策可能会对性能、可扩展性甚至他们建模领域的基本方式产生重大影响。

这种技术灵活性的影响不仅限于查询性能或数据模型适配。不同的数据库技术带来不同的操作特性 — 扩展模型、备份和恢复程序、监控和管理工具。通过将这些与服务特定需求相结合,团队不仅可以优化运行时性能,还可以优化生产中管理数据的整个生命周期。

维护和更新更轻松

这种简化维护和演进的能力是“数据库/服务”模式中数据所有权分离和封装双重原则带来的另一大好处。在数据存储孤立且只能通过定义明确的服务接口访问的环境中,常规数据库维护甚至变革性变更都可以精确地进行协调,从而最大限度地减少系统范围内的中断。

考虑数据库维护的周期性必需性——软件更新以修补安全漏洞、备份程序以防止数据丢失或性能优化以跟上不断增长的负载。在具有共享数据库的单片架构中,这些任务中的每一个都会产生深远的影响。版本升级可能需要对所有应用程序组件进行广泛的回归测试。备份过程可能会对整个系统的性能造成影响。一个设计不当的索引可能会加速一个模块的查询,同时使另一个模块的写入操作陷入停顿。

共享数据库模式重新绘制了这些影响的边界。当每个服务都拥有自己的数据库时,维护窗口可以错开,以适应各个服务的特定需求和容忍度。客户支持团队可能会安排在周日凌晨时分(此时工单量最低)修补数据库,而电子商务平台则等到周二下午的售后高峰期。写入密集型日志服务的备份可以每小时运行一次,而相对静态的产品目录服务可能只需要每日快照。

这种精细度延伸到性能调优。DBA 可以随心所欲地进行优化,因为他们知道实验性索引策略或隔离级别的转变只会在单个服务范围内产生影响。任何潜在失误的影响范围都得到了控制,从而营造出一种环境,让团队可以更大胆地追求在共享数据库环境中可能被视为风险过大的优化。

但是,当我们从日常维护中抽身而出,考虑数据架构的重大变化时,这种模式的真正威力就会显现出来。在任何足够长寿的系统中,总有那么一段时间,增量调整不再足够——当数据模型需要全面重组时,或者当当前数据库技术的局限性成为增长或功能开发的不可逾越的障碍时。

传统上,此类变更一直是 CTO 的噩梦:“大爆炸”式迁移需要数月的准备,最终在一个高风险的切换周末结束,整个工程团队都处于高度戒备状态,业务利益相关者则屏息注视着状态仪表板。风险多种多样 — 数据损坏、性能下降,或者周一早上发现测试中忽略了关键的遗留功能,这令人恐惧。此类迁移的回滚计划通常归结为“祈祷我们不需要它”,因为一旦切换完成,逆转方向几乎是不可能的。

结论

随着我们对“共享数据库”模式的探索接近尾声,很明显,这种架构方法为微服务时代的数据管理提供了一个令人信服的愿景。通过倡导单独的数据所有权、封装和各个服务的自主性,该模式带来了一系列好处,这些好处波及系统开发和运营的各个方面。

我们已经看到这种解耦如何加速开发周期,让团队可以自由地发展他们的数据模型并优化性能,而不必担心附带损害。我们已经探索了它在选择适合工作的数据库时所提供的灵活性,避免使用千篇一律的解决方案,而采用针对每项服务独特需求量身定制的技术。我们还见证了它如何将维护和迁移从高难度操作转变为可管理的增量流程,从而降低风险并提高系统的变更能力。

这些优势描绘出一幅系统图景,它不仅更具可扩展性和弹性,而且适应性更强——能够更好地跟上业务需求、用户期望和技术能力的不断发展。在敏捷性至关重要的数字环境中,快速转型、大胆尝试和无所畏惧地接受变化的能力可以决定市场领导者和落后者之间的区别。

然而,与任何架构选择一样,共享数据库模式并非没有复杂性和权衡。赋予这种灵活性的解耦也带来了有关数据一致性、分布式事务和整体数据视图的新挑战。如果不仔细管理,赋予各个团队权力的自主权可能会导致技术格局分散,从而给运营资源造成压力。虽然服务接口提供了强大的抽象,但将它们设计得既灵活又稳定需要远见和纪律。

这些挑战并非无法克服,但它们确实需要深思熟虑的策略,有时还需要新颖的解决方案。在下一篇文章中,我们将揭开“每个服务数据库”模式的潜在缺点。我们将直面如何维护跨服务边界的数据完整性、如何处理跨多个数据存储的工作流以及如何平衡服务独立性和系统范围治理需求的问题。

我们还将研究此模式对组织和运营的影响。成功管理多语言持久性环境需要什么?我们如何确保我们的分散数据不会成为孤立的数据,无法通过分析和洞察来推动业务决策?这种模式如何影响(或受到)无服务器架构、边缘计算或 AI 驱动运营等新兴趋势的影响?

通过探索这些缺点和挑战,我们的目的不是削弱“共享数据库”模式的威力,而是让我们对它有一个完整的认识——确保我们在走向去中心化数据所有权的道路上保持清醒的头脑。毕竟,真正的架构智慧不在于教条地坚持任何模式,而在于深刻理解它的优点和局限性。

所以,这不是结束,而是中场休息。我们庆祝了这个世界的优雅和潜力,在这个世界中,每项服务都是自己数据域的主人。下次见面时,我们将撸起袖子,深入探讨实现这一愿景的严酷现实——需要克服的障碍、需要避开的陷阱以及需要权衡的利弊。

通往稳健、可演进系统的道路很少是一帆风顺的。但有了像每个服务数据库这样的模式,以及对其能力和需求的清晰认识,我们就能规划出一条穿越复杂道路的道路。因此,请继续关注,我们准备平衡当今的乐观主义与务实主义,并将目光从可能性的高度转向坚实的实践基础。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值