Amazon Dynamo系统架构

0x00 摘要

本文参考了网上众多文章,把 Amazon Dynamo 架构汇总成文,为后续源码分析奠定基础。

特此感谢各位作者。

0x01 Amazon Dynamo

亚马逊在业务发展期间面临一些问题,主要受限于关系型数据库的可扩展性和高可用性,因此研发了一套新的、基于 KV 存储模型的数据库,将之命名为 Dynamo

相较于传统的关系型数据库 MySQLDynamo 的功能目标与之有一些细小的差别,例如: Amazon 的业务场景多数情况并不需要支持复杂查询,却要求必要的单节点故障容错性、数据最终一致性(即牺牲数据强一致优先保障可用性)、较强的可扩展性等。

1.1 概况

在上述功能目标的驱使下,Dynamo 需要解决以下几个关键问题:

  • 它要在 CAP 中做出取舍,Dynamo 选择牺牲特定情况下的强一致性(这也是大多数新兴数据库的权衡)优先保障可用性;
  • 它需要引入多节点,通过异步数据流复制完成数据备份和冗余,从而支持单节点故障切换、维持集群高可用;
  • 它需要引入某种 “再平衡(rebalance)” 算法来完成集群的自适应管理和扩展操作,Dynamo 选择了一致性哈希算法;

为了保证其稳定性,Amazon的系统采用完全的分布式、去中心化的架构:

  • 作为底层存储架构的Dynamo也同样采用了无中心的模式;
  • Dynamo只支持简单的键/值(key/value)方式的数据存储,不支持复杂的查询;
  • Dynamo中存储的是数据值的原始形式,即按位存储,并不解析数据的具体内容;

因此,Dynamo 叙述的是一种 NoSQL 数据库的设计思想和实现方案,它是一个由多节点实例组成的集群,其中一个节点称之为 Instance(或者 Node 其实无所谓),这个节点由三个模块组成,分别是请求协调器、Gossip 协议检测、本地持久化引擎,其中最后一个持久化引擎被设计为可插拔的形式,可以支持不同的存储介质,例如 BDBMySQL 等。

1.2 主要问题及解决方案

Dynamo在设计时被定位为一个基于分布式存储架构的,高可靠、高可用且具有良好容错性的系统。下面列举了Dynamo设计时面临的主要问题及所采取的解决方案。

问题采取的相关技术
数据均衡分布改进的一致性哈希算法
数据备份参数可调的弱quorum机制
数据冲突处理向量时钟(Vector Clock)
成员资格及错误检测基于Gossip协议的成员资格和错误检测
临时故障处理Hinted handoff(数据回传机制)
永久故障处理Merkle哈希树

1.3 数据均衡分布

数据分片实在是太常见了,因为海量数据无法仅存储在单一节点上,必须要按照某种规则进行划分之后分开存储,在 MySQL 中也有分库分表方案,它本质上就是一种数据分片。

数据分片的逻辑既可以实现在客户端,也可以实现在 Proxy 层,取决于你的架构如何设计,传统的数据库中间件大多将分片逻辑实现在客户端,通过改写物理 SQL 访问不同的 MySQL 库;而 NewSQL 数据库倡导的计算存储分离架构中呢,通常将分片逻辑实现在计算层,即 Proxy 层,通过无状态的计算节点转发用户请求到正确的存储节点。

1.3.1 Redis 集群的数据分片

Redis 集群也是 NoSQL 数据库,它是怎么解决哈希映射问题的呢?它启动时就划分好了 16384 个桶,然后再将这些桶分配给节点占有,数据是固定地往这 16384 个桶里放,至于节点的增减操作,那就是某些桶的重新分配,缩小了数据流动的范围。

1.3.2 Dynamo 的数据分片

Dynamo 设计之初就考虑到要支持增量扩展,因为节点的增减必须具备很好的可扩展性,尽可能降低期间的数据流动,从而减轻集群的性能抖动。Dynamo 选择采用一致性哈希算法来处理节点的增删。

我们想象一下传统哈希算法的局限是什么,一旦我给定了节点总数 h,那数据划分到哪个节点就固定了(x mod h),此时我一旦增减 h 的大小,那么全部数据的映射关系都要发生改变,解决办法只能是进行数据迁移,但是一致性哈希可以在一个圆环上优先划分好每个节点负责的数据区域。这样每次增删节点,影响的范围就被局限在一小部分数据。

一致性哈希是存在缺点的,如果仅仅是直接将每个节点映射到一个圆环上,可能造成节点间复杂的范围有大有小,造成数据分布和机器负载不均衡。因此一致性哈希有个优化举措,就是引入虚拟节点,其实就是再引入一个中间层解耦,虚拟节点平均落在圆环上,然后实际节点的映射跟某几个虚拟节点挂钩,表示我这台物理节点实际负责这些虚拟节点的数据范围,从而达到平衡负载的作用。

每个虚拟节点都隶属于某一个实际的物理节点,一个物理节点根据其性能的差异被分为一个或多个虚拟节点。各个虚拟节点的能力基本相当,并随机分布在哈希环上。

Dynamo将整个哈希环划分成Q等份,每个等份称为一个数据分区(Partition)。在存储数据时,每个数据会被先分配到某个数据分区,再根据负责该数据分区的虚拟节点,最终确定其所存储的物理节点。

1.4 数据复制

数据复制是提升数据库高可用的常见手段,从实现方式上可分为同步复制、异步复制、半同步复制等,从使用场景上又可分为单向复制、双向复制、环形复制等。

Dynamo 的设计中为了保证容灾,数据被复制到 N 台主机上,N 就是数据的冗余副本数目,还记得我们说过 Dynamo 中每个节点有一个模块叫做请求协调器么,它接收到某个数据键值 K 之后会将其往圆环后的 N - 1 个节点进行复制,保证该键值 K 有 N 个副本,因此 Dynamo 中实际上每个节点既存储自己接收的数据,也存储为其他节点保留的副本数据。

1.5 读写流程

Dynamo 会在数据的所有副本中选取一个作为协调者,由该副本负责转发读写请求和收集反馈结果。通常情况下,该副本是客户端从内存中维护的 数据 – 节点 映射关系中取得的,将请求直接发往该节点。

对于写请求,该副本会接收写请求,并记录该数据的更新者和时间戳,并将写请求转发给其他副本,待 W 个副本反馈写入完成后向客户端反馈写入操作成功;读取流程类似,转发读请求至所有副本,待收到 R 个副本的结果后尝试选取最新的数据版本,一旦发现数据冲突则保留冲突反馈给客户端处理。

显而易见的是,由于协调者是处理读写请求的唯一入口,因此该副本所在节点的负载肯定会飙高。

1.6 数据冲突解决

分布式系统架构中通常考虑的三个因素:

  1. 可靠性(Reliability)
  2. 可用性(Availability)
  3. 一致性(Consistency)

Dynamo选择通过牺牲一致性保证系统的可靠性和可用性 ,没有采用强一致性模型 而采用了最终一致性模型

在数据存在 N 个冗余副本的情况下,想要保证强一致需要等待所有副本写入完成才能返回给客户端写入成功,但这是性能有损的,实践中通常不这么做。Dynamo 允许用户设置至少写入 W 个副本才返回,而读取的时候需要从 R 个副本上读到值才能返回,因此只要 W + R > N,就能保证一定能读到正确的值。

但是这有个问题是如何判断返回的 R 个值中哪个是最新的呢,即每个数据都应该有一个版本信息。Dynamo 为了解决这个问题引入向量时钟的概念,简单来说就是每次写入操作,写入的副本会为这条数据变更新增一个更新者和版本号的向量组 作为版本信息,在后续的复制流程中也会带上这部分信息。

由于Dynamo中可能出现同一个数据被多个节点同时更新的情况,且无法保证数据副本的更新顺序,这有可能会导致数据冲突

数据冲突问题如何解决?Dynamo中采用了向量时钟技术(Vector Clock)

Dynamo中的向量时钟通过[node, counter]对来表示。其中 node 表示操作节点。 counter是其对应的计数器,初始值为 0 节点每进行一次更新操作则计数器加 1。

既然有版本冲突的问题,冲突版本的合并就只能交给上层应用来做

1.7 集群成员状态监测

Dynamo 想要做到 HA(高可用),除了数据复制之外,还需要定时探测集群节点的可用性,有的业界产品依赖外部服务统一处理,例如 MySQL 的 MHARocketMQ 的 NSTiDB 的 PD 等,也有的依赖于节点间自适应管理,例如 Redis 集群和 Dynamo,这二者均采用了 Gossip 协议作为集群间节点信息交换的解决方案,无需引入外部服务,是完全的去中心化的架构。

由于Dynamo采用了无中心的架构,每个成员节点都需要保存其他节点的路由信息。为了保证每个节点都能拥有最新的成员节点信息,Dynamo中采用了一种类似于Gossip(闲聊)协议的技术

Dynamo中还通过Gossip来实现错误检测任何节点向其他节点发起通信后,如果对方没有回应,则认为对方节点失效

为了避免新加入的节点之间不能及时发现其他节点的存在,Dynamo中设置了一些种子节点(Seed Node)。种子节点和所有的节点都有联系。当新节点加入时,它扮演一个中介的角色,使新加入节点之间互相感知。

1.8 容错机制

1.8.1 临时故障处理机制

为了处理临时失效的节点,Dynamo中采用了一种带有监听的数据回传机制(Hinted Handoff)。当虚拟节点A失效后,会将数据临时存放在节点D的临时空间中,并在节点A重新可用后,由节点D将数据回传给节点A。

1.8.2 永久性故障处理机制

Dynamo采用Merkle哈希树技术来加快检测和减少数据传输量。每个虚拟节点保存三颗Merkle树,即每个键值区间建立一个Merkle树。Dynamo中Merkle哈希树的叶子节点是存储每个数据分区内所有数据对应的哈希值,父节点是其所有子节点的哈希值。Merkle树最大特点是只要比较某个子树就可以完成数据同步检测和定位,进而进行同步,大大减少了同步过程中所需传输数据量,提高了系统效率。

0x02 NetFlix Dynomite

2.1 概述

Dynomite 是 NetFlix 对亚马逊分布式存储引擎 Dynamo 的一个开源通用实现,它不仅支持基于内存的 K/V 数据库,还支持持久化的 Mysql、BerkeleyDb、LevelDb 等数据库,并具有简单、高效、支持跨数据中心的数据复制等优点。Dynomite 的最终目标是提供数据库存储引擎不能提供的简单、高效、跨数据中心的数据复制功能。目前,Dynomite 已经实现了对 Redis 和 Memcached 的支持。

2.1 概念

Dynomite有数据中心、机架、节点的三层概念,数据中心可以有多个机架rack,机架可以有多个节点。每个机架数据是完整的,机架上的不同节点各有一部分数据,即是数据分片。

每个 Dynomite 集群包括多个数据中心(dc),每个数据中心都有一组机架,每个机架包括多个节点,每个机架都包括完整的数据集,该数据集被划分在同一机架的多个节点上。因此,多个机架架构能够提供更高的可用性的数据服务。机架上的每个节点都有个一个唯一的标示,该标示用来识别节点属于哪个数据集。每个 Dynomite 节点都有一个 Dynomite process 组件用来协同数据服务器提供服务,该组件具有代理、路由、协调等作用。

可能某些物理机在一个机房或者机架甚至就是一个机器上的几个虚拟机,那么这些机器之间的通信等速度肯定会更快,这些机器可以组成一个集群,就叫一个rack。

在dynomite拓扑结构中,每个rack都是一个完整集群,每个rack的都拥有完整的数据,多个rack间相互备份,这就达到了高可用。

dynomite结构中,每个rack都是一个一致性hash环,具体规则是rack上每个节点都是个redis master,是可读写的。在每个redis节点上都挂载着一个dynomite代理,每个代理持有一个tokens,一致性hash的分配就是根据这个tokens来的,tokens计算规则:从0开始 token = (4294967295 / numberOfNodesInRack) * nodeIndex。每个rack上存在节点都是可以不同的,不需要对应,因为每个rack上的tokens都是重新计算的。
当客户端的请求到达任意一个dynomite代理后,dynomite会根据tokens计算出这个key是否属于自己管理的节点,如果不是的话,会把请求发送到对应的dynomite代理上。

同时,还会把这个请求发送到其他的rack的dynomite代理上,以此来完成rack间的数据同步,这个rack间的数据同步时异步的,但是当我们要求强一致性的时候,可以通过配置参数,当有多少个rack完成数据写入时,才返回结果,根据对一致性要求程度的不同来设置不同的参数。

2.2 数据复制

Dynomite 支持多数据中心的复制,当发送写操作时,客户端能够连接到 Dynomite 集群的任意一个节点。如果 Dynomite 节点恰好接收的数据是属于本节点的数据时,该数据首先会被写到本地数据库服务中,并且异步的复制到所有数据中心的集群中的其他机架中。如果节点接收到不属于本节点的数据,Dynomite 将会以一个协调者的角色发送写的操作到相同机架上应该存储数据的节点上,并复制写操作到其他机架和数据中心的相应节点上。

Dynomite 还具有一个常规、一致性的哈希环,但是复制是不对称的。Dyno 客户端的本地写使用了基于令牌的负载均衡,Dyno 客户端在相同区域知道 Dynomite 的集群拓扑结构,因此,Dynomite 能够使用一致性哈希直接将数据写到一个具体的节点中。

2.3 Redis指令支持度

支持度较高,除了以下情况外未发现其他不支持的指令

  • keys * 、flushall、del key1 key2 等批量执行指令实际上只能处理到Dynomite直接连接的Redis节点的数据。这很好理解,因为批量指令要到多个实例去执行并合并结果,执行时间会较长,而且如果执行结果只有部分正常,合并后的执行结果将会相当复杂。其中keys * 指令会获取到Dynomite链接的Redis实例的key列表
  • 订阅发布指令不支持,估计也是因为集群下比较难处理
  • 不支持 rename 指令

2.4 优缺点及其应用于生产环境的风险评估

优点

  • 支持多主集群
  • 配置使用相对较为简单直观
  • 比起直连Redis性能折损相当少,可以忽略
  • 对Redis的支持度相当高,完全足够平时开发使用

缺点

  • 集群功能的辅助功能不够完整,缺少不停机动态扩容功能
  • 缺少内置的数据同步功能,新增节点
  • 缺少内置的数据同步功能,Dynomite或Redis节点故障停机重启后不会自动从其他节点同步数据
  • 高可用功能有一定缺陷,Dynomite节点对应的Redis挂掉之后,访问这个节点时,如果key是属于这个Redis的会直接报错,不会到其他数据中心拿数据
  • 文档比较少特别是中文文档,不够详细,比如各类配置的可选项、各配置的关联互动、异常处理说明、第三方配合使用工具说明很少,
  • 社区不活跃
  • 更新有点慢,4-6个月更新一次代码

对于数据库集群方案,以下几点非常重要

  1. 零侵入:业务系统不需要做任何改造就能接入
  2. 高吞吐量:基于现有业务峰值TPS乘以10,得出TPS要达到1万
  3. 低延时:多活业务不会出现跨机房读取数据的情况,所以定的目标延时低于1s。实际情况延时在50ms左右
  4. 高堆积能力:基于跨机房网络的不确定性,当网络闪断时能够保证指令不丢失
  5. 高可用性:当网络故障或者Redis宕机恢复时,同步任务能自动恢复
  6. 可配置性:业务系统可以自由定制需要同步哪些Key

Dynomite在第1、2、3 方面做得比较好,第4支持但是有一定缺陷,第5不够完善,6不支持。

总的来说Dynomite作为集群方案是功能不够完善,和Redis Cluster相比多了多主功能,但是缺失动态扩容、自动同步数据等功能;高可用方面也有一定缺陷。社区活跃度和文档都比较欠缺,更新较慢。生产环境使用风险较大。如果实在要用建议搭配Redis Cluster使用以解决动态扩容、新增节点和故障节点自动同步数据等问题,并且应该将其当做缓存集群,避免当做持久化数据库,特别是用户数据等核心数据。

原创文章,作者:Zhu, Yuanyuan,如若转载,请注明出处:https://www.yidc.net/archives/16092