欢迎大家来到IT世界,在知识的湖畔探索吧!
原创 分布式存储团队 哔哩哔哩技术 2022-05-20 12:00 发表于上海
本期作者
司春峰
哔哩哔哩技术专家
负责B站对象存储、NoSQL、数据传输、数据库代理等方向的研发工作,致力于提供稳定可靠高性能的存储服务。
背景
BLOB(binary large object)存储,通常也被称为对象存储(OSS, object storage service)。一般用来存储文件,如视频文件、音频文件等。目前,各个云计算厂商都对外提供对象存储服务,其中以亚马逊的S3系统最著名,S3系统也成为行业的事实标准。各个云计算厂商推出的对象存储服务,也纷纷兼容S3标准。
B站由于其内容的独特性(视频网站),对象存储也有着非常多的需求。下面我们会介绍B站的对象存储的设计与实现。为了便于大家理解,会采用由简单到复杂的过程进行。我们称这个对象存储系统为BOSS(Bilibili Object Storage Service)。目标13天精通超大规模分布式对象存储系统的架构与设计。前6天的内容详见:十三天精通超大规模分布式存储系统架构设计——浅谈B站对象存储(BOSS)实现(上)。
Day7
拓扑信息(路由信息)单点问题
经过前几天的努力,目前后端存储已经具有多副本,sharding的功能。观察我们的系统,路由数据存放于MySQL中,成为存储后端的单点。今天我们来解决这个单点问题。如下图所示,为了提升集群拓扑信息的安全和可用性,有两种方案:
方案1
- 将数据存储于etcd中,通过etcd的3副本来保证数据安全
- 在etcd之前,加入路由信息缓存层,将路由信息全部镜像到内存中
- 新加入管理节点,负责拓扑信息的变更操作
方案2
- 方案2实际上是方案1的简化版本
- 不再依赖于etcd,而是使用braft实现一个简单的KV结构。也拥有三副本,由braft完成3副本之间的数据复制。
- 在leader节点上集成集群管理逻辑,由这个管理逻辑对集群进行管理。(每个节点都有相同的逻辑,但只有处于leader状态时,才开始工作)
- 路由信息缓存层和方案一类似,区别在于:
- 使用round robin的方式从元数据服务节点(Metaserver)同步数据。
- 同步的时候,利用raft log index只增不减的特性,作为拓扑信息的版本,避免同步到旧的信息。
- 即使metaserver2个节点宕机,拓扑信息也能正常获取到。
元数据
集群中(metaserver中)需要存储的元数据主要包括:
- 表相关(shard, replica等)
- 存储节点相关(IP、磁盘、可用区、资源池等)
表相关元数据
表的元数据主要包括:
- 表的基本属性(创建时间,shard数量,每个shard的replica数量)。
- shard的属性:有哪些replica,该shard在这张表中属于第几个partition(即取mod后的下标)。
- replica的属性:需要描述其对应存储节点的信息,以及位于对应节点的具体哪块磁盘上
我们采用如下的proto文件进行描述,通过id和uuid可以建立起对应的关联信息
存储节点相关元数据
存储节点的元数据主要包括:
- resource pool的基础信息
- 每个resource pool有几个可用区
- 每个可用区由哪些节点组成
- 每个节点有几块磁盘构成
使用如下proto文件进行描述,构建后端集群的分布情况
table和存储节点间的关联关系(拓扑信息)
通过这些信息,我们可以建立如下映射关系
- 在逻辑层,我们可以做这样的映射: (table_name, key) -> shard -> [replica0, replica1, replica2]->[addr0, addr1, addr2]
- 在物理层,集群由多个资源池组成,每个资源池由多个可用区构成,每个可用区由若干台服务节点(DataNode)构成。每个服务节点管理若干块磁盘
- replica最终对应到某个服务节点(DataNode)上的某个磁盘上的一个存储单位
新的架构
Day8
回顾整个存储系统,可以发现目前S3元数据存储和object_id的生成还是单点,今天我们来解决这个问题。
元数据量级:
- 假设在S3元数据侧,每个对象需要100Byte进行描述(文件名、objectId等)
- 假设单个object大小为100KB,总存储规模为100PB,则对应的元数据空间为100T级别 (100PB/100KB * 100Byte)
- 假设单个object大小为10MB,总存储规模为1000PB,则对应的元数据空间为T级别 (1000PB/10MB * 100Byte = 10TB)
可见,对于一个大规模存储集群,通过元数据的提取和压缩,元数据完全可以使用一个小规模的NewSQL或者分布式KV进行存储。
新架构
在S3元数据侧引入新的元数据存储,修改后的架构如下图所示。
Day9
目前object_id的使用MySQL的自增ID生成,存在单点。对于object_id的需求,我们只需要全局唯一,不需要递增。可以使用如下两种方案:
方案一
- 进度信息持久化在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(用于描述文件由哪些数据块组成)、数据块。如下图所示
本地文件系统的读取过程如下(以读取”/image”文件为例):
- 由”/”找到对应的inode(每个文件系统的根inode id通常固定,比如ext2为2,不需要查找过程)
- 根据inode 2,找到对应的datablock id列表(比如上图中的block id 7)
- 将数据块(block id7)的内容读出,转换为目录项(可能是一个有序数组,也可能是个B+树)
- 在目录项中查找”image”,得到对应的inode id(如上图例子为88)
- 根据inode id(88),读取对应的inode信息(一段128 byte左右大小的数据,视不同的文件系统而有所区别)
- 解析inode的内容,得到对应的block id列表(例子中为block id 10,11,15)
- 由block id查找到对应的磁盘块,进行读取操作
对比下我们系统的架构和读取流程,可以发现和我们的系统的工作过程非常相似。我们的系统也有目录树和inode的概念。目录树就是S3元数据中的name表,而inode则是object表。
在单机文件系统上,通常每个目录是存放在一个B树中。当一个目录下的文件特别多的时候,这个B树就会成为瓶颈。同时一个文件对应一个inode,如何缓存inode避免频繁的读取磁盘,也会成为一个重要的目标。
- 假设每块磁盘大小为 10TB
- 每次写入的数据长度为10KB(即一个文件10KB),此时B树的entry数量为10TB/10KB = 10亿条。
- 如果单次写入的数据条数为1MB, 则entry数量为 10TB/1MB = 1千万条。
- 假设每条记录需要 20 + 8 + 8 + 128 = 164byte(假设每个int64由字符串表示时占用20Byte, inode需要128Byte, inode number 8 byte,不考虑其他开销)
- 1000万条约1.6GB内存,可以全量cache。
- 10亿条约160GB内存,无法全量cache
通过上面的计算,我们发现当每次写入后端的数据长度较大(比较理想)的时候,可以直接将数据写在文件系统上,不用做其他优化。这里我们实际上是在利用文件系统的设计,在磁盘之上实现了一个meta和data分离的存储。但是,生产环境中往往没有这么理想,后端接受到的value长度往往长短不一(考虑上文的erasecode的编码过程,会将数据切割成等长的k个块,然后发送给后端节点,一个5MB的文件,k=10的话,切割完成之后发送给后端的数据长度只有500KB了)。为了应对复杂场景保证读写性能,必须进行优化。
观察上面的写入过程,核心问题在于文件数量太多,导致B树的条目数和inode数量过多,而通用文件系统很难进行定制性的优化(比如把B树创建在指定的nvme ssd盘上)。为了降低文件的数量,我们可以将不同的数据写入到相同的文件中(比如每1GB 数据使用一个文件,则此时文件数量为 10TB/1GB = 1万条记录,文件系统的meta部分的内存占用降低到可以忽略的程度)。此时我们面临的问题转变为如何有效的根据key(blockid)在一堆文件中,找到对应的数据。
回想在内存里面,我们优化读取性能的时候,通常两种套路:
- 方法1:k和v放到一个hash中,o(1)的方式进行定位,然后读取
- 方法2:放在一个有序集合(比如有序数组),进行折半查找。
- 其他:其他优化折半查找次数的方法、提升索引效率的方法
在磁盘上,其实我们可用的的优化方法也类似:
- 方法1:对这些文件内容进行排序,查找时进行折半查找即可。
- 方法2:建立key(blockid, int64类型)到文件名和文件内部offset的索引,从索引中查找到文件名和位置信息之后,进行读取即可。此索引必须高效,可以放在内存或者nvme ssd中。
根据对文件内容进行排序的时机,可以分为离线排序和在线排序(写入的同时)。最直观的区别在于,在线排序会导致写入性能下降(有点废话,关键路径上做了更多的事情)。
常见存储引擎
根据上文提及的优化手段,目前常见的存储引擎有如下几种
- bitcask
- b/b+树
- LSM tree
- 上述各种引擎的优化和变种(组合)版本
B/B+树
B/B+树的本质在于写入的时候,构造一个全局有序的数据集合,然后在这个有序集合上进行折半查找。由于磁盘进行折半的开销太大(有多次IO操作),因此建立多个层次的索引,并尽可能的将这些索引load到内存中,减少查找的代价, 如下图所示:
- 由多个有序的文件构成
- 文件内部有序,文件之间也有序
- 索引信息位于内存中(尽可能)
优缺点
对于连续key访问,相应的数据都在一起,因此可以利用磁盘的顺序IO获得比较大的吞吐。由于对象存储系统通常都是离散的IO,而且上层做了key打散,因此不能发挥顺序IO的优势。同时,由于为了构造全局有序的集合,需要不停的进行分裂和调整,磁盘上的随机写IO也会非常明显。
LSM tree
LSM tree 本质上是在多个有序文件之间进行查找。
如上图所示,LSM tree的工作流程如下:
- 在内存中构建一个排序的数据集合(通常使用skip list)
- 攒到一定规模后(比如16MB),将排序后的结果dump到磁盘。为了在内存中进行排序,同时又保证数据不丢失,需要把写入操作先LOG。这样如果重启的话,可以从LOG重放,重建内存数据集合。
- 从内存中直接dump出的有序文件,放到称之为L0的层的集合中。这里可以想象,如果写入速度非常高,那么会有相当数量的有序文件堆积在L0层中。这些文件之间只有时间上的先后顺序,每个文件内部有序。此时如果需要查找一个key的话,本质上需要按照时间的先后顺序,对每个文件进行折半查找。当L0文件的数量堆积到一定程度之后,读取的效率可想而知。为了缓解这种情况,通常LSM写给的方案是阻塞写入,由后台线程对已经存在的数据先进行排序(嗯,先别写入数据了,等我排好序再来)。排序时,采用读取多个有序文件,merge后写到新文件中的方式(非原地修改)。
- L0的这些文件,经过排序之后,就可以生成一个L1有序文件集合了。如上图所示,L1的文件之间和内部都是有顺序关系的。
- 不考虑层间的排序情况,如果步骤3的情况再发生一次,此时的L1变成L2,L0的文件进行排序后生成L1的文件列表。
- 如上图所示,经过多次dump之后,不考虑各种优化手段(bloomfilter),如果需要查找key=4.5,需要在L0的3个文件、L1、L2、L3等多个文件内部进行查找。
优缺点
LSM tree在写入的关键路径上,不对key进行排序,而是由后端线程进行离线排序,因此读取的性能依赖于排序的状态。如果都排序完成,不考虑优化手段的区别,读取性能可以类比与B树。排序跟不上写入时,读取性能则退化为在多个文件间进行顺序折半查找。
bitcask
bitcask类型的引擎,可以类比与内存中的hash结构,放弃了对有序的需求,通过内存中的全索引,快速定位到磁盘上的绝对位置,然后转换为对磁盘的一次读取操作。
如上图所示,读写工作流程如下:
- 每个engine,维持一个正在写入的文件,新的写入都写入到此文件中。
- 写入文件成功后,在索引中记录key->(file_name, offset,len)信息(索引可以是内存hash或者位于nvme ssd上的其他engine) 。
- 读取时,从索引中获取到(file_name, offset,len)信息,然后打开相应的文件,seek到指定的位置,进行读取即可。
- 删除时, 在新的文件中写入删除标记,并在索引中删除对应的key 即可。
- 覆盖写的逻辑和删除逻辑类似,如上图的key=1,写入两次后,索引中的位置指向后一次写入的位置信息
数据回收流程
- 从最早写入的文件开始遍历磁盘数据(k,v)
- 判断索引是否还指向当前记录,如果不再指向,则说明有新的写入或者被删除,跳过当前记录即可。如果指向,则将当前记录重新进行写入即可。此写入和普通的写入一样,会追加写到最新文件的末尾。
- 这里需要注意,判断索引是否指向当前记录到真正执行写入,有时间差(race condition)。因此需要注意锁的使用以及锁的区间。
优缺点
- bitcask类型的引擎,由于meta的定位耗时低(位于内存或者nvme ssd中),读延迟低。通常只需要一次磁盘IO
- 写入操作,都是追加写,写吞吐高
- 所有的读取都是随机读操作,没有顺序遍历能力
- 空间回收效率低,假设一个文件中的20%的数据已经被删除,则需要读取80%的数据,并写入到新文件后,才能释放20%的空间。
常见优化手段
- bitcask的常见优化手段主要集中在优化索引的内存占用,通常内存中需要存储(k, file_name, offset, length)等信息, 因此各种优化手段集中在压缩这些信息(当然也可以不用内存,而使用nvme ssd,比如将索引放到位于nvme ssd上的rocksdb中)
- 对key的压缩,通常基于一些业务相关的先验知识,或者对于string 类型的key,可以使用trie-tree
- 假定一个文件大小为2GB,一块磁盘10TB,则其上的文件数量为10TB/2GB = 5*1024, 12bit的文件编号即可表示
- 将文件内部划分为128byte的block,一个2GB的文件有2GB/128byte =16,777,216 个块,24个bit即可表示
- length信息,可以不用存储,比如可以根据先验知识进行读取(比如4K),实际长度可以通过数据的头部进行描述。保证99%的情况下,只有一次读取过程即可(读出的结果中,有多余的数据进行剔除,不足的话,根据头部中的描述信息,再读一次,也可以对先验知识进行动态学习)。
回顾我们的场景,对后端的所有读取均为随机读取,没有顺序遍历需求。虽然bitcask的空间回收效率低,但实现简单,并且可以方便的获取key 列表(用于修复模块)。因此实现一个bitcask类型的引擎,提供put/get/del等接口即可。至此,今天任务完工,收工回家。
Day12
到现在,该分布式存储系统除了删除功能和分段上传功能外,从协议接入层到engine层,功能基本完备。今天我们来实现del功能。
简单实现
删除功能的基本实现如下:
- 网关接受到Del请求后,通过s3元数据服务,获取到当前对象的object id和对应的block id 列表
- 通过s3元数据服务,从name表中,删除相应的文件
- 从object表中,将对应的object id删除
- 通过IO服务,将对应的block id进行删除
问题
回顾上文,我们的修复模块,会将一个shard不同的replica上缺失的key(block id)进行补回,删除逻辑和修复模块会存在竞争。会出现一个刚刚被删除的key,被修复模块又重新补回的情况。
修正
删除逻辑和修复逻辑做如下修正:
- 删除逻辑给后端发送标记删除请求(而非删除请求)
- 修复逻辑发现标记删除之后,将所有的副本设置为标记删除状态
- 只有当所有的副本都处于不存在或者标记删除状态时,才进行真正的删除操作
Del功能实现完成,收工回家。
Day13
今天我们来实现水平扩容和副本修复功能。
副本修复功能
副本缺失通常出现在磁盘损坏的情况。磁盘损坏后,其上的replica也会相应损坏。此时对于存储系统而言,我们需要及时从系统中摘除这块盘,这时某些shard都出现缺失一个replica的情况(这些replica都位于刚刚被摘除的那块盘上)。系统需要在可用的磁盘(需要考虑可用区)上创建出相应的空replica,然后由修复模块自动修复数据即可。
水平扩容
修复工作流程
以3副本为例,修复功能实际工作流程为:
- 系统处于3副本状态
- 由于坏盘或者其他故障,系统降低为2副本状态
- 系统感知到副本缺失,自动创建出1个空副本,回到3副本状态
- 即3副本->2副本->3副本的过程
- 修复逻辑开始比较3个副本间的数据diff,将缺失的数据补回
扩容工作流程
扩容过程,与此类似:
- 假设系统处于3副本状态
- 开始水平扩容
- 系统自动新建一个副本,并与之前的副本建立任务关系(源和目标),此时进入4副本状态
- 建立任务关系的两个副本之间自行进行数据copy(比如创建snapshot后,copy snapshot或者逐条copy key)
- 删除源副本,又回归到3副本状态
- 即3副本->4副本->3副本的过程
异常处理
- 副本之间的copy可能会失败,又回归到原始的3副本状态。因此在copy过程中,所有的数据仍然需要写入到旧的副本上(源副本)
- 由于copy过程中,旧的副本人仍然在写入,在旧副本退出之前需要做额外的检查工作,确保旧副本上的所有数据都已经进行迁移(优雅退出)
- 3->4->3后,API侧需要及时感知分布的变化,将请求发送给新副本。我们可以在每个读写response中返回当前shard的最新分布信息(由存储层返回,而非metaserver)。当API对比发现分布发生变化时,及时去metaserver中进行更新即可。
- 需要注意,EC模式时每个副本上的数据不一样(key相同),因此源端和目的端必须一一对应(与3副本模式有所区别)
总结
回顾整个实现过程,我们从一张简单的MySQL表开始,通过13天时间,逐步构建出整个对象存储系统,并论述了各个模块的设计取舍,实现了除分段上传以外的其他功能。分段上传的具体实现不再赘述,读者可以自己思考并实践。
此外,还可以思考下面的问题:
- 如果将一张表的shard数量设定为1(而不是上文的2233),并且对外提供建表API接口(假设每次调用平均延迟100ms),此时存储后端读写逻辑与HDFS的异同(注意key只需要table内部唯一即可)。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/18580.html