如何设计一个高性能的块存储(EBS)

如何设计一个高性能的块存储(EBS)Google、微软、阿里云等等都随之入局。上文A Bite os S3 Arch 谈到了如何构建分布式存储系统来支持大规模的S3对象存储问题。

欢迎大家来到IT世界,在知识的湖畔探索吧!

1. 云计算存储发展

2003年Google 发表 paper GFS,揭示了其解决其索引这个星球巨大规模的互联网数据的存储问题,2006年Amazon推出划时代的AWS云计算服务EC2和S3,开启了改变世界IT格局的云计算时代。Google、微软、阿里云等等都随之入局。

上文A Bite os S3 Arch 谈到了如何构建分布式存储系统来支持大规模的S3对象存储问题。而云计算还有很重要的另外2大基础存储基础设施组件EBS(Elastic Block Storage)、EFS(Elastic File Service),对应于传统IT基础设施中的本地盘和共享文件存储业务。

对于云计算来说很重要的一点是超售和弹性,所以支撑EBS和EFS方案的底层基础存储层的支撑也不太可能是local的本地化方案,必须是分布式的存储资源管理和分配系统。

但是相比于S3业务,EBS底层的分布式存储系统对外提供的IO模型其实非常的不同。 S3对外基本提供接口能力为 PUT/GET/DELETE,对底层分布式文件系统的需求是Append。而EBS、EFS 对用户是需要支持随机地址空间的改写,那么如何设计底层存储系统来支持这2类业务?

2. 现状

对于这块的业务架构,当前各大互联网公司基本没有开源,基本的架构也没有或者很少正面出来说的。这一方面可能算是商业机密,另一方面也可能是从安全和舆论等层面的考量。

当然开源系统和一些互联网公司的存储系统的paper是可以参考的,比如CEPH、Lustre、HDFS、GlusterFS、FastDFS、Window Azure Storage、GFS等等。这些都自称为分布式文件系统,但具体到场景又有很多不同,对文件的定义也是各异,如下几大方面是非常的不同点

  • 是否支持Append/OverWrite: 比如HDFS文件只支持Append不支持OverWrite。而Ceph Rados 对象是可以支持随机地址空间的改写(PS:所以当前openstack 块设备 这块基于ceph的RBD是很重要的支撑)
  • 持久性保障(Close/Sync / Write):HDFS类似于本地文件系统Posix语义,不保证文件每次写入操作的持久化,只有上层调用Sync或者Close之后才保障数据持久化写入。对于CEPH 的存储基层RADOS的每一次写入都保障数据的持久化。
  • 一致性模型(Consistency-Model):HDFS、CEPH基本都保障多个副本字节级别的一致性,而GFS不保证多个副本Byte级别的一致性,而仅仅保障Record的一致性(详见参考文献文献4)。
  • 并发性(Concurrency):HDFS 支持single-Writer 和 MultiReader的一致性。而貌似大多数支持随机地址空间读写的文件系统,只支持single-writer和singleReader的一致性保障。

EBS底层的分布式存储系统,需要至少保证如下几点

  1. 读写模式:需要支持OverWrite。
  2. 持久化保证:块设备的每一次写入都需要持久化保证。
  3. 并发性:需要保证single-writer 和single-reader的一致性保证。

EBS底层分布式存储系统需要向上提供如上的几点重要的特性之外,还有很重要的一些核心需求

  1. 高可用:EBS是在基层设施层面属于最底层的支撑,EBS的稳定性对业务的连续性至关重要,所以可用性极其关键。一方面是在各种故障(比如进程重启、磁盘故障、机器故障等情况下)要对上层的业务的抖动要足够的小;另一方面在各种上层业务混杂情况下需要保证系统的性能稳定性。
  2. 高可靠:可靠性不言而喻,云盘的可靠性不能比本地磁盘的坏盘率高,比如AWS声明其块设备的可靠性相比传统的本地磁盘高1到2个数量级(0.1%~0.2%)也就是1000块EBS每年故障1~2块。而国内云计算厂商,比如阿里云、腾讯云等更是声明其可靠性超过11个9。
如何设计一个高性能的块存储(EBS)

那么如何设计和架构EBS底层的分布式存储系统来满足业务的需求,也就是如何设计向上层业务提供随机IO能力的分布式存储系统。下面分析下几种可行的构建方式。

3 EBS 构建

3.1 构建方式一

对于EBS业务来说,抽象上来说需要底层分布式文件系统提供可随机读写的文件的能力,按照最直接的方式,可以有如下图所示的逻辑映射关系,1TB Vdisk /dev/sad对应于底层分布式文件系统为 /foo/sda 这样一个文件,1TB地址空间上的chunk是按需分配的,分配的基本单元一般来说是固定的,如下图的4MB。

如何设计一个高性能的块存储(EBS)

qemu等提供了对接块设备driver的API,只要在client一侧对应分布式文件系统对接实现这样的API即可向上输出块设备。

在网易从0到1实现了这种构建方式CURVE 目前已经开源(百度也是类似这种方案),底层抽象为一典型的分布式文件系统,向业务输出随机可读写能力的文件以支撑上层块设备EBS类型业务。基本架构类似于经典的分布式文件系统的鼻祖GFS,如下图所示。

基础架构

如何设计一个高性能的块存储(EBS)

  • Master负责:
    • 存储管理整个系统的topology信息,比如pool、zone、server、chunkserver等
    • 存储管理整个文件系统的Namespace(树形目录结构,文件&目录,目录元信息等)
    • 存储管理file对应的chunk信息,以及chunk所谓位于的copyset信息
    • 存储管理file级别的快照
    • 存储管理复制组CopySet(特定chunkserver组成的复制组, ps:复制组的概念可参开博文分布式存储系统可靠性)
    • 对chunk数据合理的放置(placement),实现负载均衡和预定义的数据可靠性
    • 管理ChunkServer的上线下,收集ChunkServer的负载信息,对存在复制关系的chunk进行配置变更
    • 不同租户之间对元数据的QOS控制
    • 等等
  • ChunkServer负责:
    • 存储系统Data(数据层面)的读写(Data Path)
    • 复制单元之间的数据复制、多副本数据一致性
    • 数据校验,静默数据损坏检查
    • 坏盘情况下的数据恢复(结合Master)
    • 不同优先级IO之间的优先级控制(恢复IO、用户IO)
    • 磁盘负载(用量、负载)等信息的上报
    • 等等
  • Client负责:
    • 向Application提供类Posix文件系统接口
    • 与MDS交互实现对元数据的增删改查,快照
    • 与ChunkServer交互实现对数据的增删改查
    • 对IOPS、带宽进行指定的QOS控制
    • 等等

数据面核心

底层核心ChunkServer之间是基于一致性协议(Raft)+stateMachine的方式实现副本一致性,详见Raft peer Replicated State Machines 章节。所有的上层用户的写入依赖于一致性协议实现多副本的一致性保证,每个用户的Chunk被分配到这些复制组中。

如何设计一个高性能的块存储(EBS)

如上图所示

  • 分布式存储系统将存储池中的磁盘按照故障域划分成诸多复制组(figure-1),一个个复制组即一个个Raft实例(figure-2)(PS:复制组(CopySet)即约束了存储池中有副本关系的组合, 包含一个数据的所有副本数据的设备组合,比如一份数据写入1,2,3三块盘,那么{1,2,3}就是一个复制组。)
  • 对于EBS来说,当给用户分配一段地址空间的时候,即选择将这个chunk分配到某个复制组中(持久化记录在Master),数据面的IO通过复制组对应的一致性协议(比如Raft)实现多副本的一致性写入和读取。(figure-3)

此种架构方案优点:

  1. 简单:首先架构层次相对比较简单。client、master、chunkserver各司其职,直白的按照逻辑来实现,没有啥冗余的组件。
  2. 对MDS依赖较小:异常情况下对MDS的依赖相对来说比较小,数据面的选主之类的基本依靠各个底层的组件执行(坏盘情况下的配置变更由MDS进行统筹计算)。假如设计成类似CEPH 的HASH寻址方案可以基本在IO路路径上完全摆脱对Master组件的依赖(在复制组关系缓存在client的情况下类似CEPH的PG MAP)

此种方案的缺点:

  1. 复杂:从盘的坏盘和快速恢复层面考量,一个磁盘的数据会足够打散使得在恢复时有足够的并发度(详见:分布式存储系统可靠性),这会造成一个磁盘会有上百个复制组(上百个Raft实例),往往一个磁盘有一个服务进程,所以需要设计优秀MultiRaft组件(性能优秀、保重各种case情况下足够高的可用性和可靠性),虽然Raft协议的spec已经使得一致性协议的实现在理论上打下了扎实的基础,但是工程上的优秀实现依然充满挑战(opensource角度,基于brpc实现的c++版本的braft 是一个不错的选择)。
  2. 数据热点:EBS向业务承诺了基本的IOPS保证,但是在一些极端情况下上层EBS产生IO的chunk 落到了个别的磁盘上(比如一块盘的IOPS能力为1w,规格为IOPS 1000 的 20个EBS实例的IO恰好落到了其中1块盘上,这就导致了IO热点,无法向用户提供应有的IOPS能力保障),这种绝对寻址的架构设计方式核心上来说很难解决这个数据热点的问题。
  3. 多副本/EC:由于采用了Raft类似的一致性协议框架,要在这样一套框架上去拓展EC相对来说是比较复杂一些。一般来说可以按照chunk粒度实现类似多级缓存的机制,比如将访问上不是很热的chunk,下沉到S3系统等等,也算是一种可行的方式。
  4. 快照:快照方案较为复杂,需要结合文件的多版本以及每个Chunk实现COW的机制来实现快照。即在StateMachine层面是引入了较多的业务层面复杂性。原地改写,总体来说原地改写会使得在系统的实现复杂很多,特别是想引入一些高级特性的时候,如快照克隆等(PS Innodb 相比 leveldb复杂很多)

3.2 构建方式二

如上构建方式1是从0到1构架设计和实现。第2种构建方式主要是跟圈内的童鞋交流方案,细节无法进行的考究,但是从总体架构层面进行说明和分析没有什么大的问题。

其基本架构类似于Window Azure Storage的架构体系。在底层构建Stream Layer,在Stream Layer(类似GFS)的基础上构建Partition layer,在Partition Layer(类似BigTable)的基础上可以构建EBS方案。

如何基于这样的架构支持业务,需要换一个角度来思考如何解决这个问题,回到问题的出发点,EBS随机地址空间读写的方案核心是解决什么问题?

块设备一般来说提供了原子粒度的IO保障,比如磁盘一般来说保障512Byte的原子读写;一般SSD提供4KB级别的原子读写特性。

简单来说,块设备提供了对每一个地址空间(比如4KB)的原子写入和读取(PUT/GET)。换一个角度来说如果对每一个EBS实例的每一个4KB内容定义一个全局可编码的唯一Key,其内容为4KB的固定大小的Value。那么这个底层分布式存储系统就是一个专用的分布式Key-Value系统。

构建方式2基本就是遵循这样一个思路来构建的。

如何设计一个高性能的块存储(EBS)

如上为基本的逻辑视图示例,底层的物理层大构建是如下图所示。

如何设计一个高性能的块存储(EBS)

StreamLayer:类似GFS/HDFS层次,每一个stream 类似一个文件,stream 只支持append。stream 由一个个extent组成(类似 HDFS/GFS 的chunk),extent使用固定复制组的方案实现多副本/EC(参考微软PacificA 协议)。extent支持可变长度,在extent 不可用的通过seal/new的方式新建一个extent继续提供写入,保障写入的高可用。

Partition Server:提供table的某个区段(按照字典序分裂)的kv 存储服务(增/删/改/查)。其通过WAL实现数据的快速和及时写入,保障数据的持久性;然后写入MemStore(SkipList跳表实现),在MemStore达到固定大小,比如128MB的时候Dump成SSTable(Sorted String Table),Block Cache 负责SStable 热点数据的读缓存。一个Partiton Server 负责多个Partition。

架构方面的更加具体的参考Window Azure Storage 的paper或者BigTable开源实现方面的书籍和资料,这里不多做展开。

在实际操作层面,对于一个上层的EFS实例,有挺多的映射关系:

  • 一个EFS实例对应底层一个table的一个Region
  • 一个EFS实例对应底层一个table的多个Region

一般来说为了实现EFS的快照和克隆功能,一个EFS实例一般都选择一个table。如果是只有一个WAL,写入的吞吐容易受影响,可以在上层使用一定的间接的业务逻辑手段使得多个相邻的地址空间换分到不同的Region,从而把上层IO映射到更多的Region,使用多个WAL写入能力提升带宽。比如将地址空间按照1MB取模划分到4个独立的连续字典序区间Region。

此种构架方案的优点:

  1. 架构层次分明:Stream Layer 确保数据可靠性可用性(多副本/EC 模式)等。PartitionLayer 解决的更多的是业务逻辑问题。将多副本的复杂性收敛在Stream Layer 而不扩展到PartitionLayer,架构层面很好的正交和解耦。
  2. 快照/克隆:由于都是Append 写入,不涉及任何数据的原地改写,快照克隆方案基本类似于HBASE的快照克隆方案;快照克隆基本也是个通过安装元数据生成对应的Manifest文件的方式完成(不涉及到任何数据的Copy过程),克隆也是非常的简单,只涉及到只读基线数据的共享。可以说非原地改写的设计对于快照和克隆具有极大的友好性,使得快照克隆方案极其简单,不宜出错。
  3. 数据热点: 在极端情况下,多个虚拟盘的IO持续怼一个物理盘的情况下,可以通过 Seal-New(参看Window Azure Storage)实现快速转移,读取由于可以结合Partition Layer的内存缓存和底层三副本机制,一般来说不太容易成为瓶颈。
  4. 隔离:Proxy 设计方式使得进行带宽/IOPS隔离相对来说比较简单(这种隔离对于构建分布式NAS多挂载点的隔离方案来说显得非常友好)。
  5. 空间超售: 由于所有的写入都是新Extent的写入,并且可以通过seal-new的方式使用新的空闲空间,从这个角度来说,可以实现非常高的超售比例。


反脆弱:这是这种方案我认为最最大的优点,也是相比方案一的绝对杀手锏,很多分布式存储系统其实从 基层层面都没有好好考虑这个问题,或者到意识到这种方案的巨大优势。即从读层面WAS基本实现了 已经seal 数据的任何副本是可读的。对于写数据来说,由于写入只限制在当前 尚未seal 掉的chunk上,也就是3个磁盘上,并且在故障情况下随时可以seal掉转移。这两点使得虽然 上层一个table的数据打散到底层这么多磁盘上。但是从爆炸半径来说,基本上算是实现了本地盘方案的爆炸半径,甚至在故障,过载情况下可以实现快速转移。从这点上来说,这样基本为分布式存储方案去完全干掉本地存储方案,打下了极其坚实的基础。

此种构建方案的缺点:

  1. IO离散化:连续地址空间的IO放大。比如128KB的大IO。如果底层层面都是4KB级别打散,在常规设计下PartitionServer需要与StreamLayer进行32次交互才能够拿到全部的数据(当然可以并发)。总归对底层磁盘的IOPS层面的开销是比较大的。
  2. 读效率:从读层面,需要走一遍Proxy(Partition Layer)-> Stream Layer,相比于构建方案一多一次网络RTT。(PS:由于SSD性能的提高,传统TCP/IP网络越来越成为瓶颈,国内阿里云等一些厂商都慢慢都在采用RDMA网络方案降低网络层面的延迟,这在一定程度可以降低影响。另外Partition Layer 跟Stream Layer 在WAS中也是亲和部署,走本地通信的方案)
  3. 慢盘效应:stream layer 采用主从协议,那么不可避免的受到慢盘的影响会比较大,而Raft协议会采用3副本中最快的2副本来屏蔽慢盘的影响。按照盘性能zipf分布的特性,3副本全写性能一般来说会是单盘平均IO延迟的1倍,而类似Raft的多数派方案相比单盘平均性能反而会降低一倍,详见数据存储中的Zipf分布。
  4. IO放大:由于底层采用LSM Tree的方式,LSM天然会引入读放大。当然由于所有的Key和Value可以认为是等长的,可以考虑对底层所有的key抽离最最细粒度的索引,开销合理情况下可以考虑把所有索引都放入内存。(PS:内存是否放得下?比如基本可以使用变长整数的方式,使用48byte索引4KB,这样4*1024/6 = 682:1 也就是68TB对应100GB内存。 1TB 20盘位情况下,29GB内存基本够(最坏情况下的索引机制,全部随机打散),如果底层分片没这么碎的话会小很多。全内存索引的构建可以采用类似HDFS的Fsimage+editlog的方式实现。
  5. Compaction&数据利用率:依靠Compaction的机制来解决垃圾回收问题(ps:垃圾即覆盖写数据),Compaction过程中的读写放大一定程度会影响用户IO。另外垃圾回收滞后,会导致空间利用方面的问题。
  6. 复杂:组件相对来说比较多,涉及工作量会比较大,开发周期会是长长的RoadMap,Proxy模式下如何设计获得更高的可用性和性能相对来说比较有挑战。

3.3 构建方案三

构建方案3类似方案2的变种。核心的不同点在于,构建方案2的全局排序和垃圾回收工作依赖于后期的compaction。而构建方式3倾向于尽可能块得进行原地排序。

其基本原理是,通过WAL 写入确保数据持久性,然后应用到内存。后台定期将写入原地应用到随机读写文件(没错,底层除了需要实现AppendFile,还需要实现弱一致的随机读写文件RandomFile,多幅本的数据一致性由Client负责,这引入一个缺点,随机读写文件很难做EC),WAL使用seal-new的方式确保写入可用性,读取采用Merge 更新数据(wal) + 基线数据(随机读写文件数据)的方式实现数据的读取。在内存中建立对整个WAL内容的索引,最新的数据在WAL中则直接从WAL中读取,否则从随机读写文件中读取,其基本读写流程图如下

如何设计一个高性能的块存储(EBS)

写入:写入还是一样写入WAL,然后更新内存索引,将最新数据的使用MemTable(Hash表)指向WAL。但是最终结果的Dump其实并不是按照sstable的方式,而是直接后台异步dump到对应RandomFile的随机地址空间。

PS1:WAL 也是类似Segment方式,定期做checkpoint(刷入RandomFile),在内存中会构建所有WAL segment的索引,WAL采用Seal/New的方式来保障可用性。

PS2: RandomFile是随机读写文件,为了保障WAL长度的收敛性,后台线程会将WAL数据写入RandomFile。RandomFile 没法使用Seal-New的可用性,但是由于是BackGround的写入。所以如果发现一个副本失败,会使得BackGround dump机制阻塞(这种方案在坏盘恢复场景下,会导致BackGround写入会卡很久)。

读取: 读取会首先通过Memtable查找最新数据是否在WAL中,不存在的情况下再读RandomFile。对于RandomFile未达到一致性状态的数据,会从WAL里头进行读取。(PS:由于WAL(AppendFile) 到 RandomFile的写入是全三副本,并且一直是卡着的,所以RandomFile的恢复可以采用很鲁棒的方式,直接选中任意一个还在复制组中的副本作为数据源复制。)

这种方案可以一定程度解决。读放大、空间利用率等方面的问题。但是在克隆、快照等方面会稍微差一些,并且随机读写文件很难做EC(EC改写好麻烦)。

4. 总结

如上对3种构建方式进行了简要的构建和优缺点的说明。没有绝对的那个构建方案优于另外一个构建方案。在实践过程中应结合自身面向的实际业务特点和技术条件等进行选择,能快速跟上业务发展并且对方案做中长期的评估适时进行调整。

很重要一点是一定得进行抽丝剥茧、形而上得分析问题的本质。这样才能够把精力集中在真正的问题上,好钢放在刀刃上。

简单来说,块设备提供了对每一个地址空间(比如4KB)的原子写入和读取(PUT/GET)。换一个角度来说如果对每一个EBS实例的每一个4KB内容定义一个全局可编码的唯一Key,其内容为4KB的固定大小的Value。那么这个底层分布式存储系统就是一个专用的分布式Key-Value系统。

以上不同的方案,从逻辑上往上抽象都可以认为是向业务提供KV存储。最本质的区别可以认为是排序在什么时候进行。构建方案1总体来说是即刻全局排序。构建方案2 则采用局部有序的方案。而架构方案3则 使用架构方案1和方案2 的折中。

另外一个很重要的差别是从核心可靠性的角度来说,构建方案1采用了多数派(如Raft)方案应对分布式的CAP问题。而构建方案2、3采用PrimayBackup+Master(多数派)+ Append Only的特点来应对CAP问题。

Notes

作者:网易存储团队工程师 TOM。限于作者水平,难免有理解和描述上有疏漏或者错误的地方,欢迎共同交流;部分参考已经在正文和参考文献中列表注明,但仍有可能有疏漏的地方,有任何侵权或者不明确的地方,欢迎指出,必定及时更正或者删除;文章供于学习交流,转载注明出处

5 参考文献

  1. Raft Paper:https://raft.github.io/raft.pdf
  2. 分布式存储系统可靠性
  3. 数据存储中的Zipf分布
  4. A Bite os S3 Arch
  5. GFS
  6. HBASE原理与实践
  7. Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/40736.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信