引言
对于GFS论文的学习让我知道了一个分布式文件系统要实现什么功能与如何实现,即在一定一致性下实现对于极大规模数据的存储与访问。GFS(HDFS)的中心化设计简单且有效的实现了我们的需求,但是却留下了一个问题,就是中心化的设计使得这个系统会在扩展时出现的单点存储的问题,也会出现访问的单点的问题,虽然在中心节点上使用大型机,来得到很多倍规模的提升,但是终究有上限。ceph的出现就解决了这些问题,它使用了开创性的算法使得整个系统成了去中心化的结构,理论上支持无限扩展,并使得整个系统可以实现自动化的容错,负载均衡,这确实是非常振奋人心的一段话,这些主要是ceph中一些独到的算法来做到的,最重要的几点就是CRUSH算法,动态元数据管理与RADOS(可靠的自动分布的对象存储)。
当然这篇文章的出发点是根据论文去学习一个好的分布式文件系统的设计思路,诚然ceph是开源的,是有机会去看看关键部分的实现来巩固所学,但无奈这学期课实在是太多,光上课和完成作业就占了平时百分之七十的学习时间,后面还要准备春招,所以实在是有心无力。好在在狗勋那里知道中国软件供应链summer有ceph的项目,明年可以选择这个社区尝试一下,就算没申请成功也是一个很好的学习机会。
概述
首先论文的第一段就阐明了ceph的设计目标,即:
一个分布式文件系统,它提供了优秀的性能、可靠性和可伸缩性。
这三个词基本阐述了ceph的优点,稍作思考就知道这三点其实很难同时实现,因为其中几点是矛盾的,比如强大的可伸缩性会导致机器数量激增,必然影响性能。而可靠性必然需要数据冗余,也对性能有所影响,所以需要好的设计来解决这些问题。我认为ceph还有一个不可忽略的优点就是超大规模的存储,这个超大规模是理论无上限的,因为整个系统中并没有单点存在。
我在前面提到ceph是开源的,但它和HDFS这类项目并不是一类开源项目,借[4]中的描述,一般开源项目的来源有三:一是学校里的大牛作的课题,论文发够然后开源;二是企业里的大牛搞的产品,机缘巧合于是开源;三是某些大牛突然显灵,然后一票人跟着一起开源。这种背景其实对一个开源项目的影响很大。ceph就属于第一类,它其实是大神Sage Weil的博士研究课题,这篇论文同时也是博士毕业论文,这类开源项目在原理和技术上一般都是非常创新的,相比与其他同类产品有自己的独到之处。
从论文中可以看出,显然在Sage眼中整个系统是动态的,正因这个角度的不同催发出后面自动化的设计,动态性主要分为以下几个方面:
- 数据的动态性:一个大规模存储系统中数据的写入和读出都是非常频繁的,对数据的要求这也侧面规定了数据的一致性。
- 规模的动态性:显然这样一个大规模的存储系统是不可能在第一天就建立完成,后面不必在修改的,相反,系统的规模是一个不断的递增的过程,这也规定了系统中对于热点和分片必须有所处理。
- 设备的动态性:毕竟在ceph的设计中,对于OSD(对象存储设备)的假设就是频繁出现故障,那么在一个大型的存储系统中显然更新的工作几乎不可能人工介入完成,而且这样的问题也不能够影响业务,所以这也规定了系统的可靠性和在出现错误时的恢复功能。
- 访问的动态性:不同的时间段可能用户对于不同的商品操作频率也不一样,比如双11大家肯定是趋向于打折的商品,平时肯定对于首页展示的商品的访问就更多;ceph不对访问模式做假设,这也以为着ceph要针对不同的访问模式做出变化。
ceph的做法
上面提到的问题由以下四个技术实现:
- 分离数据和元数据
- 动态分布式元数据管理
- 可靠的自动分布的对象存储
- CRUSH算法
分离数据与元数据
这其实在分布式文件系统中是一个非常普遍的设计,因为实际上一个MDS可以管理一个或者多个路径,而这可能包含了甚至PB级的数据,这样的设计使得我们不应该让client过多的访问MDS,而是应该在读取数据时从OSD中读取,这样就大幅度减少了MDS的负载。我们发现无论如何client都需要访问MDS,因为必须知道所示请求的数据存在哪个地方,这也是ceph一个重要的做法,即ceph中取消了查表这个过程,进一步减少MDS负载,那么client如何知道数据存在哪里呢?答案就是算一算就好,MDS会在client请求的时候返回一个80字节的inode,这唯一的标示了每一个存储对象,客户端可以根据这个算出数据对象的编号,进而根据一个特殊的map得到数据的所在地点。这样看来如果不考虑其他的文件属性,每一个对象的在MDS中只占[文件名+inode(80字节)]大小。当然每一个文件还有权限,文件大小,不可变属性三个数据[5]4.2节。这样最大限度的分离数据和元数据使得MDS的负载大幅度减少。
动态分布式元数据管理
首先我们要清楚动态分布式元数据管理是为了什么?首先给出答案:减少元数据的磁盘IO和流控。
减少元数据的磁盘IO
其实这部分论文中描述的并不清楚[5]4.1,文中描述为:MDS其实可以满足大多数内存缓存的请求(减少磁盘IO?),但为了安全,元数据更新必须提交到磁盘。为了优化这个更新的过程,会把日志直接传给OSD集群,这样在一个节点宕机时,另一个节点可以快速扫描从而恢复。当然在这里我有一些疑惑,为什么不给每一个MDS配置从节点,运行诸如Paxos和Raft的共识算法,同样可以在宕机时快速恢复,这样显然更加简单一点。当然也可能是权衡了MDS的负载。这里显然文中并没有描述完全,因为一般基于日志的存储应该是包含日志压缩的,否则日志中会有大量的过期日志。
基于日志的传送数据可以使得磁盘操作都是顺序,这也使得日志的持久化能够更快。
动态子树分区和限流
我们首先来看看MDS集群的组织方式:
我们可以非常清楚的看到每一个MDS节点其实负责了文件系统一部分的路径。先不说别的,这个MDS集群的设计就非常精彩了,因为这是一个非常新颖的去中心化集群,也是支持扩展的。像GFS就使用了一个静态的子树分区(毕竟只有一个节点),这会使得对于无法预期的访问出现热点,动态子树分区优雅的解决了这个问题。
如图2所示。通过使用计数器统计每个MDS中元数据的访问量。任何操作使得受影响inode及其上层节点直到根目录的计数都增加, 从而提供每个MDS一个权值,来描述最近的载荷分布。定期的迁移数据,就可以保证元数据的负载均衡。虽然元数据更新很少见,但是终究会发生,这就涉及到分布式事务,因为要修改多个副本,文中提到**日志分录新旧MDS(类似于两阶段提交)**可以解决这个问题,但并没有详细提及这个算法。
当然出现热点的时候还会进行元数据分散分布,大量读访问的目录(如,多open)就会被复制到多个节点。每个MDS不但为client提供inode,还提供祖先的副本信息,这样下一次操作,就可以随机分配到主节点或者从节点(所有节点平等,只是存储数据)上了,这样就解决了热点问题。
CRUSH算法
这里我相信每个人开始都会有一个疑问,就是为什么不直接hash呢,原因其实很简单,我们可以看到这里的CRUSH算法其实是一对多的,也就是client从MDS中得到的inode可以映射到多个OSD上(中间还有一层)。CRUSH算法其实除了单纯的把一组数据拷贝到一组OSD上,OSD的选择也是有讲究的,它会趋向于选择容量更大的设备,这个趋向于其实很耐人寻味,为什么不直接选最大的呢,因为这样会使得最大的那个节点瞬间被存满,如果是动态转换又不好下一次再次定位到这个OSD,所以CRUSH使用了一种伪随机数算法,把PGID(由inode得到)先转化为一个随机数(相同的输入产生相同的输出),然后乘以每一个OSD的权重,最大的那一个就是我们最终存取的OSD。当然选取N个也很简单,我们在CRUSH上做手脚就可以了,这里不详细描述,见[1]。
我们来详细的描述一下一个inode如何映射到多个OSD实现去中心化:
- client向MDS发送请求,根据文件名得到inode。
- inode在hash以后与mask相&得到pgid,mask当然就是pg的最大值。
- pgid通过CRUSH算法得到多个OSD编号。
- 通过cluster map得到数据的真正存储节点地址。
可靠的自动分布的对象存储
首先如何理解可靠的自动分布的对象存储呢?首先我们前面提到了ceph是一个理论上无上限的分布式文件系统,那么这也就意味着它的维护是非常麻烦的,比如故障检测,故障修复,集群变更等,我们当然希望ceph设计之初就可以自动解决这些问题。我们再回到第一个问题,什么是可靠的自动分布的对象存储呢?我的理解就是安全的存储,能够达到客户规定的一致性,自动的故障恢复以及支持可扩展性。这对于一个去中心化的存储系统来说并不容易,我们接下来会看看ceph如何完美的实现这些地方。
备份
安全的存储由备份实现,有意思的是ceph对于数据的冗余并不是基于一致性算法来做的,其实这是非常特殊的。很多分布式系统,比如zookeeper,GFS等等都在关键的数据冗余的地方使用了一致性算法,比如ZAB,paxos等,这实现了自动的故障检测,数据恢复,但是缺点就是大量的消息通信,且在这个pg所映射的OSD中leader宕机的时候这个pg会停止提供服务。在ceph中假设一个PB级或EB级系统故障是正常发生的,这可能是不使用共识算法的一个重要原因。
我们上面提到每一个inode最后都会分配到一个pg中,而这些pg中的object会根据CRUSH算法存储到N个OSD中的,这其中的第1个OSD为leader,负责向client回复消息,其实根据CRUSH算法如果cluster map不变的话每次求出的第一个OSD也是不变的,CRUSH算法具体可参考[1]。
一次write操作如下:
ceph的OSD读取操作是随机选择OSD的,并不像共识算法一样需要经过leader,这也意味着为了数据的一致性我们必须在每一次拷贝中把数据拷贝到三个节点中,显然这样看起来开销也很大。这里采用了一种优化的策略,即在全部的节点把数据写到内存中时就返回给leader一个ack,当leader收到全部的ack以后就立马返回,此时client的读取操作是可以看到数据的,当全部的数据存进磁盘以后向leader提交commit,当全部的节点提交以后向client返回client。显然细心的同学可以看到这种做法是一种较弱的一致性,因为对客户端来说可能看到一个以前已经看到的值,当然我认为这是有办法通过client避免的,虽然我们没办法保证服务器角度的一致性,但可以保证客户端角度的一致性。比如我们在client为每一个读取的操作维护一个seq,如果客户端已经读过未入盘的数据,但在下一次读取时一个OSD宕机,新补充的OSD并没有这一条消息,此次读取我们就可以看到一个比上次读取还要旧的数据,那么它的seq是小于上一次读取的,client很容易拒绝这个请求,即满足单调读一致性;如果这个OSD宕机恢复的过程客户端并没有感知,那么也是满足单调读一致性的。
当然文章中客户端也可以缓存数据直到commit,这样不但保证了一致性,还不会使得数据丢失。
故障检测
其实在一个运行共识算法的集群中,故障检测的会通过心跳包来实现,并且会自动恢复。但在ceph中并没有这么做,但是检测是一定要基于心跳包的,ceph的做法是一个pg的全部OSD之间通过心跳包互相检测,当检测到一个OSD下线的时候(长时间没有收到来自这个节点的心跳包)就会把这个节点标记为down。这里paper中提出RADOS可确认两种OSD活性,是否OSD可访问和是否通过CRUSH算法被分配数据,其中第一种也就是前面描述的这种;第二种文中并没有描述,我认为就是节点数据是否还足以被分配,因为[1]中描述的CRUSH算法是根据节点的总容量来计算的,显然当数据满了以后如果不加以标记会使得其他有容量剩余的节点也无法工作,这也许在CRUSH可以在检测到这种状态的时候重新分配一个,具体paper中并没有提到。
当然文中有一个很重要的概念就是监视器,但是论文中没有详细描述,只是泛泛一提,我总结了一下,它最重要的用处就是维护一个cluster map,其中是OSD到具体地址的映射。因为这个作用,所以它可以维护集群关系,在某个OSD宕机(故障恢复)或者集群新加入一个OSD的时候(可扩展性)会检测到变化,并把这个变化返回给OSD,OSD当收到一个新的cluster map的会检测自己和新加入的OSD在经过CRUSH算法以后有什么关系,当然这样看起来只有三种关系:
- 新加入的OSD和自己没有关系。
- 新来的OSD是自己的leader。
- 新来的OSD是自己的follower。
第一种情况什么也不会发生;
第二种情况,对于副本PG,所在的OSD会提供当前的PG version给主OSD;
第三种情况,如果OSD为PG的主节点,它收集现在的(和过去的)副本PG版本号,如果主节点不是最新的PG状态, 它会从PG所在的OSD检索最近PG变化日志(或者一个完全的PG内容概述,如果需要), 这样可以得到最新的的PG内容,主节点会发送给每个副本节点一个新的日志更新(如果需要, 可以是一个完全的内容概述),这样主节点和副本各方都能知道PG的内容。;
Ceph 监视器 使用 Paxos。
这里因为文章中并没有描述监视器的实现,如果只是一个简单的Paxos集群的话,单点的监视器可能会成为瓶颈;问题不在存储,因为一个OSD对应一个地址并不会花费太多存储空间,OSD号是一个整数,四个字节,ipv6的IP地址就算我们用字符串存储,最大也就4*8+7=39个字节,加上端口和一些其他信息算128个字节。1GB的空间就够存储2^13个机器,这已经很庞大了。问题的关键在于需要监视整个集群,以便及时更新集群关系,我认为这可能是一个可能的瓶颈所在。
其实paper只是描述了大概,还有很多问题尚未解决,还需继续学习,比如:
问:
- 并没有提及元数据修改时的做法,即客户端是否存在缓存?
- 客户端权限的作用?
- MDS集群中如何响应client,是类似于redis集群的转发吗?这需要每一个节点维护全部的消息,但是个不错的做法。
- 动态子树分区在数据拷贝以后如果元数据修改该如何做?
- …
所有具体的细节还是需要深入源码,奈何课太多,而且马上春招,暂且放置一段时间吧!
参考:
- 博文《详解Ceph的杀手级技术CRUSH》
- 博文《Ceph工作原理详解》
- 博文《深入解析Ceph分布式存储的内核客户端》
- 博文《Ceph浅析(上):概况与设计思想》
- 论文《Ceph: A Scalable, High-Performance Distributed File System》