双活架构是一种数据弹性架构,它通过独立且地理上分散的集群和节点将数据库信息分布在多个数据中心。它是由独立的处理节点组成的网络,每个节点都可以访问一个共同的复制数据库,这样所有节点都可以参与一个共同的应用,确保本地低延迟,并且每个区域都可以独立运行。
通过在 Redis Enterprise 中使用跨多个集群的全局数据库来实现双活架构或双活地理分布拓扑。这通过实现 CRDTs(无冲突复制数据类型)来达成。这种数据库被称为“无冲突复制数据库”或“CRDB”。
CRDB 提供了优于其他地理分布式解决方案的三大基本优势
CRDB 是一种跨多个 Redis Enterprise 集群创建的数据库,这些集群通常位于世界各地不同的数据中心。每个参与集群中的数据库称为一个“CRDB 实例”。只要 CRDB 数据集能够容纳在 CRDB 实例内存中,每个 CRDB 实例就可以进行不同的配置——由不同数量的分片组成,并在不同数量或类型的集群节点上运行
此外,一个 CRDB 实例可以运行在启用了高可用性和数据持久性的多可用区 (AZ) 集群配置上。第二个 CRDB 实例可以运行在没有高可用性或数据持久性的单可用区集群配置上。这些 CRDBs 可以在任何云环境中同时运行,包括 Google Cloud、Azure 和 AWS。这种增加的灵活性优化了您的特定用例的基础架构成本和数据库性能。
使用 CRDB 的应用连接到本地 CRDB 实例端点。所有 CRDB 实例之间使用双向数据库复制,形成网状拓扑结构,也就是说,应用对本地实例的所有写入都会复制到所有其他实例,如下所示
CRDB 架构基于大多数 Redis 命令和数据类型(如前所述称为无冲突复制数据类型)的另一种实现。在 Redis Enterprise 中,我们的 CRDB 实现基于使用 Redis 模块数据类型 API 构建的专有 Redis 模块。
读取命令使用本地 CRDB 实例在本地处理。CRDT 层固有的无共识机制不需要其他双活实现中常见的“读取修复”。
写入 命令遵循基于操作的 CRDT 原则,分两个步骤处理
基于操作的 CRDT 要求将效果更新以源端 FIFO 一致性的方式精确地投递到所有 CRDB 实例。CRDB 通常依赖 Redis 复制机制,并进行了一些修改以满足这些保证。
CRDB 复制由 syncer(同步器)实现,它们联系远程主节点并请求对等复制(如下图所示),并为此目的引入了一种新的数据库复制机制。对等复制的工作方式与标准 Redis 复制类似
一旦对等节点建立复制链接,只有由 CRDT 模块生成的 CRDT 效果会被传播。根据拓扑结构,可能会应用额外的过滤以仅包含特定更新。使用网状拓扑时,对等复制将仅携带在本地 CRDB 实例上生成的效果。
CRDB 实例可以将不同的复制流推送到其他对等节点,以便对等复制机制可以管理许多不同的复制积压。
当识别到远程公网 IP 时,会自动对对等复制应用 GZIP 压缩,以便更好地利用广域网链接。此外,如果在连接建立过程中识别到 SSL 握手,加密将自动启用。
对等复制操作如下所示
如果一个或多个 CRDB 实例发生故障,全局 CRDB 下的其他实例将继续读写,提供持续的可用性和灾难恢复。即使大多数 CRDB 实例(例如,5 个中有 3 个)停机,剩余的 CRDB 实例也不会中断,并且可以继续处理读写请求。在此类区域性故障中,无法连接到本地 CRDB 实例的用户通常会被导向到指向可用 CRDB 实例之一的其他数据中心。这即使在用户的本地 CRDB 实例停机时,也能为应用读写提供持续的可用性。
在极少数情况下,CRDB 实例可能会经历完全的数据丢失,需要从头开始进行数据库复制。这种情况需要特殊处理,因为恢复中的 CRDB 实例可能已经向其部分对等节点发送了更新。由于无法再期待进一步的更新,我们不能假定所有对等节点最终都会收敛(某些效果消息可能已被部分对等节点收到,但未被其他对等节点收到)。在此场景下,Redis Enterprise 实现了一种涉及所有相关 CRDB 实例的协调机制。协调完成后,恢复中的实例可以简单地从任何其他副本进行完全同步。
在 CRDB 部署中应用了多种一致性特性
CRDB 冲突解决基于 CRDT 的三个原则
每个 CRDB 实例分别为每个数据集对象/子对象维护一个向量时钟。此向量时钟在实例级别的任何更新操作发生时,或当另一个 CRDB 实例传来针对同一对象的更新操作时进行更新。
当从另一个实例收到更新操作(及向量时钟)时,以下过程会在每个 CRDB 实例中单独执行
阶段 1: 分类更新操作
收到的更新操作可能代表 (1) 新更新,(2) 旧更新,或 (3) 并发更新。
分类算法工作如下
当实例 A 从实例 B 接收到关于对象 X 的更新时
其中 x_vc[a] 是实例 A 中对象 X 的向量时钟,x_vc[b] 是实例 B 中对象 X 的向量时钟。
阶段 2: 在本地更新对象
CRDB 冲突解决算法基于两个主要过程
过程 1: 无冲突数据类型/操作的冲突解决
在许多并发更新状态下,可以根据适用数据类型的属性完全无冲突地处理更新。以下是一些示例
在所有这些情况下,此实例中的对象值根据数据类型策略进行更新。
过程 2: 使用最后写入获胜 (LWW) 机制的冲突解决
在非无冲突数据类型(例如 Redis String,映射到 CRDT 的 register)的并发更新情况下,应应用冲突解决算法。我们使用了 LWW 方法,通过利用操作时间戳作为决胜点来解决此类情况。
请注意,即使区域之间存在时间戳偏差,我们的解决方案也能以强最终一致性的方式工作。例如,假设实例 A 的时间戳始终领先于其他实例的时间戳(即在决胜点的情况下,实例 A 始终获胜)。这确保了最终一致的行为。 示例: 对于由多个地理分布实体访问的用户账户,密码被更改。在这种情况下,更改会强制其他用户注销,这可能是许可证强制执行场景下的正确行为。
数据类型: 字符串
用例: 非并发 SET 操作
冲突解决: 无冲突
注意:无冲突,key1 最后在 t5 被设置为“value3”。
数据类型: 字符串
用例: 并发 SET 操作
冲突解决: 最后写入获胜 (LWW)
注意:由于 t2>t1 且遵循 LWW 规则,key1 被设置为“value2”。
数据类型: 字符串
用例: APPEND 对比 DEL
冲突解决: 添加获胜
注意: APPEND 是一个被视为“添加”的“更新”操作,因此它会胜过 DEL 操作。
数据类型: 字符串
用例: 并发过期
冲突解决: 较大的 TTL 获胜
注意:在 t6,实例 B 执行了持久化操作(通过 PERSIST key1),这意味着其 TTL 被设置为无限(即 -1),这比由 实例 A 设置的 100 更大。
数据类型: 计数器
用例: 并发增量/减量操作
冲突解决: 无冲突
注意:在 CRDT 层中,计数器值是所有操作的总和。
数据类型: 计数器
用例: 并发删除和增量操作(计数器值中的观察到移除)
冲突解决: 添加获胜
注意: INCRBY 是一个被视为“添加”的“更新”操作,因此它会胜过 DEL 操作。此外,实例 A 在 t3 执行的计数器删除操作逻辑上意味着重置计数器。
数据类型: 集合
用例: 对集合进行并发 SADD 操作
冲突解决: 无冲突
注意:此示例是无冲突的,因为 SADD 是一个关联操作。
数据类型: 集合
用例: 对集合进行并发 SADD 和 SREM 操作
冲突解决: 添加获胜
注意:集合元素中的添加获胜。
数据类型: 集合
用例: 对集合进行的并发复杂操作(观察到移除)
冲突解决: 集合元素中的添加获胜
注意:在 t5,实例 A 只能从 key1 移除元素 A 和 B 。
数据类型: 发布/订阅 (Pub/Sub)
用例: 已发布的 mesaj 会传播到所有 CRDB 实例
冲突解决: 无冲突
注意:在PUBLISH 回复中,只计算本地订阅者。
数据类型: 字符串
用例: 垃圾回收
冲突解决: 无冲突
注意:CRDTs 大量使用逻辑删除标记 (tombstones)。逻辑删除标记在被所有实例观察到后成为垃圾。
下一节 ► 自动分层存储