在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
函数,据传该函数计算速度相当块,关于该函数的故事的连接:http://www.azillionmonkeys.com/qed/hash.html,以后可以测试一下。
计算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" } |
|