1.概述
在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算法示意圖
2.功能驗證
2.1.重命名
2.1.1.重命名文件
以文件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。
2.1.2.重命名文件夾
以文件夾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後,文件夾會重新分配區間
|
結論:文件重命名後,文件夾會分配到當前所有可用子節點上
2.2.負載均衡
利用這個功能可以對已經存在的目錄文件進行Rebalance,使得早先創建的老目錄可以在新增存儲節點上分佈,並可對現有文件數據進行遷移實現容量負載均衡。爲了便於控制管理,rebalance操作分爲兩個階段進行實際執行,即fix layout和migrate data。操作gluster volume rebalance <VOLNAME>{start|stop|status},同時執行以上兩個階段操作,先Fix Layout再Migrate Data
2.2.1 分佈區間(FixLayout)
使得早先創建的老目錄可以在新增存儲節點上分佈。爲相應目錄分配分佈區間後,如果之前的文件通過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 |
2.2.2.數據遷移(MigrateData)
遷移數據是爲了實現負載平衡,新增或縮減節點後,在卷下所有節點上進行容量負載平滑。爲了提高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操作後,刪除了之前的所有連接標識,對文件進行了真實移動。
3. 核心功能分析
3.1.cached子卷
在分佈式哈希算法內,緩存哈希作爲在文件讀取的時候讀取的子卷從而達到直接定位文件的目的,該部分我們將分析緩存子卷的記錄和讀取。
3.1.1.記錄緩存子卷
在該部分,使用函數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); |
3.1.2.獲取緩存子卷
通過inode與hash卷獲得緩存子卷cached_subvol,函數名稱爲dht_subvol_get_cached。
layout = dht_layout_get (this, inode);//首先獲得layout subvol = layout->list[0].xlator;//然後通過layout獲得緩存卷 |
3.2.哈希算法機制
在glusterfs內,文件夾創建的同時,會爲文件夾設置擴展屬性,在該擴展屬性內包括了分佈到該文件夾下的區間範圍,文件存儲定位是通過計算文件名字的hash值來決定文件是存儲到哪一個節點的。
3.2.1.分佈區間設置
擴展屬性設置分爲幾步:
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個子卷,區間分配如下圖:
6.疑惑解答
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個卷一致不會被上傳文件。
7.可配置選型
鍵名 |
類型 |
默認值 |
描述 |
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" } |
|
3.3.哈希算法
當文件上傳的時候,首先會經歷文件的創建操作(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; } } |
這樣就找到了相應子卷,文件就會在捲上創建。
3.4. 函數SuperFastHash
在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; } |
3.5.重命名
重命名包括2個方面的重命名:文件夾重命名與文件重命名。
3.3.1.文件重命名
1)首先通過文件的新舊路徑獲得源hash卷,源緩存卷,目標hash卷,目標緩存卷;
2)如果hash到的子卷與源子卷不爲同一個子卷,創建一個由目標hash卷指向源緩存卷所指向文件的連接,這樣以後訪問新的文件的時候會重定向訪問舊的文件路徑;
3)重命名操作,將相應子卷對應文件進行重命名,這兒所指的相應子卷如果目標緩存子卷與源緩存一致,則就是更新爲以前文件的名字;如果不一致,則更改目標hash所指向文件的名字;
3.3.2.文件夾重命名
如舊目錄路徑爲/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; } |
3.6.查詢操作
分佈式查詢流程圖
由上圖,總結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)如果爲文件,則直接對相應節點下的文件發起查詢並返回
3.7.文件夾修復
分佈式文件夾修復流程圖
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); |
3.8.創建文件夾
3.8.1.工作流程
1、在hash brick上創建文件夾test,並創建成功;
2、再在其他brick上創建文件夾test,沒有強制確保是否一定成功,最後一次創建文件夾test返回的錯誤會返回給客戶端;
3、爲每個brick對應的test分配區間,存儲到layout中;
4、爲每個brick上的test設置xattr,擴展屬性如果並沒有完全正常修復,並不會報錯;
3.9.分區分佈
3.9.1.工作流程
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修復
4.0.數據遷移(MigrateData)
對mountpoint遞歸遍歷目錄兩遍,第一遍只對文件進行操作,進行文件遷移:
1)copy文件至臨時文件(臨時文件需要位於mount point下)
2)複製屬性,遷移擴展屬性,更新uid/gid/time
3)rename臨時文件名爲原文件名
第二遍只對子目錄進行操作,遞歸對子目錄調用gf_glusterd_rebalance_move_data
4.1.讀取目錄 (FixLayout)
基於分佈式自己的特點,在對一個目錄下所有的目錄項進行讀取的時候,會去服務端第一個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的負責均衡
4.2.創建文件(dht_create)
4.2.1.工作流程
1、在hash捲上創建文件test;
2、返回如果成功,設置inode的layout;
3、不管正常錯誤,均向客戶端返回;
4.3.刪除文件夾(dht_rmdir)
4.3.1.工作流程
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操作後,直接返回客戶端;
4.4.刪除文件(dht_unlink)
4.4.1.工作流程
1、獲得文件的hash卷hashed_subvol,和緩存卷cached_subvol;
2、如果hashed_subvol不等於cached_subvol,則先刪除hashed_subvol上的文件,如果刪除失敗,則直接返回客戶端;如果刪除成功,則在cached_subvol上調用刪除(unlink),不管刪除成功或失敗均返回給客戶端,只是返回的狀態不一樣;
3、如果hashed_subvol等於cached_subvol,不管刪除成功或失敗均返回給客戶端,只是返回的狀態不一樣;
4.源碼分析
4.1.重要函數流程分析
4.1.1.函數dht_create分析
該函數爲文件創建過程,分佈到父目錄所分佈的子捲上,新增節點不參加分佈,大致經歷如下幾個步驟:
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,通過記錄的信息直接獲得子捲進行操作。
4.1.2.函數dht_get_du_info分析
該函數主要用於收集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;
4.1.3.函數dht_is_subvol_filled分析
該函數是判斷hash後得到的卷是否已經剩餘空間達到臨界值,核心代碼如下圖:
代碼說明:將該卷subvol與dht中記錄的捲進行對比。如果找到了記錄的卷,首先判斷其disk_unit單位是否爲p(p代表百分比),如果是則判斷conf->du_stats[i].avail_percent<conf->min_free_disk是否成立,即可用的百分比是否小於規定的最小剩餘空間比,如果成立,則認爲該卷空間已經慢;同理如果disk_unit單位爲b即採用字節數,判斷是否滿。
4.1.4.函數dht_free_disk_available_subvol分析
該函數的主要作用就是從dht的所有子卷中,選出剩餘空間最大的子卷,其主要的部分通過一個for循環實現:
代碼說明:循環將可用的空間與max進行對比,如果遇到有比max值更大的值則將值賦給max,最後獲得的max就是剩餘空間最大的卷,獲得max後再與min_free_disk進行比較,如果更小將subvol賦值給avail_subvol。
4.1.5.函數dht_create_cbk分析
在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操作,並且爲父節點提供參數,參數的說明已經在流程圖中體現。
5、分佈式冗餘刪除文件夾問題
5.1.分佈式
目錄刪除失敗問題,其實就是op_ret是否爲-1的問題,如果op_ret == -1,則會導致文件夾刪除失敗,即分佈式卷不會進行rmdir操作而是直接返回客戶端;
1、 如果某個brick down掉:
文件夾刪除失敗;客戶端會報錯誤終端沒有找到(Transport endpoint is not connected),由client xlator報錯;
2、 如果某否文件夾在某個brick上不存在:
文件夾刪除失敗;客戶端什麼錯誤也不報,這與操作系統的體驗是一致的,即不管刪除文件夾成功或失敗均不報錯;
5.2.冗餘
該模式下,任何一個副本,只要沒有出現每份brick均不可鏈接,則刪除文件夾成功,當邏輯卷重啓後,會自動在剛纔down 掉的brick上將應該刪除的文件,文件夾刪除;
5.3. 分佈式冗餘
1、該模式下,任何一個副本,只要沒有每份brick均不可鏈接,則刪除文件夾成功,當邏輯卷重啓後,會自動在剛纔down 掉的brick上將應該刪除的文件,文件夾刪除;這樣就有效地避免了髒數據;
2、如果某份副本的brick全down掉,相等與分佈式卷的一個brickdown掉,文件夾不能刪除;
5.4. 總結
1、分佈式功能不管對於文件,文件夾的創建,刪除均沒有提供高可用性的功能;
2、冗餘功能剛好彌補了分佈式在高可用性的不足,分佈式冗餘功能能夠正常支持刪除操作;
6.疑惑解答
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個卷一致不會被上傳文件。
7.可配置選型
鍵名 |
類型 |
默認值 |
描述 |
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" } |
|