2020年(3)
2019年(3)
2018年(12)
2017年(13)
2016年(11)
2015年(55)
2014年(74)
2013年(39)
2012年(2)
2011年(18)
分类: 云计算
2014-04-18 15:49:44
在glusterfs中,文件的定位采用弹性hash算法进行定位。集群中的任何服务器和
客户端只需根据路径和文件名就可以对数据进行定位和读写访问。换句话说,GlusterFS不需要将元数据与数据进行分离,因为文件定位可独立并行化进行。GlusterFS中数据访问流程如下:
1)计算hash值,输入参数为文件路径和文件名;
2)根据hash值在集群中选择子卷(存储服务器),进行文件定
3)对所选择的子卷进行数据访问。
GlusterFS目前使用Davies-Meyer算法计算文件名hash值,获得一个32位整数。Davies-Meyer算法具有非常好的 hash分布性,计算效率很高。假设逻辑卷中的存储服务器有N个,则32位整数空间被平均划分为N个连续子空间,每个空间分别映射到一个存储服务器。这 样,计算得到的32位hash值就会被投射到一个存储服务器,即我们要选择的子卷。后面我们会对这部分有详细的分析。
在具体代码实现中,Dht为glusterfshash算法实现部分,处于gluster客户端xlator树的中间层,整个文件系统hash算法均由该部分负责,该部分具体处的位置如下图所示:
dht在整个gluster系统中的位置示意图
其在客户端卷配置文件如下:
15 volume v1-dht
16 type cluster/distribute
17 subvolumes v1-client-0 v1-client-1
18 end-volume
该部分目录树形结构如下:
Dht目录树形结构图
文件功能说明:
dht.c:分布式hash算法的主文件,在它内部包含了dht xlator的初始化,析构,对文件的操作定义,xlator参数的合法性验证,事件通知等;
dht-common.c:实现了dht.c中定义的所有dht相关的文件操作;
dht-common.h: dht-common.c的头文件,包含了dht部分的结构体定义,和一些函数的声明;
dht-diskusage.c:dht下所有的子卷相关的存储节点的存储空间使用情况收集;判断某个卷磁盘空间是否已经被塞满;找出可用磁盘空间最多的卷;
dht-hashfn.c:通过计算获得hash值
Dht部分具体代码分析
在gluster中,xlator的设计还是很清晰的,dht作为xlator的一员,依然继承了xlator的实现风格,我们结合类图首先整体认识dht:
dht整体类图:
类图说明:
init函数:用于dht的初始化,在客户端挂载服务启动的时候,客户端会解析xlator树,调用每一个节点的init函数进行xlator的初始化,这个时候dht也被初始化;
fini析构函数:在解析客户端xlator树时被调用将其从内存中清除;
options结构体:用于其参数声明;
fops结构体:dht最核心的部分,其所有动作如如何进行文件的创建,读写等都在其内部进行定义,并且在dht-common.c实现;当它的父节点调用它时,将会调用这些操作完成它应该完成的功能,包含最重要的分布式hash
notify函数:dht的事件通知函数
6)reconfigure函数:对其部分配置参数进行重新配置;
注:上面的所有函数和结构体在所有的xlator都有(个别有例外)
2.功能验证
Hash下的文件夹xattr信息:
>>> xattr.listxattr("renqiang") (u'security.selinux', u'trusted.gfid', u'trusted.glusterfs.dht') >>> xattr.getxattr("renqiang","trusted.glusterfs.dht") '\x00\x00\x00\x01\x00\x00\x00\x00?\xff\xff\xff\x7f\xff\xff\xfd' |
Hash下文件的xattr信息:
>>> xattr.listxattr("7") (u'security.selinux', u'trusted.gfid') >>> xattr.getxattr("7","trusted.gfid") '-\x9c;{\xfb\xd6F\xa3\xa4\xe4\xaf\xf1\x08\xc8\x01\x1d' |
下面开始分析重点函数和结构体部分流程(具体结构体的内部参数说明请看gluster源码研究之基础数据结构部分)
Init函数处理流程
dht的初始化过程,实际上就是对它已经在配置文件中赋值过了的参数进行合法性检查,和在配置文件中不能赋初值,或者没有赋初值的一些参数赋初值的过程。其初始化流程可以表示成这样:
init执行流程图
说明:
父子节点检查主要是检查是否有至少2个子节点,比存存在父节点;
结构体对象在使用前都需进行内存分配;
Conf为结构体dht_conf_t的对象,里面维护了xlator需要的很多基础参数;
Xlator操作过程中会涉及到很多加锁解锁的地方,在此对锁进行初始化,则之后需要用锁的地方可以直接加锁解锁了;
当conf参数都赋值完后,要将其赋值给xlator的private参数;
初始化过程中如果遇到异常等将直接转入错误处理部分,即释放掉初始化过程中分配的内存,初始化失败;
fini执行流程
该过程就是将dhtxlator占用的内存资源释放掉,由于该部分涉及到内容比较简单,在次不再赘诉,可以去看代码了解
fop部分
该部分包含了所有dht涉及到的操作。操作分为2类:1类是结合hash算法来实现的操作;2类是间接利用了hash的结果来实现的操作。
为什么会分为2类呢?当文件的创建,通过hash,磁盘空闲空间等可以确定文件将创建哪一个子卷对应的存储节点上,同时将选择的子卷相对应的信息用 dht的相应参数进行了记录;当进行文件的写,读,扩展数据的操作等时,可以直接从记录的子卷信息中获得相应的信息,然后获得相应的文件,图示如下:
hash算法示意图
以文件py-compile为例,重命名前,该文件存储在子卷v2-client-1对应的节点上,
py-compile文件存储在test2下面即v2-client-1下面: test2 |-- 123 |-- 34 |-- liuhong `-- py-compile |
重命名为py-compile.bak1后,hash后的子卷与更名前子卷相同,该种情况文件仅重命名
-rwxr-xr-x. 1 root root 4142 1?.11 17:34 py-compile.bak1 其扩展属性: >>> xattr.listxattr("/mnt/test2/py-compile.bak1") (u'security.selinux', u'trusted.gfid')//普通文件xattr |
重命名为360buy.com后,在test1,test2即v2-client-0, v2-client-1两个子卷下
test1 |-- 123 |-- 345 |-- 360buy.com //生成了一个文件360buy.com `-- liuhong test1下:---------T. 1 root root 0 1?.16 14:10 360buy.com//文件为空文件 该文件xattr: >>> xattr.listxattr("/mnt/test1/360buy.com") (u'security.selinux', u'trusted.gfid', u'trusted.glusterfs.dht.linkto') >>> xattr.getxattr("/mnt/test1/360buy.com","trusted.glusterfs.dht.linkto") 'v2-client-1\x00' //连接到的地址为子卷v2-client-1 test2 |-- 123 |-- 34 |-- 360buy.com `-- liuhong Test2下:-rwxr-xr-x. 1 root root 4142 1?.11 17:34 360buy.com >>> xattr.listxattr("/mnt/test2/360buy.com") (u'security.selinux', u'trusted.gfid')//扩展属性没变,还是这个键 |
结论:当文件重命名其hash子卷与源子卷不为同一个子卷后,会在hash到的子卷通过mknod创建一个空文件,在xattr中设置了其源子卷为哪个子卷。
再重命名360buy.com为py-compile,会删除子卷v2-client-0上的空文件,在子卷v2-client-1上的文件更名为py-compile。
以文件夹liuhong为例,重命名前,存在节点test1,test2,test3,test4:
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。test1,test2,test3省略 test4 |-- 123 `-- liuhong |
添加2个节点test7,test8后,将文件夹liuhong重命名为360buy.com,则
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。test1,test2,test3省略 test4 |-- 123 `-- 360buy.com test7 |-- 123 `-- 360buy.com test8 |-- 123 `-- 360buy.com 重命名虽然在新加的brick上创建了文件夹,这些文件夹没有为期分配hash区间 >>> xattr.listxattr("/mnt/test7")//没有分配hash区间的xattr (u'security.selinux', u'trusted.gfid', u'trusted.glusterfs.test')//.test属性是为了测试文件系统是否支持xattr。 文件hash不能分配到这些子卷上来,当其他子卷填满后文件可分布这些节点上来 执行命令gluster volume rebalance v2 fix-layout start,可以为新创建的文件夹分配hash区间: >>> xattr.listxattr("/mnt/test7/360buy.com") (u'security.selinux', u'trusted.gfid', u'trusted.glusterfs.dht') 解释:在glusterfs hash部分,设计思想是原来分布的文件不会更改其父目录的分布区间,以免添加新的brick后为了配合一致性hash引起以前的文件的移动,所以新创建的父文件夹没有分配hash区间。执行命令fix-layout后,文件夹会重新分配区间
|
结论:文件重命名后,文件夹会分配到当前所有可用子节点上
利用这个功能可以对已经存在的目录文件进行Rebalance,使得早先创建的老目录可以在新增存储节点上分布,并可对现有文件数据进行迁移实现容 量负载均衡。为了便于控制管理,rebalance操作分为两个阶段进行实际执行,即fix layout和migrate data。操作gluster volume rebalance
使得早先创建的老目录可以在新增存储节点上分布。为相应目录分配分布区间后,如果之前的文件通过hash算法会会对应到新添加的目录下,则会在新添加目录下建立连接,而真实目录还是位于以前目录下,使用到的命令为fix layout,如:glustervolume rebalance v2 fix-layout start。
举例:dht逻辑卷之前有brick相关目录为/mnt/test1, /mnt/test2, /mnt/test3, …/mnt/test10,如果在该逻辑卷下创建目录360buy.com,在该逻辑卷田间2个brick/mnt/test11,/mnt /test12,现在重命名目录360buy.com为360top.com,则在新添加的brick下会用文件夹360top.com,但是这2个文件 夹没有分布区间,文件不能通过hash直接分配到这2个brick。
执行命令:glustervolume rebalance v2 fix-layout start,则
命令运行前: test10 `-- 360top.com |-- account.ring.gz |-- autogen.sh |-- config.h |-- config.sub |-- container.ring.gz |-- COPYING |-- glusterfs.spec |-- Makefile |-- Makefile.am `-- object.builder test11 `-- 360top.com test12 `-- 360top.com 命令运行后: |-- glusterfs.spec.in |-- Makefile |-- Makefile.am |-- object.builder `-- proxy-server.conf test11 `-- 360top.com |-- cert.crt |-- libtool `-- THANKS test12 `-- 360top.com |-- missing `-- NEWS 进入目录test11查看: [root@04:57:06@/mnt/test11/360top.com]#ll ?荤.?.12 ---------T. 1 root root 0 1?.19 16:52 cert.crt ---------T. 1 root root 0 1?.19 16:52 libtool ---------T. 1 root root 0 1?.19 16:52 THANKS 进入目录test12查看: [root@04:57:20@/mnt/test12/360top.com]#ll ?荤.?.8 ---------T. 1 root root 0 1?.19 16:52 missing ---------T. 1 root root 0 1?.19 16:52 NEWS 注 “T”为连接标识,即当dht读取 文件hash到该文件,会重定向到xattr记录的连接brick下读取文件内容 扩展属性内容: >>> xattr.listxattr("missing") (u'security.selinux', u'trusted.gfid', u'trusted.glusterfs.dht.linkto') >>> xattr.getxattr("missing","trusted.glusterfs.dht.linkto") 'v2-client-2\x00'//即该文件重定向到子卷v2-client-2 |
迁移数据是为了实现负载平衡,新增或缩减节点后,在卷下所有节点上进行容量负载平滑。为了提高rebalance效率,通常在执行此操作前先执行Fix Layout,下面测试:
对逻辑卷执行命令gluster volume rebalance v2 migrate-data start:
执行命令前,在2.2.1节已经有结果,test11,test12下的文件: [root@04:57:06@/mnt/test11/360top.com]#ll ?荤.?.12 ---------T. 1 root root 0 1?.19 16:52 cert.crt ---------T. 1 root root 0 1?.19 16:52 libtool ---------T. 1 root root 0 1?.19 16:52 THANKS 进入目录test12查看: [root@04:57:20@/mnt/test12/360top.com]#ll ?荤.?.8 ---------T. 1 root root 0 1?.19 16:52 missing ---------T. 1 root root 0 1?.19 16:52 NEWS 执行命令后再查看: Test11 [root@05:15:42@/mnt/test11/360top.com]#ll ?荤.?.224 -rw-r--r--. 1 root root 1432 1?.19 16:51 cert.crt -rwxr-xr-x. 1 root root 219204 1?.19 16:48 libtool -rw-r--r--. 1 root root 90 1?.19 16:48 THANKS Test12 [root@05:12:25@/mnt/test12/360top.com]#ll ?荤.?.12 -rwxr-xr-x. 1 root root 11014 1?.19 16:48 missing -rw-r--r--. 1 root root 0 1?.19 16:48 NEWS |
结论:执行rebance操作后,删除了之前的所有连接标识,对文件进行了真实移动。
在分布式哈希算法内,缓存哈希作为在文件读取的时候读取的子卷从而达到直接定位文件的目的,该部分我们将分析缓存子卷的记录和读取。
在该部分,使用函数dht_layout_preset在缓存中记录缓存子卷的信息。在该函数中,主要有2个重要的函数dht_layout_for_subvol,它主要用于找到该子卷的索引,并且获得其layout:
for (i = 0; i < conf->subvolume_cnt; i++) { if (conf->subvolumes[i] == subvol) { layout = conf->file_layouts[i];//获得该子卷layout break; } }
|
另外一个函数是inode_ctx_put,该函数主要用于将layout记录到当前文件的inode的ctx内,在需要缓存子卷的时候,可以通过该文件的inode,和hash卷获得layout,然后通过layout获得缓存子卷:
记录layout inode_ctx_put (inode, this, (uint64_t)(long)layout); |
通过inode与hash卷获得缓存子卷cached_subvol,函数名称为dht_subvol_get_cached。
layout = dht_layout_get (this, inode);//首先获得layout subvol = layout->list[0].xlator;//然后通过layout获得缓存卷 |
在glusterfs内,文件夹创建的同时,会为文件夹设置扩展属性,在该扩展属性内包括了分布到该文件夹下的区间范围,文件存储定位是通过计算文件名字的hash值来决定文件是存储到哪一个节点的。
扩展属性设置分为几步:
1)根据总范围与子卷数目,计算每个区间的大小
0xffffffff:总范围大小;cnt:子卷数目;chunk:每个区间的大小 chunk = ((unsigned long) 0xffffffff) / ((cnt) ? cnt : 1); |
意味着每个文件获得文件存储的概率是一样的。
2) 计算一个start_subvol,即通过hash获得一个开始分配区间的子卷索引。
3) 为开始子卷到最后的子卷分配区间范围
for (i = start_subvol; i < layout->cnt; i++) { err = layout->list[i].err; if (err == -1) {//目录存在且没有扩展属性,有扩展属性不再设置 layout->list[i].start = start;//区间开始值 layout->list[i].stop = start + chunk - 1;//区间结束值 start = start + chunk; gf_log (this->name, GF_LOG_TRACE, "gave fix: %u - %u on %s for %s", layout->list[i].start, layout->list[i].stop, layout->list[i].xlator->name, loc->path); if (--cnt == 0) { //如果几个子卷分配完毕,最后一个的stop为最大值 layout->list[i].stop = 0xffffffff; break; } } } |
4)为子卷索引从0到start_subvol的子卷分配区间,与上面分配方式一致,只是分配的子卷不一样
5)分布好的区间,会存储到相应目录的xattr中:
将上面设置保存到layout的区间内的参数取出来放到disk_layout中 ret = dht_disk_layout_extract (this, layout, i, &disk_layout) 将disk_layout放到键为trusted.glusterfs.dht的xattr参数内 ret = dict_set_bin (xattr, "trusted.glusterfs.dht", disk_layout, 4 * 4); 通过操作setxattr将xattr设置到文件的扩展属性中 STACK_WIND (frame, dht_selfheal_dir_xattr_cbk, subvol, subvol->fops->setxattr, loc, xattr, 0); |
举例说明:如果总范围为100,有4个子卷,区间分配如下图:
1.当最初加入创建2个brick,以后又添加2个brick到卷中,为什么刚开始创建文件的时候文件总是会被上传到最开始创建的2个brick中呢?
这种情况只会在根目录下出现,
在dht部分,dht_layout结构体的子结构体list[0]
struct { int err; /* 0 = normal -1 = dir exists and no xattr >0 = dir lookup failed with errno */ uint32_t start; uint32_t stop; xlator_t *xlator; } list[0]; |
如果计算的hash大于start且小于stop,则会选择该xlator,否则选择其他xlator
if (layout->list[i].start <= hash&& layout->list[i].stop >= hash) { subvol = layout->list[i].xlator; break; } |
而通过gdb调试获得4个子卷的分布参数如下:
(gdb) p layout->list[0] $8 = {err = -1, start = 0, stop = 0, xlator = 0x87f310} (gdb) p layout->list[1] $9 = {err = -1, start = 0, stop = 0, xlator = 0x87ff10} (gdb) p layout->list[2] $10 = {err = 0, start = 0, stop = 2147483646, xlator = 0x87d440} (gdb) p layout->list[3] $11 = {err = 0, start = 2147483647, stop = 4294967295, xlator = 0x87e710} |
而计算的hash为2936160380,因此该次选择第4个xlator;第一二个start=stop=因此hash算法不会选择他们。
if (!dht_is_subvol_filled (this, subvol)) { gf_log (this->name, GF_LOG_TRACE, "creating %s on %s", loc->path, subvol->name); STACK_WIND (frame, dht_create_cbk, subvol, subvol->fops->create, loc, flags, mode, fd, params); goto done; } |
然后程序中就会执行判断选择的卷是否被填满,如果最后2个卷一直不被填满,则前2个卷一致不会被上传文件。
键名 |
类型 |
默认值 |
描述 |
lookup-unhashed |
GF_OPTION_TYPE_STR |
{"auto", "yes", "no", "enable", "disable", "1", "0", "on", "off"} |
|
min-free-disk |
GF_OPTION_TYPE_PERCENT_OR_SIZET |
10% |
Percentage/Size of disk space that must be " "kept free |
unhashed-sticky-bit |
GF_OPTION_TYPE_BOOL |
|
|
use-readdirp |
GF_OPTION_TYPE_BOOL |
|
|
assert-no-child-down |
GF_OPTION_TYPE_BOOL |
{"diff","full" } |
|
当文件上传的时候,首先会经历文件的创建操作(create),然后再经过writev操作。
在创建的时候,会通过文件名计算其hash值,然后通过计算的hash值与卷的分布区间进行对比,选择hash值在相应区间的子卷,然后将文件创建到该子卷。主要经历这样几个步骤:
1)如果操作的路径为“/”,则不会进行下面的hash计算而是直接选择第一个状态为正
常的子卷为目标子卷:
if (is_fs_root (loc)) { subvol = dht_first_up_subvol (this);//选择第一个正常子卷为目标子卷 goto out; } |
注:这种情况只会出现在查询根目录时候存在,因为只有这种情况才会为路径为“/”。
2)获得文件父目录的layout,其内存储了目录的分布区间信息
layout = dht_layout_get (this, loc->parent); |
3)通过文件的名字计算其hash值,hash值的计算中用到了Davies-Meyer算法,可以使计算的值尽量在区间内分散,算法本身在此暂不给出。
4)将计算获得hash值与分布区间进行比较,判断该文件应该存储的节点
for (i = 0; i < layout->cnt; i++) {//遍历该目录分布的子卷 if (layout->list[i].start <= hash && layout->list[i].stop >= hash) { subvol = layout->list[i].xlator;//比较找到的子卷 break; } } |
这样就找到了相应子卷,文件就会在卷上创建。
在glusterfs中,在扩展属性中通过键查找其在属性列表中的位置用到了SuperFastHash
函数,据传该函数计算速度相当块,关于该函数的故事的连接:,以后可以测试一下。
计算hash值的调用 int hashval = SuperFastHash (key, strlen (key)) % this->hash_size; |
Hash算法具体实现部分代码如下:
。。。。。。。。。。。。。。。。。。。 /* Main loop */ for (;len > 0; len--) { hash += get16bits (data); tmp = (get16bits (data+2) << 11) ^ hash; hash = (hash << 16) ^ tmp; data += 2*sizeof (uint16_t); hash += hash >> 11; }
/* Handle end cases */ switch (rem) { case 3: hash += get16bits (data); hash ^= hash << 16; hash ^= data[sizeof (uint16_t)] << 18; hash += hash >> 11; break; case 2: hash += get16bits (data); hash ^= hash << 11; hash += hash >> 17; break; case 1: hash += *data; hash ^= hash << 10; hash += hash >> 1; }
/* Force "avalanching" of final 127 bits */ hash ^= hash << 3; hash += hash >> 5; hash ^= hash << 4; hash += hash >> 17; hash ^= hash << 25; hash += hash >> 6;
return hash; } |
重命名包括2个方面的重命名:文件夹重命名与文件重命名。
1)首先通过文件的新旧路径获得源hash卷,源缓存卷,目标hash卷,目标缓存卷;
2)如果hash到的子卷与源子卷不为同一个子卷,创建一个由目标hash卷指向源缓存卷所指向文件的连接,这样以后访问新的文件的时候会重定向访问旧的文件路径;
3)重命名操作,将相应子卷对应文件进行重命名,这儿所指的相应子卷如果目标缓存子卷与源缓存一致,则就是更新为以前文件的名字;如果不一致,则更改目标hash所指向文件的名字;
如旧目录路径为/glusterfs/test,新目录路径为/glusterfs/test1,则文件夹重命名要径路如下步骤:
1)首先通过文件的新旧路径获得源hash卷,源缓存卷,目标hash卷,目标缓存卷;
2)检查文件夹对应的所有子卷对应节点是否能连接通,如果有一个子卷连接不通则报错;
3)对新指向的路径执行opendir操作,该操作会在现存所有子卷上执行,如果某个节点上没有该目录则会创建;
conf->subvolume_cnt:所有子卷,而不仅仅是该目录以前存在的子卷 for (i = 0; i < conf->subvolume_cnt; i++) { STACK_WIND (frame, dht_rename_opendir_cbk, conf->subvolumes[i], conf->subvolumes[i]->fops->opendir, &local->loc2, local->fd); } |
4)对执行了opendir操作的路径执行readdir操作;
STACK_WIND (frame, dht_rename_readdir_cbk, prev->this, prev->this->fops->readdir, local->fd, 4096, 0);//可以看到读取的目录大小为4096,在glusterfs中,这为一个目录的大小,意味着仅读取一个目录 |
5)在所有相应节点均执行了readdir操作后,先对hash到的子卷对应目录执行重命名操作;
STACK_WIND (frame, dht_rename_hashed_dir_cbk, local->dst_hashed, local->dst_hashed->fops->rename, &local->loc, &local->loc2); |
6)再对除开hash节点外的其他节点相应目录执行重命名操作;
for (i = 0; i < conf->subvolume_cnt; i++) {//在其他子卷上重命名文件夹 if (conf->subvolumes[i] == local->dst_hashed) continue; //&local->loc:旧路径;&local->loc2:新路径 STACK_WIND (frame, dht_rename_dir_cbk, conf->subvolumes[i], conf->subvolumes[i]->fops->rename, &local->loc, &local->loc2); if (!--call_cnt) break; } |
分布式查询流程图
由上图,总结Lookup工作流程如下:
通过路径中的名字,计算hash卷;
2、获得缓存卷,可能为空;
3、如果是第一次查询某个文件,文件夹,链接等:
1)通过哈希卷查找对应的文件,文件夹:
STACK_WIND (frame, dht_lookup_cbk, hashed_subvol, hashed_subvol->fops->lookup, loc, local->xattr_req); |
如果计算文件,文件夹名没有找到相应的hash卷,conf->search_unhashed=1或者2则所有的brick查找:
if (ENTRY_MISSING (op_ret, op_errno)) { gf_log (this->name, GF_LOG_TRACE, "Entry %s missing on subvol" " %s", loc->path, prev->this->name); if (conf->search_unhashed == GF_DHT_LOOKUP_UNHASHED_ON) { local->op_errno = ENOENT;//报对应的文件文件夹没有找到 //发起全逻辑卷查询该对象操作 dht_lookup_everywhere (frame, this, loc); return 0; } if ((conf->search_unhashed == GF_DHT_LOOKUP_UNHASHED_AUTO) && (loc->parent)) { ret = inode_ctx_get (loc->parent, this, &tmp_layout); parent_layout = (dht_layout_t *)(long)tmp_layout; if (parent_layout->search_unhashed) { local->op_errno = ENOENT; dht_lookup_everywhere (frame, this, loc); return 0; } } } |
进行全逻辑卷查找时候,查找如果是链接,且有链接子卷则删除脏连接:
if (is_linkfile) { //删除脏连接 gf_log (this->name, GF_LOG_INFO, "deleting stale linkfile %s on %s", loc->path, subvol->name); STACK_WIND (frame, dht_lookup_unlink_cbk, subvol, subvol->fops->unlink, loc); return 0; } 注:因为进行全逻辑卷查找之前已经确认该对象通过hash卷没有被找到,如果现在通过全逻辑卷查找又找到该对象为链接,意味着该链接通过正常流程hash卷已经不能找到了,所以认为该链接为脏连接 |
查询结果既有文件,也有目录,出现错误EIO:
if (local->file_count && local->dir_count) {//相同的名字既有文件也有目录 gf_log (this->name, GF_LOG_ERROR, "path %s exists as a file on one subvolume " "and directory on another. " "Please fix it manually", local->loc.path); //返回客户端信息:EIO错误 DHT_STACK_UNWIND (lookup, frame, -1, EIO, NULL, NULL, NULL, NULL); return 0; } |
如果全是目录,则查找目录;如果文件没有缓存卷则报错,如果有缓存卷没有hash卷则该文件不创建链接;如果2者均有且不一致会建立从hash卷到缓存卷的链接;并且将其缓存卷存入layout内;
2)如果是目录,还会对所有brick查找该文件夹,同时会检查该文件夹的一致性;
3)如果计算文件名找到相应的hash卷,则直接对hash到的brick执行查询操作,并且将该文件的inode记录到layout中;
dht_itransform (this, prev->this, stbuf->ia_ino, &stbuf->ia_ino); if (loc->parent) postparent->ia_ino = loc->parent->ino; ret = dht_layout_preset (this, prev->this, inode);//设置layout到ctx中 |
4)如果是链接,且链接xattr中没有卷名,则进行全盘查找该对象;
//查找链接到的子卷 subvol = dht_linkfile_subvol (this, inode, stbuf, xattr); if (!subvol) {//如果没有链接到的子卷,则全盘查找对象 gf_log (this->name, GF_LOG_DEBUG, "linkfile not having link subvolume. path=%s", loc->path); dht_lookup_everywhere (frame, this, loc); return 0; } |
5)如果是链接则直接对链接到的卷进行查询操作,查询返回后如果仍是链接或者目录等,则进行全盘查询,如果找到的是文件,则将该文件记录到layout中,以方便以后对该文件的查询使用,下次对该文件查询不会再经过链接而是直接查找文件:
ret = dht_layout_normalize (this, &local->loc, layout);
if (ret != 0) { gf_log (this->name, GF_LOG_DEBUG, "fixing assignment on %s", local->loc.path); goto selfheal;//进入目录修复流程 } //设置layout dht_layout_set (this, local->inode, layout); |
4、如果不是第一次查询:
1)通过inode获得文件的layout;
if (is_revalidate (loc)) { //通过inode获得layout local->layout = layout = dht_layout_get (this, loc->inode); |
2)检查相应对象的layout是否正常,如果不正常会转入第一次查询方式;
if (layout->gen && (layout->gen < conf->gen)) { gf_log (this->name, GF_LOG_TRACE, "incomplete layout failure for path=%s", loc->path);//需要重新分布
dht_layout_unref (this, local->layout); local->layout = NULL; local->cached_subvol = NULL; goto do_fresh_lookup;//进入新鲜查询过程 } |
3)检查是否为链接,如果为链接则系统会报错ESTALE;检查是否为目录,为目录会查询所有的brick,然后返回;同时会检查扩展属性中的分布 区间与内存中的分布区间是否一致,如果不一致,系统会向客户端操作系统报错ESTALE(有脏数据错误)(相当于会将查询的相关内容从内存中清空),系统 会重新发起对glusterfs的查询调用,进入首次调用流程;
如果查询到的对象是链接或者读取到的文件夹的扩展属性的分布区间与内存中的分布区间不一致,均会执行如下代码: if (local->layout_mismatch) { local->op_ret = -1; local->op_errno = ESTALE;//会对文件夹发起相当于首次查询
/* Because for 'root' inode, there is no FRESH lookup * sent from FUSE layer upon ESTALE, we need to handle * that one case here */ root_gfid[15] = 1; if (!local->loc.parent && !uuid_compare (local->loc.inode->gfid, root_gfid)) { dht_do_fresh_lookup_on_root (this, frame); return 0; } } |
4)如果为文件,则直接对相应节点下的文件发起查询并返回
分布式文件夹修复流程图
1、检查文件夹是否需要修复
当每个brick查询相应完后,会计算检查是否需要触发文件夹的修复操作,主要就是对holes,overlap,missing,down,misc几个进行检查:
1)计算down等的值
for (i = 0; i < layout->cnt; i++) { if (layout->list[i].err) { switch (layout->list[i].err) { case -1: case ENOENT://文件夹没有找到 missing++; break; case ENOTCONN: down++;//某份brickdown掉 break; case ENOSPC://没有剩余空间 down++; break; default: misc++; } continue; } |
2)检查区间是否有洞还是重叠
if ((prev_stop + 1) < layout->list[i].start) { hole_cnt++; } if ((prev_stop + 1) > layout->list[i].start) { overlap_cnt++; overlaps += ((prev_stop + 1) - layout->list[i].start); } prev_stop = layout->list[i].stop; |
1、 修复过程
当经过第1步检查到文件夹需要修复后,则会进入修复流程
首先,计算layout值,步骤如下:
1) 对路径进行hash,通过start =(hashval % layout->cnt)获得一个开始分配的子卷;
ret = dht_hash_compute (layout->type, loc->path, &hashval); if (ret == 0) { start = (hashval % layout->cnt);//start即为第一个开始分配的子卷 } 注:这就将导致子文件夹与父文件夹的区间不一致; |
2) 从hash到的子卷对应的layout进行分区赋值;
3) 再从0-hash卷进行分区赋值,且最后分配到的卷的layout的结束总是0xffffffff;
其次,准备修复,该过程可以分为3步:
1) 创建文件夹(查询err=ENOENT,则在这些节点创建文件夹)(如果在某个brick下没有找到相应的文件夹,则创建):
判断需要修复创建的目录数: for (i = 0; i < layout->cnt; i++) { if (layout->list[i].err == ENOENT || force) missing_dirs++; } |
2) 修复属性(只修复err=-1的节点);(一般属性为最后读取的文件夹的属性;uid,gid,时间均选择每个副本中最大的)
首先判断需要修复的属性数: for (i = 0; i < layout->cnt; i++) { if (layout->list[i].err == -1) missing_attr++; } 然后发起修复调用: for (i = 0; i < layout->cnt; i++) { if (layout->list[i].err == -1) { gf_log (this->name, GF_LOG_TRACE, "setattr for %s on subvol %s", loc->path, layout->list[i].xlator->name);
STACK_WIND (frame, dht_selfheal_dir_setattr_cbk, layout->list[i].xlator, layout->list[i].xlator->fops->setattr, loc, stbuf, valid); } } |
3) 修复xattr(只修复err=-1的节点);(将之前计算得到的layout写入各自的xattr属性内)
首先计算需要修复的xattr数目:
for (i = 0; i < layout->cnt; i++) { if (layout->list[i].err != -1 || !layout->list[i].stop) { /*err != -1 would mean xattr present on the directory * or the directory is itself non existant. * !layout->list[i].stop would mean layout absent */
continue; } missing_xattr++; } |
然后发起修复调用:
抽取内存中计算获得的区间: ret = dht_disk_layout_extract (this, layout, i, &disk_layout); 设置xattr的值: ret = dict_set_bin (xattr, "trusted.glusterfs.dht", disk_layout, 4 * 4); 发起xattr修复操作 STACK_WIND (frame, dht_selfheal_dir_xattr_cbk, subvol, subvol->fops->setxattr, loc, xattr, 0); |
1、在hash brick上创建文件夹test,并创建成功;
2、再在其他brick上创建文件夹test,没有强制确保是否一定成功,最后一次创建文件夹test返回的错误会返回给客户端;
3、为每个brick对应的test分配区间,存储到layout中;
4、为每个brick上的test设置xattr,扩展属性如果并没有完全正常修复,并不会报错;
1、对mount point递归调用sys_lgetxattr(fullpath,"trusted.distribute.fix.layout", &value, 128)
2、每次调用触发fusetranslator,并传递触发dht translator
3、触发调用dht translator接口函数dht_getxatt
4、 由于指定了trusted.distribute.fix.layout,触发dht_selfheal_new_directory进行目录layout修复
对mountpoint递归遍历目录两遍,第一遍只对文件进行操作,进行文件迁移:
1)copy文件至临时文件(临时文件需要位于mount point下)
2)复制属性,迁移扩展属性,更新uid/gid/time
3)rename临时文件名为原文件名
第二遍只对子目录进行操作,递归对子目录调用gf_glusterd_rebalance_move_data
基于分布式自己的特点,在对一个目录下所有的目录项进行读取的时候,会去服务端第一个brick读取出所有的目录,还会通过读取该逻辑卷所有的brick来读取出所有的目录项。
1、 如果是目录则均从第一个卷读取;
//如果是链接,则不读取,如果是文件夹且卷不是第一个则不读取文件夹 if (check_is_linkfile (NULL, (&orig_entry->d_stat), NULL) || (check_is_dir (NULL, (&orig_entry->d_stat), NULL) && (prev->this != dht_first_up_subvol (this)))) { continue; }//目录只读取第一个子卷 |
2、 如果是链接不显示到客户端;
3、 逻辑卷以索引为顺序,一个卷的目录项读取完了再读取接下来的目录项
if (count == 0) {//如果某个卷返回数量为0 /* non-zero next_offset means that EOF is not yet hit on the current subvol */ if (next_offset == 0) {//开始在下一个卷读取项 next_subvol = dht_subvol_next (this, prev->this); } else { next_subvol = prev->this;//仍然在本卷读取目录项 } if (!next_subvol) { goto unwind; } STACK_WIND (frame, dht_readdirp_cbk, next_subvol, next_subvol->fops->readdirp, local->fd, local->size, next_offset); |
注:分布式功能本身不仅仅包括了文件存取的负载均衡,其实也包括了IO的负责均衡
1、在hash卷上创建文件test;
2、返回如果成功,设置inode的layout;
3、不管正常错误,均向客户端返回;
1、对所有的子卷发起opendir操作,如果操作失败,不再对该卷子卷执行readdirp操作;如果opendir操作成功,再执行readdirp操作;
2、执行readdirp后,如果对应目录项下有除链接与".",".."外,没有其他项,则会调用lookup查询该链接是否为一个链接(没有意外均是),如果是则删除该链接;
3、所有子卷执行readdirp完毕后,所有子卷执行rmdir操作,执行rmdir操作前先检查local->op_ret == -1即只要有几个brick返回失败,则rmdir将不再执行而是直接返回错误信息给客户端;
4、每个brick执行rmdir操作时,如果某个子卷返回op_errno != ENOENT&& op_errno != EACCES,则当所有子卷rmdir操作返回后,会对该目录项进行修复操作(注:只能修复目录及其相关属性),本次删除操作失败,否则所有子卷 rmdir操作后,直接返回客户端;
1、获得文件的hash卷hashed_subvol,和缓存卷cached_subvol;
2、如果hashed_subvol不等于cached_subvol,则先删除hashed_subvol上的文件,如果删除失败,则直接返回客 户端;如果删除成功,则在cached_subvol上调用删除(unlink),不管删除成功或失败均返回给客户端,只是返回的状态不一样;
3、如果hashed_subvol等于cached_subvol,不管删除成功或失败均返回给客户端,只是返回的状态不一样;
该函数为文件创建过程,分布到父目录所分布的子卷上,新增节点不参加分布,大致经历如下几个步骤:
1).计算文件名hash值,查找目标卷;若未找到则返回;
2).如果目标卷空闲容量在预定水位以下,则创建文件并返回;
3).查找空闲容量在预定水位以下子卷,在其上创建文件,并在目标卷上创建链接指向实际文件;
方法dht_create作用是通过一些规则算法在存储节点上创建文件。整个过程会经历如下几个主要步骤:
1)通过dht_get_du_info函数收集每个子卷对应的brick的剩余的磁盘信息,将这些信息读取到参数;
2)通过frame获得的local参数,再通过local找到相应的子卷subvol,如果找到则通过调用 STACK_WIND(STACK_WIND作用与使用见glusterfs源码研究-基础数据研究)向子卷发起create操作,回调由 dht_create_cbk函数进行处理(dht_create_cbk解析会再稍后给出),如果没有找到继续下面的执行流程;
3)调用dht_subvol_get_hashed函数,通过该函数运用hash算法从子卷集合中获得一个子卷subvol:
4)调用dht_is_subvol_filled函数,检查subvol是否已经到达临界空间值,如果还有剩余空间则调用STACK_WIND,否则继续下面的流程;
5)调用dht_free_disk_available_subvol,寻找最大剩余空间的brick,如果找到的为subvol调用 STACK_WIND;如果找到空间更大的brick,则调用dht_linkfile_create,创建一个从subvol到 avail_subvol(找到的最大剩余空间对应的卷),在调用dht_linkfile_create之前进行了2个赋值 local->hashed_subvol = subvol; local->cached_subvol= avail_subvol;这2个赋值很重要,因为它们已经赋值,后面的读写等操作不用再进行hash,通过记录的信息直接获得子卷进行操作。
该函数主要用于收集dht的子卷所关联的硬盘剩余空间信息,与这些子卷没有关系的信息将不会进行收集。总的调用图如下:
dht_get_du_info函数调用图
在该函数中,有如下执行流程:
首先判断更新收集的硬盘剩余空间信息时间间隔是否达到了,因为硬盘信息是隔一定周期才收集的;
如果已经超过了收集周期,则会进行相应的操作,首先对frame出事一些参数 copy到statfs_frame;
调用dht_local_init,对local对象的一些参数赋值且为frame的local参数赋值,然后返回local对象;
通过一个for循环,向dht的所有子卷发送获得其相关联的brick剩余空间信息获取请求,关键代码如下:
statfs_local->call_cnt = conf->subvolume_cnt; for (i = 0; i < conf->subvolume_cnt; i++) { STACK_WIND (statfs_frame, dht_du_info_cbk,conf->subvolumes[i], conf->subvolumes[i]->fops->statfs,&tmp_loc); } |
从for循环可以看出,对每一个子卷调用了statfs操作,当返回信息时会调用dht_du_info_cbk方法返回回调的信息,回调函数的关键代码如下:
dht_du_info_cbk关键代码截图
从图中可以看到,当回调时候,dht xlator会从其记录的子卷卷中找出返回的子卷,并且将返回的该子卷所对应的空闲百分比,和空闲空间记录到相应的参数中。
更新收集空间的时间参数:conf->last_stat_fetch.tv_sec = tv.tv_sec;
该函数是判断hash后得到的卷是否已经剩余空间达到临界值,核心代码如下图:
代码说明:将该卷subvol与dht中记录的卷进行对比。如果找到了记录的卷,首先判断其disk_unit单位是否为p(p代表百分比),如果 是则判断conf->du_stats[i].avail_percent
该函数的主要作用就是从dht的所有子卷中,选出剩余空间最大的子卷,其主要的部分通过一个for循环实现:
代码说明:循环将可用的空间与max进行对比,如果遇到有比max值更大的值则将值赋给max,最后获得的max就是剩余空间最大的卷,获得max后再与min_free_disk进行比较,如果更小将subvol赋值给avail_subvol。
在dht_create函数执行后,当子卷所有create请求操作执行完成后,会有对dht xlator的create相应操作,也就是回调操作,dht_create_cbk就是处理该回调操作。
dht_create_cbk执行流程图
流程图说明:
prev=cookie:子卷中将其frame保存到cookie参数中并将其传送给dht xlator;在dht 中,直接将cookie赋值给prev参数,以为后面dht_layout_preset调用提供参数;
调用dht_itransform函数,通过一定规则为&stbuf->ia_ino重新计算值;在dht_itransform内部,核心代码如下:
代码说明:首先通过dht_subvol_cnt函数获得subvol在dht xlator中记录的索引号,找到赋值给cnt,然后获得y,最后将y值赋值给&stbuf->ia_ino,这样就成功赋值了;
调用dht_layout_preset函数,为inode的参数数组_ctx赋值;
上面的参数准备完毕之后开始调用函数DHT_STACK_UNWIND,该函数会继续发起对dht xlator的父节点的create操作,并且为父节点提供参数,参数的说明已经在流程图中体现。
目录删除失败问题,其实就是op_ret是否为-1的问题,如果op_ret == -1,则会导致文件夹删除失败,即分布式卷不会进行rmdir操作而是直接返回客户端;
1、 如果某个brick down掉:
文件夹删除失败;客户端会报错误终端没有找到(Transport endpoint is not connected),由client xlator报错;
2、 如果某否文件夹在某个brick上不存在:
文件夹删除失败;客户端什么错误也不报,这与操作系统的体验是一致的,即不管删除文件夹成功或失败均不报错;
该模式下,任何一个副本,只要没有出现每份brick均不可链接,则删除文件夹成功,当逻辑卷重启后,会自动在刚才down 掉的brick上将应该删除的文件,文件夹删除;
1、该模式下,任何一个副本,只要没有每份brick均不可链接,则删除文件夹成功,当逻辑卷重启后,会自动在刚才down 掉的brick上将应该删除的文件,文件夹删除;这样就有效地避免了脏数据;
2、如果某份副本的brick全down掉,相等与分布式卷的一个brickdown掉,文件夹不能删除;
1、分布式功能不管对于文件,文件夹的创建,删除均没有提供高可用性的功能;
2、冗余功能刚好弥补了分布式在高可用性的不足,分布式冗余功能能够正常支持删除操作;
1.当最初加入创建2个brick,以后又添加2个brick到卷中,为什么刚开始创建文件的时候文件总是会被上传到最开始创建的2个brick中呢?
这种情况只会在根目录下出现,
在dht部分,dht_layout结构体的子结构体list[0]
struct { int err; /* 0 = normal -1 = dir exists and no xattr >0 = dir lookup failed with errno */ uint32_t start; uint32_t stop; xlator_t *xlator; } list[0]; |
如果计算的hash大于start且小于stop,则会选择该xlator,否则选择其他xlator
if (layout->list[i].start <= hash&& layout->list[i].stop >= hash) { subvol = layout->list[i].xlator; break; } |
而通过gdb调试获得4个子卷的分布参数如下:
(gdb) p layout->list[0] $8 = {err = -1, start = 0, stop = 0, xlator = 0x87f310} (gdb) p layout->list[1] $9 = {err = -1, start = 0, stop = 0, xlator = 0x87ff10} (gdb) p layout->list[2] $10 = {err = 0, start = 0, stop = 2147483646, xlator = 0x87d440} (gdb) p layout->list[3] $11 = {err = 0, start = 2147483647, stop = 4294967295, xlator = 0x87e710} |
而计算的hash为2936160380,因此该次选择第4个xlator;第一二个start=stop=因此hash算法不会选择他们。
if (!dht_is_subvol_filled (this, subvol)) { gf_log (this->name, GF_LOG_TRACE, "creating %s on %s", loc->path, subvol->name); STACK_WIND (frame, dht_create_cbk, subvol, subvol->fops->create, loc, flags, mode, fd, params); goto done; } |
然后程序中就会执行判断选择的卷是否被填满,如果最后2个卷一直不被填满,则前2个卷一致不会被上传文件。
键名 |
类型 |
默认值 |
描述 |
lookup-unhashed |
GF_OPTION_TYPE_STR |
{"auto", "yes", "no", "enable", "disable", "1", "0", "on", "off"} |
|
min-free-disk |
GF_OPTION_TYPE_PERCENT_OR_SIZET |
10% |
Percentage/Size of disk space that must be " "kept free |
unhashed-sticky-bit |
GF_OPTION_TYPE_BOOL |
|
|
use-readdirp |
GF_OPTION_TYPE_BOOL |
|
|
assert-no-child-down |
GF_OPTION_TYPE_BOOL |
{"diff","full" } |
|
GlusterFS 的分析和应用
[ 论文摘要] 随着互联网发展的深入,数据存储的需求得到了空前的增长。如何利用软件在廉价机器上实现高性能、高容量、高可靠性、高扩展性的存储系统便成了很值得研究的问题。作为一个分布式文件系统, GlusterFS采用了独特的弹性 hash 算法,实现了没有元数据的非中心式的架构设计。本文以分析 GlusterFS 的原理实现为主要目的,并进行了简单的部署。过程中,结合当前流行的 Hadoop Distribute File System ( HDFS ),对其总体架构的设计、分布式存储的实现、底层存储结构和可靠性进行了对比分析。
[ 关键词] glusterfs; 分布式文件系统 ; HDFS ;弹性 hash 算法
[Abstract] With the deepening of the development of the Internet, the demand of saving data on the internet is largely growing. It has become worthy of study that how to realize a high-performance, high capacity, high reliability, highly scalable storage system in an inexpensive way. GlusterFS is a distributed file system. Using a unique Elastic-Hash Algorithm, it realizes a non-centric architecture without metadata. The main purpose of this paper is to analyze the principle of GlusterFS, and with a simple deployment case finally. Comparing with the Hadoop Distribute File System (HDFS), I make a comparative analysis of the overall architecture design, distributed storage, the underlying storage structure and reliability.
[Key Words] glusterfs; Distribute File System; HDFS; Elastic Hash Algorithm
7.2. NFS 协议和 Gluster Native Protocol 协议的区别
随着互联网的持续发展,一方面,为了更加便利地存取个人数据,越来越多的消费者开始将自己的资料上传到了互联网上;另一方面,各种互联网内容提供商为了更好的服务用户,也开始不断地增加着各种资源。这样一来,如何可靠、安全地存储这些大量的数据就成了一个值得研究的课题。
随着需要存储的数据越来越多,单个设备的存储能力已经不能够满足大规模数据存储系统的需要,而且数据的不断增长也要求存储系统的存储能力可以不断地进行扩展。所以如何将多台较为廉价的计算机通过网络连结起来,并通过软件来实现一个高容量、高可靠性、高扩展性的存储系统成了研究的热点问题,而分布式文件系统(Distribute File System )就是其中很重要的一个概念。
在实现文件存储的网络化方面,比较早的有 NFS 等。但 NFS 是在单一的服务器上运行的,只是将文件存储服务的提供者和使用者进行了分离,可以说这只是一种文件访问协议,而并没有做到连结多台服务器来共同提供存储服务。
所以相比较与 NFS 这种分布式文件系统,有学者把这种大规模存储的文件系统也叫做集群文件系统(Clustered File System )。比较有名的就是 Lustre 文件系统、 Hadoop 分布式文件系统( HDFS )和本文要分析的 Gluster 文件系统( GlusterFS )。
作为一个后起之秀, GlusterFS 有着很多独有的特色,尤其是其弹性 hash 算法,成功消除了系统对元数据服务器的依赖,不仅使得系统可靠性得到了大幅的提升,也使得线性扩展成为了可能。但目前国内外 GlusterFS的资料相对较少,也暂时没有相应的书籍或论文可以参考,除了少数比较好的博客,其余的主要还是来自 Gluster的官方网站。
为了强调 GlusterFS 的特色设计,文章主体部分(即 2 至 6 节)结合了目前非常流行的 HDFS ,对系统的总体结构、分布式存储、底层存储结构、可靠性进行了对比分析。并在第 7 节给出了一个 GlusterFS 的一个完整的部署过程和应用。
GlusterFS 旨在成为一个通用的、 PB 级存储的、可线性扩展的分布式文件系统,提供统一的全局命名空间,并支持 NFS 、 CIFS 、 HTTP 等通用的网络访问协议。
GlusterFS 的大部分功能都是使用 Translator 机制来实现的,这借鉴了 GNU/Hurd 的设计思想,将功能点设计成一个个的 translator 。每一个 translator 被编译成了共享库文件( .so 文件),并定义了统一的接口,这样通过串行组合很多的 translator 就可以很灵活地实现复杂的功能。在 GlusterFS 中使用的 translator 主要有:
1) Cluster :各种集群模式,目前有 AFR ( Automatic File Replication )、 DHT ( Distributed Hash Table )、 Stripe ;
2) Features :特色功能,有 locks 、 read-only 、 trash 等;
3) Performance :跟性能相关的功能,有 io-cache 、 io-threads 、 quick-read 、 read-ahead 、write-behind 等;
4) Protocol : Gluster Native Protocol 通信协议的客户端和服务器端实现,有 client 、 server 等;
5) Storage :跟本地文件系统直接交互的 POSIX 接口实现;
6) NFS :将 GlusterFS 以 NFS 服务器的形式提供服务的功能;
7) System :目前有 posix-acl ,提供访问控制功能;
8) Mount : FUSE 接口实现;
9) Mgmt :弹性卷管理器;
10) Encryption :加密功能;
11) Debug :系统调试相关的功能实现;
下图是一种功能串联的实现:
图 2 ? 1
Hadoop 作为近年来发展迅猛的一种分布式计算 / 存储技术,为人们所瞩目,并引发了大量的研究。其采用的分布式文件系统 HDFS ( Hadoop Distributed File System )是 2003 年 Google 工程师发表的论文《The Google File System 》 [1] 即 GFS 的一种开源实现。 HDFS 的目的在于为特殊的应用(尤其是MapReduce 应用)提供存储服务,而并不是一个通用型的、交互式的文件系统。在《 The Google File System 》 [1] 中提到 GFS 设计的几个基本假设:
1) 在大规模集群中,错误不再是异常;
2) 大量的超大文件;
3) 新增数据为主,很少修改已有数据;
4) 针对应用软件来设计文件系统可以简化应用软件开发和提高性能;
所以其设计采用了 “write-once-read-many (一次写入多次读取) ” 的文件访问模型:文件一经创建、写入和关闭之后,就不需要改变。简化了数据一致性问题,从而使高吞吐量的数据访问成为可能。
HDFS 是一个 Master/Slave ( NameNode/DataNode )的结构, NameNode 负责存储目录结构、文件属性、文件块及副本的位置等元数据信息, client 需要先与 NameNode 通信获取元数据之后才去DataNode 获取文件块,最终在 client 端进行组合拼装成一个完整的文件。 HDFS 的基本架构如下图所示:
图 2 ? 2
相比较与 GlusterFS 的非中心式设计, HDFS 的架构相对复杂一些,配置起来也会比较复杂,但其原理却是很清晰易懂的。
在 VMware 7.0.1 下搭建的环境:三台 server ( 192.168.37.147 、 192.168.37.148 、192.168.37.149 )、一台 client ( 192.168.37.139 ),操作系统为 centOS 6.0 , GlusterFS 版本为3.2.5 。如下图所示:
图 3 - 1
由于目前 GlusterFS 的文章比较少,且阅读 C 语言写的源代码实在有些难度,所以我还使用了下面这些工具来辅助进行分析:
l wireshark :通过对 client 和 server 的通信过程进行抓包,然后利用关键字搜索进行分析,利用的关键字主要是“ trusted.glusterfs.dht ”和“ gfid ”。
l getfattr :用来查看目录的扩展属性,如: “getfattr -d -m . –e hex /test1/dir1” ,即可查看 /test1/dir1 的扩展属性了。
l lsof 、 netstat 、 ps 等系统命令。
考虑一种简单的模型: N 个存储服务器 server 构成存储池,每个存储服务器对应一个 ID ;
l 存储文件:获取当前 server 节点数为 N ,对 path 进行 hash 计算(模 N )计算,直接将文件存放在计算结果对应的 server 节点上 。
l 读取文件:将文件读取请求发送到每个 server 节点,有该文件的 server 就将文件发给 client 。
在这种简单的模型里,每台 server 的负担是比较重的,即需要接受所有 client 的所有文件的存取请求,而且因为每一次文件请求都是广播行为,所以其网络负担也是比较重的。但是其优点也是明显的:在网络通畅的情况下,线性扩展是非常容易的。
由于弹性 Hash 算法中使用到了扩展文件属性,所以这里先简述一下这个概念。
扩展文件属性是文件系统的一种功能,在 linux 中 、 、 、 、 以及 文件系统都支持扩展属性(英文简写为 xattr )。以 ext4 为例,文件系统结构中主要的是 inode table 和 data blocks ,前者用来存储文件的元数据(数据块的位置信息等),每个文件或目录对应一个 inode 记录;后者即实际文件的数据块。扩展属性存放在 inode table 的每一条记录中。
图 4 ? 1
任何一个文件或目录都对应有一个 inode ,所以就可以有一系列的扩展属性。扩展属性是 name/value 对,name 必须有名称空间前缀,中间用点 “.” 分隔(如 user.name )。目前有四种名称空间: user 、 trusted 、security 、 system 。 GlusterFS 主要使用 trusted 名称空间进行保存其需要的一些信息。
在前面简单的模型中,文件查找时, server 端的计算量可以分成两类:
1) 文件父目录( path 最后一个目录节点)的定位查找
2) 父目录下文件的定位查找
该算法利用文件系统的扩展属性保存了 hash 计算需要的信息,减免了第二步的计算量,一定程度上降低了server 端的负担。
将简单模型精细化为:假设有 N 个节点, hash 函数的值域对应的 32 位整数空间就被均分成 N 个子空间,每个节点对应一个子空间(以下称为 hash range ),对文件名进行 hash 计算后会落在 N 个子空间中的一个,这样就去其对应的节点存取文件。
该算法将每个节点的 hash range 信息保存在每个节点上文件父目录的扩展属性中名为trusted.glusterfs.dht 的属性里,如下图所示:
图 4 ? 2
增加新节点时,新节点会完全复制目录结构和扩展属性中的 gfid (目录 id ),但不复制 hash range 属性。所以如果在虚拟卷的旧目录下新增文件,该新节点将不会参与分布。如下图所示,创建 Dir1 和 Dir2 后,新增节点 ③ ,如果新建文件的父目录是 Dir1 或 Dir2 (例 :/Dir1/newfile.txt 或 /Dir2/newfile.txt ),那么该文件只会被分布在节点 ① 或 ② ,而不会分布在节点 ③ ,因为节点③上的 Dir1 或 Dir2 的扩展属性中没有trusted.glusterfs.dht 属性。
图 4 ? 3
1) 给当前的所有节点分配 hash range ;
2) 向当前的所有节点发送创建目录 Dir1 请求,并发送目录的 gfid 和分配给该节点的 hash range ;
3) server 端创建目录 Dir1 ,并将 hash range 写入扩展属性 trusted.glusterfs.dht 。
图 4 ? 4
在 Dir1 已经存在的情况下,存储 Dir1/file1.txt 时,过程如下:
1) 对文件名 “file1.txt” 进行 hash 计算,得到 32 位的 hash 值;
2) client 向每个 server 节点请求该节点上 Dir1 的扩展属性 hash range ;
3) 确定 hash 值落在哪节点的 hash range 中,然后就将文件存储在该节点上;
图 4 ? 5
在将 test-volume 所在的任意一台 brick 服务器 server1 挂载到了 client 上后,进行文件读取的过程如下:
1) 对文件名 “file1.txt” 进行 hash 计算,得到 32 位的 hash 值
2) 读取 Dir1/file1.txt 时, client 需要向所有的 server 节点上获取 Dir1 的扩展属性 hash range 。
3) 看 hash 值落在哪个节点的 hash range 中,再去对应的节点请求文件。
图 4 ? 6
如果仅仅只有上面的解决方案,系统就有两个重要的问题:一个是部分节点空间不足导致创建文件失败;另一个是老节点的负载比较重导致负载不均衡。对于这个两个问题, GlusterFS 提供了对应的解决办法:
1) 使用链接文件。新建文件时,如果 hash 选定的目标节点已满,那么系统就会选择其他节点中负载最小的节点进行文件存储,同时在原目标节点上创建链接文件指向实际的文件。这种方式同样用于解决文件重命名问题。
2) 提供了 rebalance 机制,以人工的方式进行 hash range 的重新分配和文件的迁移。默认情况下会进行简单的 hash range 平均分配,并不考虑原有的存储情况。但这种简单平均分配 hash range 的方式可能会导致大量的文件迁移,所以这种机制还有改进的余地。
针对原来算法中广播文件请求带来的网络负担,我的一种改进思路如下:
1) 每个节点的父目录的扩展属性中保存所有参与分布的节点的 hash range ,而不仅仅只有该节点的 hash range 。这样就可以避免向所有的节点请求 hash range 信息。
2) 新增节点完全复制目录结构和扩展属性(含 gfid 和所有参与该目录下文件分布的节点的 hash range 信息)。这样 client 端去任意一台 server ,无论新旧,都可以获得所有参与该目录下文件分布的节点的 hash range 信息。
3) client 每次查找文件时需要随机的获取两台 server 上的扩展属性,并进行比对,如果一致则进行文件定位和访问,否则需要先进行扩展属性的修复。
这时,其实也就可以说也出现了一点 “ 元数据 ” 的感觉,但是这个元数据是被备份了多份的,这样就会存在一致性问题,至于那种更好还需要更多的实验去验证。
典型的集群文件系统使用的是 Master/Slave 结构的,如 lustre 和 GFS 等,有专门的元数据服务器(在lustre 中是 MDS ,在 GFS 中是 NameNode )。
HDFS 是基于 Master/Salve 结构( NameNode/DataNode ),由 NameNode 维护着统一的名字空间、文件块及其副本的分布信息等元数据。
以文件读取为例,其时序图如下:
图 4 ? 7
这种中心式的结构的优点是:可以灵活地对文件块进行分布,容易进行性能优化和访问控制。
但它所带来的问题也是显而易见的:
1) 如果 NameNode 出现故障,那么整个文件系统就完全崩溃了。
2) 任何的文件操作都需要跟 NameNode 进行交互,这就导致了 NameNode 容易成为整个文件系统的瓶颈,导致系统性能不能随着存储节点的增加而线性提高。
所以在整个系统的可靠性和扩展性方面, HDFS 会不如 GlusterFS ,所以它并不太适合作为可靠性为主的通用存储系统,而更适合于并行计算为主且系统可靠性性要求较低的应用环境,如 Page-Rank 网页排名算法应用等。
在该模式下,并没有对文件进行分块处理,文件直接存储在某个 server 节点上。 “ 没有重新发明轮子 ” ,这句话很好的概括了这种 GlusterFS 的设计思路。因为使用了已有的本地文件系统进行存储文件,所以通用的很多linux 命令和工具可以继续正常使用。这使得 GlusterFS 可以在一个比较稳定的基础上发展起来,也更容易为人们所接受。因为需要使用到扩展文件属性,所以其目前支持的底层文件系统有: ext3 、 ext4 、 ZFS 、 XFS等。
由于使用本地文件系统,一方面,存取效率并没有什么没有提高,反而会因为网络通信的原因而有所降低;另一方面,支持超大型文件会有一定的难度。虽然 ext4 已经可以支持最大 16T 的单个文件,但是本地存储设备的容量实在有限。所以如果有大量的大文件存储需求,可以考虑使用 Stripe 模式来实现,如考虑新建专门存储超大型文件的 stripe 卷。
其实 Stripe 模式相当于 raid0 ,在该模式下,系统只是简单地根据偏移量将文件分成 N 块( N 个 stripe 节点时),然后发送到每个 server 节点。 server 节点把每一块都作为普通文件存入本地文件系统中,并用扩展属性记录了总的块数( stripe-count )和每一块的序号( stripe-index )。如下图,这个是第一块,另外两块的index 分别是 0x3100 和 0x3200 。
图 5 ? 1
使用 du -h 命令可以查看每个块所占用的硬盘空间,相加即等于整个文件的大小。所以使用这种方式系统就可以支持存储更大的文件,并且因为可以并行读取,文件的读取速度也可以得到大幅的提升。
在 HDFS 中,文件的分布式存储基本是在块( Block )级别上进行的,就如其设计的初衷:适合大文件存储。其将文件进行分块,将不同的块分布在不同的服务器上,由 NameNode 来记录每个文件对应的所有块的位置信息。
在 HDFS 块的默认大小为 64M ( ext4 支持的最大块的 8k ),一个大文件会被拆分成一堆块集合,每个块都会被独立地进行存储。其目的是减少寻址时间占整体传输时间的比例,而且可以降低整个系统的负担:一方面,因为块越大,总的元数据就会越少,减少了内存占用;另一方面,存取一个大文件时,访问 NameNode 的次数和数据传输量都会减少,从而也就降低了 CPU 和网络的负担。
图 5 ? 2
在以计算为主和超大文件存储的应用环境下,分块的好处是显而易见的,它使得 MapReduce 可以对大文件的每一块进行独立地计算处理,而且可以在计算机网络内进行文件块的动态迁移,将文件块迁移到计算空闲的机器上,充分利用 CPU 计算资源,加快处理速度。这一点是 GlusterFS 无法做到的。对于 GlusterFS ,因为是根据文件名进行 hash 计算的,所以文件一经创建,基本就确定了位置了,如果不人工干预,是不会发生改变的。
但这种设计的问题也是明显的,因为分块导致了文件难以修改数据,这也就是《 The Google File System》 [1] 中提到的第三条假设: “ 新增数据为主,很少修改已有数据 ” 所导致的,所以这样存储系统并不适合作为通用文件系统进行使用。
Replicated 模式,也称作 AFR ( Auto File Replication ),相当于 raid1 ,即同一文件在多个镜像存储节点上保存多份,每个 replicated 子节点有着相同的目录结构和文件。 replicated 模式一般不会单独使用,经常是以“ Distribute + Replicated” 或“ Stripe + Replicated ”的形式出现的。如果两台机的存储容量不同,那么就如木桶效应,系统的存储容量由容量小的机器决定。
Replicated 模式是在文件的级别上进行的(相比较于 HDFS ),而且在创建卷 volume 时就确定每个server 节点的职责,而且只能人工的进行调整。这样的话就相对的不灵活,如果一个节点 A 出了问题,就一定要用新的节点去替代 A ,否则就会出现一些问题隐患。
在 Replicated 模式下,每个文件会有如下几个扩展属性:
图 6 ? 1
读写数据时,具体的情况如下 :
l 读数据时:系统会将请求均衡负载到所有的镜像存储节点上,在文件被访问时同时就会触发 self-heal 机制,这时系统会检测副本的一致性(包括目录、文件内容、文件属性等)。若不一致则会通过 change log 找到正确版本,进而修复文件或目录属性,以保证一致性。
l 写数据时:以第一台服务器作为锁服务器,先锁定目录或文件,写 change log 记录该事件,再在每个镜像节点上写入数据,确保一致性后,擦除 change log 记录,解开锁。
如果互为镜像的多个节点中有一个镜像节点出现了问题,用户的读 / 写请求都可以正常的进行,并不会受到影响。而问题节点被替换后,系统会自动在后台进行同步数据来保证副本的一致性。但是系统并不会自动地需找另一个节点来替代它,而是需要通过人工新增节点来进行,所以管理员必须及时地去发现这些问题,不然可靠性就很难保证。
HDFS 的可靠性是在文件块级别上进行的,所以可以定义每个块的副本数,比较灵活,而且由于同一个文件的多个文件块不一定在同一台存储服务器上,甚至不一定在同一机架 rack 上,所以丢失一个文件的所有数据的可能性更低。所以相比之下, HDFS 的可靠性更高一些。
在通常情况下,整个集群上的所有节点可以认为是等同的,这时候所有的存储节点可以看作是一个平坦的存储空间。按这种考虑,文件块的所有是否放在同一个机架上就没有什么值得研究的实际意义,但实际情况却非如此。
实际情况中,集群是由大量的机架组成的,某些问题可能会导致整个机架同时崩溃,所以同一个机架或不同一个机架上的两个节点同时崩溃的概率是不同的。 Rack-Aware Replica Placement (机架感知的副本存放策略) 就是为了这个目的而设计的。
该策略的具体实现需要自己编写脚本,输入参数为 IP 地址,输出为 Rack Id ,编写完的脚本需要在JobTracker 的 hadoop-site.xml 配置文件中配置 topology.script.file.name 。这样 Hadoop 在启动时就会去检查并利用这个脚本来进行 rack-aware 策略。
合理的策略下,文件块的副本会被分别放在不同的 rack 机架上。因为不同一个机架上的两个节点同时崩溃的概率低于同一机架上的两个节点,所以仅从单个文件的角度来看, HDFS 的这种方案的确比GlusterFS 有着更好的可靠性。
NFS 是 Network File System 的简写 , 即网络文件系统 . , NFS 允许一个系统在网络上与他人共享目录和文件。通过使用 NFS ,客户端程序就可以像访问本地文件一样访问远端系统上的文件了。
NFS 的两个主要的进程(守护进程)
l biod 守护进程:运行在所有 client 机上,当客户机上的用户要读写服务器上的文件时, biod 守护进程将此请求发送至服务器。
l nfsd 守护进程:运行在 server 机上,接收来自 client 机的请求,并转换为本地文件系统的读写,并将结果返回给 client 机。
实现的方式基本流程如下:
1) NFS 的底层使用 RPC 实现,且 NFS 守护进程使用的端口号是临时决定的,所以使用该协议时需要使用portmap ( rpcbind )提供注册服务。
2) server 端进程启动时向 portmap(rpcbind) 进行注册
3) client 端进程先通过服务名在 portmap ( rpcbind )中找到目标服务进程的端口号
4) 根据目标服务进程的端口号,发送某目录挂载 mount 请求,由 mountd 负责检查权限,然后返回文件目录句柄
5) client 进程利用远程的文件目录句柄,虚拟的将文件目录挂载到本地,此后对文件操作可以透明地进行了。
在 GlusterFS 中, server 端的 glusterfs 进程会同时启动 nfs 和 mountd 线程,并向 rpcbind 进行注册(可以通过 rpcinfo 命令查看)。
所以在使用 NFS 协议挂载 GlusterFS 时,会有如下通信过程:
图 7 ? 1
通过查看 /etc/glusterd/nfs/nfs-server.vol 配置文件可以知道,其实 GlusterFS 的 nfs-server 其实也是一个 GlusterFS 客户端,只是它没有将 GlusterFS 挂载到本地文件系统,而是将 glusterfs 逻辑卷重新以 NFS的方式分享出去。如下图, nfs-server 将三个逻辑卷 test-volume 、 stripe-volume 和 replicated-volume重新以 NFS 服务器的形式分享出去:
图 7 ? 2
直接使用 Gluster Native Protocol 时,数据流如下
图 7 ? 3
使用 NFS 时数据流是:
图 7 ? 4
所以使用这种方式,性能的下降是必然的,同时还会带来一个 “ 单点故障 ” 的问题。在使用 Gluster Native Protocol 协议时,因为 client 跟每个 server 节点是有端口直接相连,如下图所示( 1832 即 client 端的glusterfs 进程 ID ):
图 7 ? 5
所以即使某个 server 节点出了问题,也不会影响 client 端对其他 server 节点的访问,即不会有 “ 单点故障 ” 问题。但是在 NFS 协议下,如果 mount 的那个节点出了问题, client 就无法访问这个逻辑卷了。
但不得不承认, NFS 协议已经非常通用了,所以,虽然这种方式性能和可靠性都不佳,但是使用起来是很方便的。
安装 GlusterFS 之前需要安装一些基础依赖的包。由于 GlusterFS 的 client 端需要使用 FUSE ( file system in user space )进行挂载逻辑卷,所以需要安装:
yum install fuse fuse-libs
安装 NFS 相关的 rpcbind 和 nfs-utils :
yum install rpcbind nfs-utils
进入到已下载并解压好的 Glusterfs-3.2.5-src 下:
1) 运行 “./configure”
2) 运行 “make”
3) 运行 “make install”
安装完后运行 “glusterfs --version” ,查看是否安装成功。
首先进行 server 端的配置,下面在 server1 ( 192.168.37.147 )上进行:
1) 将多个 server 组成一个可信存储池( trusted storage pool )命令如下:
gluster peer probe 192.168.37.148 192.168.37.149
2) 在三个 server 上分别新建存储目录 /test1 、 /test2 、 /test3 ,然后建立逻辑卷,命令如下:
gluster volume create test-volume 192.168.37.147:/test1 192.168.37.148:/test2 192.168.37.149:/test3
3) 启动卷: gluster volume start test-volume
如下图,是新建的 test-volume 卷的信息:
图 7 ? 6
在 client 端( 192.168.37.139 )的配置,就是将卷 volume 以 NFS 的方式 mount 装载到某个目录节点上。
mount –t nfs –o vers=3 192.168.37.147:/test-volume /mnt/glusterfs-nfs
下面是通过 df 查看到的信息:
图 7 ? 7
这样就可以像本地的文件目录一样对 /mnt/glusterfs-nfs 进行文件操作了