Google File System解析 [转]
Google File System是Google的分布式系统。本文主要对论文中的技术点进行探讨。
2.3 Architecture
文件路径(namespace)数据结构
GFS提供类似于POSIX文件系统的路径管理,这里可以使用copy-on-write的tree来管理 namespace。btree的所有非叶子节点代表了一个目录,该节点的子树表示该目录的结构。 叶子节点可以是目录,也可以是普通文件。 每个节点有一个属性字段表示该节点是目录还是普通文件。
how to mapping from files to chunks
GFS文件系统支持快照,由于目录结构使用copy-on-write的tree来管理,因此目录结构天生就支持快照。每个普通文件对应了多个block,这些block也必须支持快照,设计怎样的数据结构来记录文件与block的对应关系?
最字节的方式也是使用copy-on-write的tree的方式来管理每个文件的block。 一种方式是把block和namespace放入一颗tree中,这样可以解决问题, 但是namespace和block的查找方式并不相同,在逻辑上也不是同一个层次的抽象, 把它们糅在一起不利于控制软件复杂度。 另外一种方式是每个文件的block使用单独的tree来管理, 这样就需要处理两棵独立的tree的原子操作,由于copy-on-write的tree已经很复杂, 再加上两棵独立的tree的copy-on-write无意更大的增加了软件的复杂度。
如果文件指只持追加操作,并且snapshot只读,同时只允许在当前最新的版本上创建snapshot。 那么文件到block的映射可以使用链表或者数组的方式组织, 而文件里面保存了当前snapshot中最后一个block的指针(链表方式)或者偏移(数组方式)。 这样就能够很好的处理文件到block的mapping。
GFS本身是支持random write的,所以不能通过上诉简单的方式实现。 如果把snapshot限制为只读,并且只允许在当前最新的版本上创建snapshot。 那么leveldb的snapshot的实现方式给了我们另外一种简单的数组实现。 这里假设blockid递增且不回收。每个block有一个blockid和offset, offset表示他在当前文件中的偏移。每个文件保存一个blockid, 表示当前文件说使用的最大的blockid。如果是修改已经存在的block, 则创建一个新的block并拷贝原有block的内容,赋值新的blockid, 同时更新当前snapshot的文件节点中保存的blockid, 这样创建的block与原始block拥有相同的offset;如果是追加写, 处理过程相同,只是新的block有全新的offset。从上面的逻辑可以看出, 同一个offset可能有多个block,每个block对应了一个快照的内容。 读取一个文件的快照的时候对于同一个offset的多个block, 选择blockid小于等于文件节点中记录的blockid的最大的那一个block。 这样的设计虽然在工程实现上降低了复杂度,但是block的回收成为一个难题, 这样每次创建和删除snapshot的时候都必须修改属于该snapshot的所有block的引用计数。
对于上诉两种实现方式而言,GFS对外提供的关于snapshot的更新接口就只有checkpoint, 所有其他关于snapshot的接口都是只读接口。从论文说暴露的内容来看, GFS也只是提供了关于snapshot的上诉语义。因此上诉实现方式是足够的。
如果对snapshot没有任何限制,那么就只有使用copy-on-write的tree来组织文件到 block的mapping了。对这种实现方式的理解暂时不够深刻, 先不表述了。然而,通过copy-on-write的tree的方式来组织在功能上是最完备的。
2.6.3 Operation Log
原文中有这样一句话:The checkpoint is a compact b-tree like for that can be directly mapped into memory and used for namespace lookup without extra parsing.
checkpoint可以直接map到内存,说明这个compact内部使用的是offset而不是pointer, 而checkpoint被加载以后显然是存在修改操作的, 那么后续的修改操作是使用pointer还是offset?如果使用pointer, 那么两者显然无法统一。更加合理的解释是后续的修改也是使用的offset, 同时存在一个内存管理对象将offset映射到地址。
2.7 Consistency Model
GFS最大的一个用户就是BigTable,我们知道BigTable的sstable的每一项的内容都是有索引的, tablet最终的索引存放在chubby中,对于有索引的数据,索引只索引那些defined的内容, 因此GFS提供的一致性模型满足需求。但是有一个东西没有索引,那就是日志。 考虑以下执行场景:
- tablet server append一条日志,该日志是没有成功的, 因此在GFS的各个replica中有的有该日志的内容,有的没有
- tablet server宕机
- 新的tablet server起来回放日志
注意,在第3步的时候,新的tablet server可能读取到最后一条日志; 可能读取不到最后一条日志。那么BigTable怎么给客户端展现一致的状态呢? 肯定不能发生下面的情况:
- tablet server A从其中一个replica中读取到最后一条日志, 在对客户端的响应中最后一个修改是成功的,然后tablet server A挂掉
- tablet server B重新加载该tablet,它从另外一个replica回放日志, 却没有读到最后一条不一致的日志,在对客户端的响应中最后一次修改是失败的
显然,BigTable不能够对外暴露上诉不一致的状态。那么BigTable如何处理这种情况? 原文中有一句话:Checkpointing allows writers to restart incrementally and keeps reads from processing successfully written file data that is incomplete from the application’s perspective. 不知道是否是描述解决方案的。
在任何tablet server加载一个tablet的时候,它的过程包括:
- 加载所有已经生成的sstable
- 回放日志
- 做minor compaction,就是checkpoint
- 对外服务
只有在checkpoint(第3步)成功以后,tablet server才会对外服务, 而从此以后最后一条不一致的日志的影响就确定了,要么成功要么失败, BigTable对外体现出一致状态。
3.1 Lease and Mutation Order
Lease协议
原文中有这样一句话:A lease has an initial timeout of 60 seconds. However, as long as the chunk is being mutated, the primary can request and typically receive extensions from the master indefinitely. These extension requests and grants are piggybacked on the HeartBeat messages regularly exchanged between the master and all chunkservers.
由这样的描述我们可以看到renew lease是由chunkserver发起的。过程如下:
- chunkserver发现lease快过期,在心跳中向master申请延长lease
- master收到renew lease的请求,延长本地lease,同时发送renew lease的响应
- chunkserver在收到响应后延长本地lease
注意,关于lease,master lease时间的延长必须在chunkserver lease时间延长之前进行,这样才能保证数据的安全性。 因为这样就是chunkserver的lease时间没有成功修改, 带来的影响就是master等待更长lease超时,而不会发生数据不一致。
如果renew lease是由master发起,那么过程如下:
- master收到chunkserver的心跳,如果发现发现应该更新lease,则进行后续的操作
- master延长本地lease时间
- master发起renew lease的请求,或者在心跳中包含新的lease超时时间
- chunkserver在收到renew lease的请求后更新本地lease
在这两种协议中,master都不会主动检查lease的过期,lease的延期都是由chunkserver触发。 如果需要主动检查lease过期,master可以有一个定时任务,用于扫描所有发出去的lease, 从而达到主动检查lease超时的目的。
层级lease
层级lease是实现分布式系统的一种有效手段。 层级lease需要注意的一点是下层lease的时间必须是上层lease时间的2.5倍。推导如下:
第k层的lease时间长度使用表示
第k层的最大不可服务时间用表示
为了保证上层服务的波动不影响下层服务,那么必须大于
, 即
而,即k+1层的不可服务时间等于k+1层的 lease时间加上k层的不可服务时间, 这种情况发生在k+1层的lease刚好快过期的时候k层进入不可服务状态
假设最极端的情况,
于是得到,得到
, 为了安全期间,取2.5倍。
lease带来的不可服务时间
由上面的分析我们知道,lease带来的不可服务时间是当前层的lease时间加 上上层的不可服务时间。对于GFS而言,一个文件不可服务的时间等于 chunkserver的lease时间(60s),加上master的主从切换时间。 这样文件的不可服务时间就是分钟级的,对于线上引用而言显然不可忍受, 那么GFS的用户如何解决这个问题呢?答案就是在当前文件不可写的情况下,创建新的文件。
master的primary和shadow之间是否要同步lease信息
如果master的primary和shadow之间不同步lease信息,一旦master的primary宕机, shadow每个block都必须等待lease的时间长度,确保任何一个chunk不会有多个primary存在。 这样,在这段时间内master是不能响应客户端的读请求的。
另外一种方法就是master的primary和shadow之间同步lease信息, 这样可能回导致加大的信息同步量。
由于chunk的数量太大,在master的primary和shadow之间同步每个chunk的lease信息不太现实。
另外一种方法,shadow master在成为primary master之后, 像每个chunk的所有replica询问谁是primary, 这样可以减少master切换带来的不可服务时间。但是对于gfs而言, 这会导致大量的请求,从而对集群的服务产生抖动感染。如果集群中lease的数量较少, 可以采用这样的方法。
Lease应该使用绝对时间还是相对时间
Lease必须保证,在master认为某个节点的lease过期后,该节点必须自己也认为自己的lease 过期了。因此不能够使用相对时间。因为如果包的延迟太大,比如相对时间是10ms,但是网络传输 时间超过10ms了,master此时认为node的lease过期,但是node接受到请求的时候却会延长lease, 导致两边认为不一致。因此必须使用绝对时间。
但是网络中的各个节点的时钟可能不一致。可以在网络中部署ntp服务,让各个节点的时钟一致。
mutation order
每次mutation,chunk primary都先修改本地数据,这样才能实现atomic的append, 因为这样的逻辑对于append操作而言,chunk primary上表示的文件长度一定是最长的。 所以append的时候对于所有replica都是append操作。
4.4 Garbage Collection
how to identify orphand chunks?
前面讲述了file到chunk的mapping实现细节,通过文件+offset查找chunk是很自然的事情。 那么如果通过chunk反向查找文件,如何确定一个chunk是orphaned? 这里可以使用引用计数的方法。chunk的引用计数代表该chunk属于多少个副本。 在扫描chunk的时候,回收所有引用计数为0的chunk就ok。
4.5 Stale Replica Detection
原文中有这样一句话:Whenever the master grants a new lease on a chunk, it increase the chunk version number and informs the up-to-date replicas.
如果chunkserver汇报的chunk version比master记录的version低, master会认为相应的chunk是stale的,会把相应的chunk干掉。 这个地方其实是有多个节点的分布式一致性问题的。问题的关键是, master一定是在chunkserver修改chunk version之后修改, 否则就会出现chunk的所有replica都失效的情况。协议如下:
- master对primary chunk grant lease,携带增加后的chunk version
- primary chunk响应后,master通知所有up-to-date的replicas
- 超时或者所有up-to-date的replica都响应后,master修改本地的chunk version
5.2 Data Integrity
通过checksum保证已经破坏的数据在分布式系统中不扩散,这一点至关重要, 否则都不知道自己是怎么挂掉的。
通过心跳统计搜集集群信息
作为上千个节点的集群,master必须及时的搜集集群信息,以做出正确的决策。 比如,master必须知道当前集群存活的节点数量,如果有太多的节点宕机, 那么master最好的策略就是无为而治。因为如果这个时候大量的增加副本数量 以满足配置的副本数就会导致整个系统陷入replication的潮,导致系统崩溃。 下面提供一种算法,用于master搜集集群信息。master对每个chunkserver维护一个 hb_sequence,表示master最近给chunkserver发送心跳的时间点(单位为秒), master会把这个值写入向chunkserver发送的心跳请求中; 同时维护一个hb_res_sequence,表示最后收到chunkserver心跳请求的时间点, chunkserver的心跳请求中包含hb_res_sequence, 每次收到chunkserver的心跳请求更新该值。 如果大于某个给定的值,则认为该chunkserver已经下线。
chunkserver如何处理呢?chunkserver收到master的心跳后, 得到这次请求的hb_sequence,chunkserver更新全局的g_hb_sequence, 同时向master发起心跳请求,请求中包含hb_sequence。 同时chunkserver存在一个定时器线程定期检查本地时间和g_hb_sequence的差值, 如果发现长时间没有收到master的心跳请求,chunkserver就杀死自己, 因为他认为在这么长的时间内没有收到master的心跳,那么master肯定已经认为自己挂了, 所以就把自己挂了。
通过这个算法master可以搜集集群的状态。算法存在两个问题。
首先是master的心跳间隔需要比较精确。
其次,master是否需要与shadow同步sequence?master发出的hb_seqence保持递增, 那么shadow如何得到最近的hb_sequence呢? shadow master可以与chunkserver维持相同的心跳, 也就是说shadow master维护自己的sequence, chunkserver处理primary和shadow的心跳的策略的唯一不同就是如果心跳来自shadow master, chunkserver不修改g_hb_sequence。这样primary master和shadow master各自独立的维护集群状态, master发生主从切换后就不会存在大面积的chunkserver下线。
primary master和shadow master之间也可以通过相同的双向心跳维持状态一致。
转自:http://nosql-wiki.org/foswiki/bin/view/Main/GoogleFileSystem