- 路由层收到请求之后,查询MySQL(某种元数据服务,这里简化成MySQL), 来确定与后端的哪台服务器进行通信。
- 根据MySQL返回的地址信息,与数据层的服务节点(data-1...data-4)进行同行。
缺点
- 存储路由信息MySQL会成为瓶颈,性能和空间都会有问题。
- 后端进行扩缩容的时候,需要对路由MySQL进行大量的变更操作(需要修改每条key所对应的存储层服务器的地址信息)。
问题关键
上面的方案的主要的问题在于,路由元数据的压缩不够明显,每条记录的元数据都进行了存储。常识告诉我们,计算和存储之间可以进行转换,即通过计算来降低存储空间。
改进方案
- 引入虚拟的sharding层
- 将key通过计算的方式得到一个虚拟的shard
- 路由MySQL中只存放虚拟的shard到存储层服务器地址的映射信息
- 虚拟的sharding的数量可以先拍脑袋决定,比如2233个。
新写入过程
- 路由层收到key之后, hash(key) % 2233 得到一个[0, 2232]之间的数字,比如X, 即shardX。
- 查询路由MySQL,得到shardX所对应后端的数据存储层MySQL服务器的地址。
- 与数据存储层MySQL进行通信,获取数据。
优点
- 这里路由层MySQL只需要存储2233条记录即可,每条为(shardID, 数据存储层MySQL的IP),shardID为主键。
- 由于这些数据变动的概率非常小,变动的内容有限,完全可以在路由层的内存中缓存。
- 存储层进行扩容(缩容操作)的时候,只需要在路由MySQL中修改一条记录即可。即shardID到数据层MySQL新的地址信息。
- 路由层在感知(主动/被动)到变化后,只需要在更新本地的路由表即可。
- key(blockid)到shard的映射信息,由计算得到,无需存储(eg: md5sum(blockid) % shard的数量)。
对存储层的影响
假设水平扩容的实现过程如下:
- 从存储节点A上,将隶属于shardX的所有数据copy到存储节点B上
- 更新路由表中shardX的地址信息(从节点A的IP变更为节点B的IP)
- 由于在copy的同时,仍然有数据继续写入,因此需要一些容错逻辑,这里不展开。
写入流程如下:
- IO层将请求发送给存储层节点时,需要标示对应的shard信息(比如shard id)
- 存储层的服务节点在处理请求时
- 判断对应的shard在本地是否存在,存在则进行处理。不存在,返回特定错误,提醒写入逻辑更新路由表(有可能shard在进行迁移)。
- 如果对应的shard存在,这直接写入。
data表schema更新
为了便于迁移属于同一个shard的所有数据(快速扫描出来,该shard的所有数据),存储层表的schema更新如下(新增shard_id字段):
block_idvalueshard_idblockID实际数据shardID
迁移数据时,根据shard_id字段进行过滤即可。至此,Day4工作完成,今天我们完成了数据层的sharding过程,并为水平扩容打下了基础。
Day5到目前后端的数据均存在MySQL中,MySQL的好处在于稳定易用,但是功能过于复杂,性能也不能满足要求。今天我们对MySQL进行替换。
数据存储节点语义
- 对外提供PUT/GET 接口(先忽略Del接口)。
- 相应的参数为shardID, key(int64), value。
根据这些需求,可以将存储节点进行如下两种设计:
存储节点设计简介
- 从上至下分为 RPC 层、shard层和引擎层
- RPC层负责通信
- shard层将RPC请求转换为对具体的某个shard的读写操作
- engine层则负责将请求转换为对磁盘的读写操作
方案对比
方案1
- 请求进入RPC层之后, 根据shardid 进行分发,获取到对应的shard实例(句柄)
- shard使用key和value操作engine层
- 一个节点(或者一块磁盘)公用一个engine,使用shardID作为key前缀,用于区分不同的shard(在迁移的时候,可以使用shardID为前缀扫描属于该shard的所有的key和value)
方案2
- 方案2作为方案1的简化版本
- 区别在于,一个shard实例(句柄)对应一个engine实例(而非方案1的全局公用)
- 优点在于:
- 实现更简洁
- 进行数据搬迁的时候,可以对整个engine进行snapshot拷贝即可(无需逐条扫描)
引擎的实现
今天我们直接使用RocksDB作为我们的单机引擎,不做其他优化。
新架构
数据存储节点替换了原有的MySQL服务,今天的目标达成,收工。
Day6前面几天已经实现了数据存储层的sharding。但是sharding只能解决水平扩展问题,容灾仍然有问题。今天我们对数据存储集群的资源重新进行整理。
- 引入资源池和可用区(故障隔离域)的概念。同一个资源池内的机型同构(简化资源调度逻辑,比如相同的磁盘数量和磁盘大小)。不同的业务可以使用不同的资源池,做到存储层资源隔离。
- 将不同交换机下的节点定义为不同的可用区(故障隔离域)。可用区之间实现交换机级别的隔离。
- 每个存储集群由一个或者多个资源池组成。资源池之间IO隔离,资源池内部机型同构。
- 每个资源池内部,由多个可用区组成。每个可用区由若干台服务器(存储节点)组成。
- 修改路由表中shard到IP的映射关系。
- 一个shard对应到多个Replica(比如3副本)
- 路由表中存放每个Replica所在存储节点的地址信息。
- 一个shard对应的Replica被放置于不同的可用区中(比如3个Replica放在不同的可用区)。
- 3副本模式的时候,任何一个交换机下的节点宕机,都不会影响读写操作。
新架构
下图为一个资源池 4可用区的模式,每个shard拥有3个副本(Replica)。
- 资源池A由4个可用区组成(可用区0、可用区1、可用区2、可用区3)。
- 每个可用区由3个存储服务节点(Node)组成。
- 以Shard1为例,其所对应的3个Replica(Replica0,Replica1,Replica)分布于资源池A的3个存储节点上。
- 这3个节点分别位于可用区0,可用区2和可用区3
路由层的MySQL中存储的信息如下:
- shard1->(Replica1, Replica2, Replica3)
- Replica1->IPof(可用区0,Node1)
- Replica2->IPof(可用区2,Node0)
- Replica3->IPof(可用区3,Node2)