方案一
- 进度信息持久化在MySQL中
- 接入层
- 对外提供RPC接口
- 由后端线程去MySQL中进行预分配(incr操作)。再从分配后的空间,对外提供ID分配操作。
- 每次重启后,先去MySQL中进行预分配,再对外提供服务
- 所有的接入层均对外提供分配操作(即上图中的取号器0、取号器1、取号器2都对外提供服务)
问题
方案一的缺点在于,MySQL仍然是单点。
方案二
- 方案二可以认为是方案一的改进版, 使用RAFT实现一个简单的KV
- 此KV对外提供INCR 操作。每次操作返回incr前后的值,作为接入层的安全分配区间
- 这样实现简单,也避免了MySQL的单点。
- 以braft单个raft group约5000 QPS的性能计算,每次预分配10000作为分配空间,分配性能足够。
问题
RAFT异常宕机时,约有5s左右的不可写入时间,因此需要计算(评估)合理的预分配范围,确保这段时间有足够的key可用。
新架构
新的架构如下所示:
Day10对象存储系统,由于整体规模庞大,存储成本会有比较大的压力,这点与计算型系统有较大差别。到目前这个系统还是使用3副本的方式来保证数据的可靠性和服务的可用性。今天我们来降低副本数。Erase code(纠删码/EC)是目前比较常见的降副本的方式。其具体原理的细节这里不再赘述,一言以蔽之就是"时间换空间/CPU换存储"。
EC的基础功能
- 将原始数据切分为等长的数据块
- 通过对数据块进行矩阵计算,生成编码块(也称校验块)
- 数据块的数量用k表示,编码块的数量用m表示
- 所有的数据块和编码块的长度相等(先忽略数据块的padding问题)
- 从这(k m)个块中,任意取出其中的k个,可以通过矩阵运算的方式还原出(k m)个
- eg:
- 假设原始数据为(a,b,c,d,e,f), 使用k=6,m=3的方式,生成3个编码块(g,h,j),总共9个块
- 任意取其中的 b,c,e,f,h,j 则可以还原出a,d,g
如下图所示,编码的具体过程,由6个数据块生成3个编码块。
纠删码空间占用
- 副本数 = (k m)/m, 以k=6, m=3为例,则副本数为 1.5副本
- 3副本模式,可以理解为k=1,m=2的特殊场景
读写过程
如上图所示,我们可以对比下EC模式和3副本模式的写入过程。
副本模式时写入过程
- 根据key进行计算,得到数据应该存放到哪个shard上
- 由shard的信息,定位到对应的3个Replica的地址信息
- 将请求发送给Replica所在的节点进行写入即可(这3个节点位于不同的可用区中)
EC模式时写入过程
- 与副本模式的差别在于不再将原始数据发送给后端存储节点
- 而是先进行编码,然后将编码后的k m个块,发送给后端的节点
- 此时要求后端的Replica的数量等于 k m,而不是原来的3副本
- 这k m个Replica位于k m个节点,并且这些节点处于不同的可用区中
- 理论有k个实际写入成功即可(实际上会根据k和m的数量进行处理)
数据块和编码块间的顺序关系
- 由于这k m个Replica之间的数据是不一样的(可以观察图上数据块的颜色),而且数据块和编码块的之间也有顺序关系(比如返回给用户的时候,一定是按照(数据块0数据块1数据块2)的顺序),因此需要有地方来描述这些数据块之间的关系。
- 我们希望在读取之前就能确定这些块的关系,否则需要读取k个以上的块进行解码才能得知
- 这里我们在Replica的元数据中加入sn_in_shard 用来标识对应的k m个replica的先后关系(可以再查阅Day7时ReplicaModel的定义)
EC模式的读取过程
- IO服务收到读请求后,计算出key对应的shard
- 检查shard对应的k m个replica的信息,得到前k个replica(由sn_in_shard描述),优先将请求发送个给这k个replica。当这k个replica返回结果时,直接拼接原始数据返回即可(读取的时候,尽可能避免走EC解码,EC解码对CPU消耗很大)。
- 同时,新建若干个定时器,构造对应批次的backup request,对应剩余的m个replica
- 如果第一批次返回,则取消这些定时器,避免浪费后端IO
- 如果第一批次的返回值有部分报不存在或者出错,则立即启动上面的定时器
- 由于写入的时候,任意k个以上成功就返回,所以有可能写入的时候失败了
- 或者此时后端部分节点异常
- k m中有任意k个成功后,即可通过矩阵计算还原出原始数据块并返回
对其他模块的影响
修复模块会受影响。3副本模式的修复过程,只需要读取任意一个replica上的数据,就可以进行修复。现在需要读取k个数据,并重新进行EC计算,然后才可以进行修复。
k和m的选择
根据上文的计算k越大, m越小,则副本数(k m)/k=1 m/k越小。那么我们是不是可以把k设置成100,m设置成1呢?这里的k和m 一般受如下几个条件制约
- 一个shard的replica数量等于k m,而这些replica是位于不同的node中,这些node是位于不同的可用区中。k=100,m=1要求有101个可用区,一般公司的故障隔离域没有这么多,规模也没有这么大。
- 原始数据输入后,会切割成k个数据块,如果原始数据长度为1MB的话,那么切割后到后端存储节点只有10KB了,会造成后端节点的IOPS上升(相当于放大了101倍)。
- 读取的时候,需要并行读取100个数据块,长尾和IOPS都会上升。
那么是不是可以在约束k的情况下,降低m的值呢?这里又引入了另外一个话题。
数据可靠性(安全性/持久性)计算
数据可靠性讨论一般指在给定的磁盘故障概率情况下,不发生数据丢失的概率。先不考虑原始写入的时候只写入部分成功的情况。
可靠性相关因数
- 磁盘故障概率:直观解释,假设在某鱼购入一批硬盘,某天中午都同时发生了故障,那么一定会丢数据。
- 坏盘后的修复速度:假设磁盘故障不是同时发生的,那么只要在连续故障导致数据丢失之前,将数据修复回来,也就是修复速度大于故障速度的话,就不会发生数据丢失。假设有个神器,无论磁盘多大,都能在1秒内修复,那也不会发生数据丢失。
- 能够容忍发生连续损坏的盘的数量:假设修复时间恒定(比如1小时修复一块盘),那么此时3副本(可以容忍连续损坏2块盘)的数据可靠性小于10副本的情况(可以连续损坏9块盘)
回顾上文,EC使用k m的策略,任意k个块就可以恢复原始数据,也就是允许m个损坏。所以从数据可靠性考虑,m越多越好。实际环境中k m的策略,需要根据资源(机器数、故障隔离域数)、业务IO特点(比如文件大小)进行具体调整。
Day11目前从架构层看,我们基本完成了S3的大部分功能(不考虑分段上传和Delete操作)。目前engine层还是使用的Rocksdb,而rocksdb在面对长度比较长的value时(1K以上),会有比较大的写性能问题(compaction导致)。今天我们足够解决这个问题。
优化目标
在讨论这些引擎的差别之前,我们先看下存储层的目标。所有的存储的目标都是为了最终数据的读取操作,如果没有读取的话,也就没有存储的动力。如何有效的满足读取目标是各个存储引擎的优化目标。注意,这里我们使用有效的满足而不是高效的满足,区别在于,不是所有的读需求都要求高性能,有些场景,只要能满足就行了。
假设我们采用最简单的策略:
- 收到一个写请求的时候,在文件系统中的某个指定的目录中(比如"/"下),以收到的key(blockid, int64 类型)为文件名,创建一个新的文件,并将文件内容写入.
- 收到读请求的时候,以key文件名,打开文件并读取内容返回。文件不存在,则返回not found。
这种方案如果简单进行测试,发现可以工作,但是是否可以完成目标,该如何进行评估呢?回顾文件系统的实现,我们会发现,本地文件系统,通常也分为元数据存储和数据存储两块。其中元数据主要包括目录树(文件名相关信息)、inode(用于描述文件由哪些数据块组成)、数据块。如下图所示