paper gfs 2003
经历了一次甚是吊诡的新年,肺炎盛行,春节假期延长,幸运的是这次的冠状病毒致死性并不强,虽然赶上了春运的扩散,好在反应及时,应对及时,开始朝着乐观的方向前进了。感受到了外卖送不进小区,感受到了口罩,消毒水断货,没有物流的供应,没有及时的外卖送达,没有集会,所有景点全部关门,根据籍贯开始清查,所有公共交通,整个世界的停转,光怪陆离的2020拉开序幕。
那么言归正传,GFS被设计为分布式水平扩展的文件系统,在便宜的x86服务器集群中提供自动容错能力,高并发读写的扩展性能力,设计源自于多年来的工作负载和技术环境积累,开始探寻不同于传统文件系统接口的设计,目前在google内部部署很多,使用广泛,被证明了是一个成功的存储系统,
介绍
性能、扩展性、可靠性、可用性是gfs设计基本准则,不同于传统文件的设计,启发来自于目前日新月异的工作负载和新的需求,总结出以下设计目标,第一,组件失效是经常发生的,不同于传统商业存储系统使用大量商用硬件保证可靠性,出错原因众多,比如磁盘,网络,电源,天灾人祸等等,因此系统需要做到监控,错误探查,自动恢复,容错;第二,文件很大,xGB大文件很多,同时每个文件包含众多doc,考虑到文件大小和访问模式,block size需要重新设计;第三,大多数文件写少读写,多数写一次然后多次读取,且读写都有模式,顺序读和追加写为最常见的模式;第四,重新设计文件api可以提供更好的灵活性,gfs简化了一致性模型的同时没有给应用开发带来负担,提供原子record append来保证并发写性能和一致性。目前的规模为1000s节点共计300TB存储空间,100s客户端并发访问。
设计参考
关键假设
- 便宜可得的x86服务器代替商用硬件,容错和自动回复很重要
- 文件数目需要可控,最多百万级别;文件大小需要设计,需要>100M级别,GB数量级文件占主流,小文件也支持,不过不做优化
- 负载主要包括,流式顺序读和极少的随机读,每次读>1M,应用需要进行一定程度的聚合读来提升性能
- 负载主要包括,追加写,写的大小需要>1M,写入后更新很少,更多的操作时读取和删除,支持少量的随机写入,但性能不做优化
- 同一个文件并发读写,需要保证一定程度的一致性
- 吞吐导向优化,而非延迟
接口(不兼容posix)
- open、close、read、write、snapshot、 record append
架构
- 单点master+多chunk servers,每一个节点为linux系统服务器,系统跑在用户态下,client和chunk server可以同机器部署
- 文件切分为chunk,chunk固定大小,64M,chunk id 64bits唯一标识,chunk server存储每个chunk对应linux系统中一个文件,每个chunk三副本(默认,用户可以指定)在多个chunk servers中
- master维护所有元数据,包括目录,文件,访问权限,文件到chunk的映射表,当前的chunk和chunk server对应关系,租约控制,GC,chunk迁移,负载均衡,和chunk server之间的心跳
- client实现file api,和master交互得到元数据信息,数据相关流动直接和chunk server交互
- client和master均不缓存数据,减少设计复杂性,不需要考虑缓存一致性问题,性能损失可控,因为负载大多数为超大文件中的顺序流式读,chunk server利用linux的buffer cache进行缓存
单点master
- 单点设计master大大简化的架构,master拥有全局信息,可以做负载均衡,同时为了避免单点瓶颈,减少client和master的交互,数据流都直接和chunk server交互
- 读操作,1 根据文件名,和offset以及固定大小的chunk,得到文件名和chunk index发送给master,2 master返回chunk servers,clent缓存这些元数据,缓存结构文件名+chunk index作为key,value为chunk handle 3 client发送读请求到最近的replicas,指定chunk handle和range,下次读取同样chunk不需要和master交互,直接文件重新打开或者缓存到期失效,一般来说,client单次请求会跨chunk,此时和master的一次交互就可以获得所有chunk信息,减少了master成为瓶颈的可能性
chunk size
- 根据负载,最佳值选择了64M,采用惰性空间分配减少浪费
- 优势,1 大chunk减少master交互,大的chunk使得操作在同一个chunk中的概率增加了,2减少网络开销,3减少master元数据大小,保证元数据全部在内存中,保证了性能并减少master成为瓶颈的概率
- 缺点,hotspot,小文件场景下大并发访问同一个chunk造成瓶颈,索性顺序读的时候这种场景不多,解决办法就是这种文件,提高其复制因子,多副本下读扩展性就上来了
元数据
- master主要存储三类元数据,第一,文件和chunk名称空间,mapping files和chunks,每个chunk在哪个chunk server上,所有元数据都在内存中,前两者使用op log和周期性checkpoint技术持久化在master和remote存储中,第三个信息随心跳上报,master不持久化
- 内存数据结构,操作速度快,后台可以周期性检查状态进行GC,chunk迁移负载均衡,缺点是内存大小会成为瓶颈,目前64Mchunk需要64bytes元数据存储,因为假设了都是GB级别的大文件,因此大多数chunk是满的;同时目录结构采用前缀压缩方式减少内存占用。单点master元数据都在内存也是一个设计权衡,这样的设计极大的简化了实现难度,同时保证了性能,稳定性,是一个优秀的 trade off
- 通过和chunk server的心跳信息,来维护chunk位置信息,如果在master持久化上述信息,我们发现难度较大,很容易出错,chunk server频繁的出错,重启,恢复等等,维护一个一致性视图总是艰难的
operator log
- 维护了gfs元数据的持久化信息
- 维护了logic时间线,保证并发操作串行化有序执行,files and chunks,都包含version
- 所有操作都需要元数据持久化本地和远端完成后,才对client可见
- flush 内存checkpoint做了细节的设计,保证不中断IO
一致性模型
- 目录文件的创建和其他操作保证原子,单点master和全内存操作,适当的锁保护可以做到并发操作的有序性,因此做到一致性
- 文件并发写同一个offset,成功有一致性但是结果未定义,失败无一致性
- 文件并发record append,成功有一致性且有定义,失败无一致性,保证至少一次语义写入,会有pad或者重复的record,使用chunk version检查chunk的数据是否都是最新的
- client会缓存元数据,因此在缓存有效的时间窗内,依旧有可能读到老的chunk,或者心跳尚未更新到master时
- 磁盘失效造成的数据损毁,会有定时的后台检查,checksum机制,此时master会从其他副本重新得到新数据,自动均衡三副本
客户端编程
- 需要依赖record append而不是update overwrite等等,需要写入的数据有自校验性和自识别性,包含checksum,seq number等等
- 先写临时文件,使用record append,然后原子重命名为新文件,对外可见
- 写入过程记录checkpoint,用户级别checksum,直到全部数据写入成功
- 用户需要处理重复和padding问题,pading问题使用每个record一个checksum可以检测出来,使用序列号,来检测重复的record
系统api交互
租约机制和全局修改顺序
- 每一次修改定义为对数据或者元数据的改动,每一次修改都需要在三副本chunk server中修改完成,使用租约技术来保证三副本之间的一致性,master会决定三副本中哪一个为主要副本,并赋予其租约,主节点对所有修改增加序列号,保证三副本的一致性,全局修改顺序由master选出的在租约期间内的主节点产生序列号完成
- 租约机制主要是为了减少client和master的交互,将写入的权限从master下放到三副本chunk server的主节点,租约默认有60s的生命周期,只要chunk一直在修改中,主节点可以通过心跳向master节点无限续租,master有权限在租约没到期就将其回收,比如当master需要取消某次修改操作时,即使主节点和master断链,master依旧可以下发新的租约到其他节点,当旧的租约到期之后
- 写操作:client向master获得chunk当前租约信息以及复制集信息,master负责选择出主节点;master回复复制集信息,以及租约信息,client缓存上述信息,直到主节点不可达或者主机点返回租约到期;client将数据分别写入三副本,此时在内存buffer中,LRU cache结构(数据和控制指令解耦,走不同的流程,大幅度提高性能);所有复制集数据写完后,client向主节点发送写入请求,主节点分配唯一递增序列号,先在本地完成写入;主节点发送请求到复制集其他节点,要求按照序列号线性写入,所有复制集都完成时,主节点返回client写入成功;如果有任何失败,都会返回client,主节点失败,复制集其他节点不会执行,数据是一致的;复制集某个节点失败,主节点不会对外可见本次修改;
- 为了尽可能利用带宽资源,数据流动根据网络拓扑,client优先向最近的发送数据,同时会有流水线来利用起来全双工的网络,增强数据在数据集内部的流动速度
- 由于系统中存在大量并发追加写,如果不在gfs中实现,多并发client需要依赖外部同步比如分布式锁才能完成这个操作,gfs单独优化了record append保证一致性,和上述写操作不一样的是,如果追加发现64M的chunk剩余空间不够,会先做一次三副本的统一padding,然后告诉client重试下一个chunk写,record必须有1/4 chunk size的大小保证了空洞不会太多,如果chunk空间足够,会在写入成功后将offset返回client,注意会有空洞padding或者重复数据,在client重试的场景下,按照前文所述的方式进行client规避即可
- copy on write的snapshot机制(文件或者目录级别),对chunk增加引用计数,首先回收所有租约,保证chunk有修改得时候会先找master请求获得租约,给master重新复制一份chunk创造时机;
master操作
master执行所有名字空间的请求,chunk的复制,决定新chunk创建位置,负载均衡,GC旧chunk等等
- 为了高并发性能,在snapshot或者其他耗时操作中不阻塞主要流程,master通过细粒度的锁控制,来提高并发的同时,保证并发操作的串行临界区,和传统fs不同,gfs中map靠全路径映射到元数据信息,没有软连接和硬链接,inode的概念,直接为每一个元数据区目录结构分配读写锁即可,当在指定目录下操作指定文件,需要依次获取递归目录结构中的读锁,以及全路径的读或者写锁,取决于具体的操作;通过精度控制锁,可以保证一致性的同时或者高性能
- 复制集位置分配,多机架,不同机器,不同机房等等策略,提高可用性,充分利用带宽资源
- chunk的创建,重新复制,负载均衡迁移,创建位置分配,参考磁盘利用率,当前正在创建的数目,资源利用等等
- GC,文件和chunk层面的惰性回收资源,标记文件删除,重命名为隐藏文件,周期扫描中发现回收隐藏文件超过3天,将其彻底回收,元数据销毁,并消减chunk引用计数;chunk扫描中,找到引用计数为0的chunk,先清理元数据,chunk server之后可以清理掉chunk释放物理资源
- GC相比于及时删除的优势是,保障了可用性,同时清理数据的统一路径,将删除具体操作从前台转移到后台,尽可能的减少IO影响,主要的劣势是有的时候用户想要删除释放资源,这个可以用过指定立即删除来保障
- 旧chunk检测,有的复制集曾经失效,因此数据变老,此时需要保证client不要读到过期的chunk,master为每个chunk维护一个version字段来区分最新的和过期的chunk,当master分配新的租约时,会更新version并通知所有最新的chunk,master和chunk server都会持久化记录这个最新的version,如果此时一个chunk失效了,他不会收到这个最新的verison,当该chunk server重启并联系master发送version时,master会发现version落后,将其chunk标记为老的chunk,后续会被GC
- 为了保证一致性,master会把version发送给client。client读数据时也会将version发送给chunk server,多重校验保证client不会读到stale数据
容错和诊断
用快速恢复和复制集来保障高可用
- 快速恢复,master和chunk server设计为秒级别重启,不论他们如何终止
- chunk复制,每个chunk多台机器多个机架放置多副本
- 主节点的复制,op log和checkpoint都会复制到多台机器,机器挂了,通过DNS访问备节点提供可用性,同时提供影子节点提供读可用性,会落后主节点x秒
- 数据可用性,checksum技术,每个chunk server需要定期检查自己的chunk,注意record append允许不同的复制集chunk不完全一致,因此没办法复制集之间checksum,只能单独校验,这里是个很大的遗憾
- chunk被切分为64KB大小的block,携带32bit checksum,持久化在log中,和用户数据分开存放,每一次读取,都会做校验
- gfs servers提供基于事件的诊断日志,server up and down,rpc request and response等等,用来诊断系统,重放负载等等都很有用
性能和经验
一开始gfs设计使用在生产环境,后来开发和实验环境也需要部署,生产环境支持相对容易,开发和实验环境,就需要整套的权限控制,quotas等等控制系统,支持的系统多了一周,发现了一些驱动和linux内核相关的错误。
相关工作和结论
通过对工作负载的观察,进行了很多针对性设计,record append以及顺序读,大文件而不是海量小文件,读多写少,数据和控制分离,master单点瓶颈通过精心设计来规避,最终在指定的负载下完成了水平扩展性,并具备容错恢复能力