前言
gfs是谷歌研发的一套非常经典的分布式存储系统,当年的论文对后续分布式存储的发展有非常重要的指导与借鉴意义,影响深远。实验室参与开发的冷存储系统当时也借鉴了其中的一些思想。关于gfs论文的分析也非常多了,本文不打算重复太多的gfs原理,而是尝试把一些分布式存储系统通常关注的要点分类整理出来,简要地一窥其全貌。
应用场景
分布式存储系统必定是面向一些特定的应用研发的,其系统特性必定针对应用场景做了权衡与折衷,不可能各个方面都尽善尽美。先来看看gfs面向什么场景。
- 基于大规模的廉价的消费级服务器或PC机组建,因此机器宕机、网络故障是常态,系统的容错性非常重要,必须有完善的系统状态监控、数据完整性检查、数据重构恢复等手段,数据的分区容灾也要考虑。
- 存储大文件,文件的分片与block size等的设置就必须仔细考虑。
- 主要是顺序读写文件,写文件也一般是追加的方式,极少随机读写。
- 存在多客户端并发追加同一个文件的场景,要保证这个场景下的一致性。
- 读写需要高吞吐,对多客户端并发读写多个文件的吞吐量有高要求,对单个请求的响应时间没有太多要求。
- 上层应用能够容忍一定程度的数据不一致,包括读到过时数据,多次读有一定概率读到不一样的数据等情况,应用程序有一定办法容忍或解决这些小概率问题。
元数据管理
元数据用master节点管理,在逻辑上与数据节点分离。单一的master结构管理方便,开发简单,适合提升系统的一致性。其实很多系统的元数据都是分离管理的,有单独的元数据集群来维护,这个设计本身没有特别之处。
为了保证性能,提高读写的响应时间与吞吐量,gfs的master做了以下几点工作:
- 元数据在内存中管理,单机读写性能高,尽量消除单点造成的性能瓶颈。
- 文件系统的接口为了性能,牺牲了一部分通用性。所有文件与目录以key-value的形式组织,key是文件目录的全路径。构建了逻辑上分层,实现上扁平化的元数据存储,使得读写元数据效率非常高,通常只需要一次put和get请求。但是也牺牲了ls和mv等通用文件系统操作的性能,上层应用的使用场景有一定限制。
- 管理每个文件的chunk列表。由于gfs面向大文件,文件分片的size可以设得足够大,一般是几十上百M的量,因此大大减少了需要维护的chunk列表长度,使得master的单机内存足以支撑海量文件的元数据存储。
- 由于master存在单点故障的问题,因此一般会维护一个master集群,够成单leader,多slave的结构。写入时由leader转发给各个slave,但所有节点都可以响应读请求,加速了读取元数据的性能。
- client会缓存对于某个chunk的chunk server列表,减少了与master的交互。
- master不负责存储chunk到chunk server的映射,而只是负责file到chunk的映射。在chunk server启动或者加入集群的时候向master汇报该映射信息。这个设计简化了master对元数据的维护。
数据一致性
gfs的一致性为了性能作出了一定的让步,同时它既不是一个CP系统,也不是AP系统,而是C与A都作了折衷。分析gfs的一致性,可以分成master和chunkserver两部分分类讨论。
master一致性:
- master集群只有一个leader,采用全序广播的方式把请求发送给各个slave。所有请求都在master里通过operation log进行排序,且只有所有节点都写入成功,才向客户端返回成功。因此元数据写入不存在写冲突的情况。但任何一个节点挂掉,写入都会失败。
- 对元数据的读取不仅仅从leader上读,所有master节点都可能收到读取请求,由于存在replication lag,在slave节点可能会读到过时数据,因此不能满足线性一致性的要求。应用程序必须能容忍读到过时的元数据的风险。
- 元数据加锁处理,不会存在多个客户端并发访问的冲突情况。
chunkserver一致性:
- 对每一个chunk,master都会指派多个chunk server去负责存储,默认是3副本。master会从多个chunk server中选出一个作为primary,并赋予chunk version id,利用心跳租约机制来监视primary是否存活。client在拿到chunk server列表之后,会缓存起来,并向所有chunk server发送数据。其中数据的顺序是由primary来决定。因此chunk version id和primary决定的顺序就保证了所有节点接到的请求都是有序一致的。如果没有错误发生,所有节点数据满足一致性。
- 当primary宕机或网络故障,master到primary的心跳就断了,此时master会等到心跳过期以后,指派另一个chunk server作为master,并更新chunk version id,这样通过该租约id就可以发现那些chunk server上的数据是过时的。
- 必须所有chunk server都写入成功,才向客户端返回成功,这个设计看来是偏向一致性的,牺牲了写入时的可用性。这样能保证只要chunk server的chunk version id一样,数据就必定是最新的,就不需要引入一致性算法来管理一致性,简化了设计。
- 对chunk server的读取不一定走primary,这样读取会有不一致的风险,论文里没有怎么提及这方面的保护。
- 由于客户端会缓存primary,这样当发生网络分区或宕机时,客户端没有更新最新的chunk server,可能会向过时的chunk server发起读写。不过由于一般的场景是对chunk进行追加,因此很大可能是读到超过末尾的数据而不是读到过时数据。这会导致读错误,client重新向master请求最新的chunk server。
- 当发生写错误时,可能会造成chunk server之间数据不一致。
- 当多个客户端并发修改同一个chunk时,虽然数据是一致的,但多个客户端的写入会互相穿插,可能会导致数据混乱。其实这个在单机文件系统中也是会发生的,内核提供文件锁来进行保护。gfs出于性能的考虑,没有引入任何锁的同步机制,而是建议使用原子追加或者写匿名文件然后利用原子性的元数据rename操作,来解决这个问题。
- 当发生写错误时,由于客户端重试,原子追加可能会在多个chunk server发生数据的重复或者padding,造成在数据字节层面是不一致的,但在逻辑上可以通过给每个请求编号,在读取时候过滤相同id的数据块,实现一致性。
缓存一致性
gfs由于面向大文件的顺序读写,客户端缓存意义不大,因此避免了设计上针对数据的缓存一致性问题。chunk server上也不单独做缓存管理,而是直接利用linux 内核的page cache,buffer cache来缓存。不过上面提到,为了减少master的压力,客户端会缓存一些元数据,这样会有一定概率发生缓存不一致,有读到过时数据的风险。
高吞吐
gfs通过对文件分成多个chunk,不同的chunk分散在不同server上,而且同一个chunk也由多个chunk server提供读取业务支持,由于集群由大规模机器组成,因此能满足读写的高吞吐,特别是读取性能会非常好。
写入时把控制与数据写业务分离,控制由primary负责写入顺序的保证,但数据写则是选择最近的机器,用流水线的方式形成一条多个chunk server的数据通路,减少写入延迟,充分利用每个chunk server的写入带宽。
master在分配chunk server时,会优先选择负载低的机器,进行负载均衡,保证系统整体的吞吐量。
因为gfs面向大文件,chunk size一般设得比较大,这就减轻了与master都交互,而且client与chunk server间都是TCP长连接来传输,降低了网络开销。
snapshot
master定期自动做snapshot,采用copy-on-write机制,元数据的构造要满足copy-on-write要求。
可以针对某个目录或文件做snapshot。仍然是copy-on-write机制:
- 元数据层面简单地把chunk同时链到snapshot的文件上,使得chunk有多个引用。
- master撤回所有涉及的chunk的租约,禁止对原文件进行写入。
- 当client要写chunk时,会延迟写入,先检查引用数是否大于1,是则通知每个chunk server在本地进行chunk的复制,然后对新chunk赋予租约,返回client。client不会感知到与普通写的任何差别。
垃圾回收
删除文件不直接删数据,而是先把元数据rename成隐藏文件,然后通过后台定时扫描任务清除隐藏文件,同时清理孤儿chunk,这样大大加速了删除的性能,不过会造成空间浪费。
sharding
gfs里不仅对不同文件进行sharding,而且对文件内部也分成chunk。分片的数据由master统一管理,对当前负载、机器容量、以及分机架容灾等共同考虑,决定分片的位置。对缺失的数据分片,会进行重新复制;对容量失衡的机器,也会进行rebalance。这就需要master对所有chunk server进行强有力的监控,包括心跳存活以及各方面状态的监控。
数据完整性
硬盘会损坏,存在硬盘里的数据也可能会因为各种原因造成数据丢失,因此chunk server要定期进行扫描,然后向master汇报,master对损坏的chunk进行重新复制。