1. 基本介紹
sheepdog是近幾年開源社區新興的分布式塊存儲文件系統,采用完全對稱的結構,沒 有類似元數據服務的中心節點。這種架構帶來了線性可擴展性,沒有單點故障和容易管理的特性。對於磁盤和物理節點,SheepDog實現了動態管理容量以及 隱藏硬件錯誤的特性。對於數據管理,SheepDog利用冗余來實現高可用性,並提供自動恢復數據數據,平衡數據存儲的特性。除此之外,sheepdog 還有具有零配置、高可靠、智能節點管理、容量線性擴展、虛擬機感知(底層支持冷熱遷移和快照、克隆等)、支持計算與存儲混合架構的特點等。目前,開源軟件 如QEMU、Libvirt以及Openstack都很好的集成了對Sheepdog的支持。在 openstack中,可以作為cinder和glance的后端存儲。
sheepdog總體包括集群管理和存儲管理兩大部分。集群管理使用已有的集群管理工具來管理,存儲管理基於本地文件系統來實現。目前支持的本地文件系統包括ext4和xfs。
編譯后的sheepdog由兩個程序組成,一個是守護程序sheep,一個是集群管理工具dog,守護程序sheep同時兼備了節點路由和和對象存儲的功能。
Sheep進程之間通過節點路由(gateway)的邏輯轉發請求,而具體的對象通過對象存儲的邏輯保存在各個節點上,這就把所有節點上的存儲空間聚合起來,形成一個共享的存儲空間。
Sheepdog由兩個程序組成,一個是后台進程sheep,一個是前台管理工具dog。Dog主要負責管理整個sheep集群,包括集群管理,VDI管理等。集群管理主要包括集群的狀態獲取,集群快照,集群恢復,節點信息,節點日志,節點恢復等。VDI管理包括VDI的創建,刪除,快照,檢 查,屬性等等。
Dog是一個命令行工具,啟動時,會向后台sheep進程發起TCP連接,通過連接傳輸控制指令。當sheep收到控制指令時,如果有需要,會將相應指令擴散到集群中,加上對稱式的設計,從而使得dog能夠管理整個集群
2. 基本架構
- 由corosync完成集群成員管理和有關集群消息傳遞,比如對於節點加入刪除等情況檢測;
- 由Qemu VM作為Sheepdog的客戶端,進行快照克隆、創建虛擬卷等操作命令的執行,提供NBD/iSCSI協議支持;
- 由gateway實現數據的DHT路由,接收QEMU塊驅動的I/O請求,通過散列算法獲得目標節點,然后轉發I/O請求至該節點;
- 由Sheep store數據本地存儲.
- Corosync發送有關集群處理的消息給Sheep,Sheep再進行集群節點的加入刪除等操作
- Qemu和Dog(提供了一系列系統命令)發送命令解析后的請求給Sheep,Sheep再根據具體的請求類型進行相關處理
3. 啟動流程
3.1 sheep啟動
啟動過程中會有一些初始化的工作,對於基本目錄的初始化,對於obj、epoch、journal路徑的初始化,以及對於集群和工作隊列的初始化。下圖可以看到sheep基本的啟動流程
3.2 創建監聽端口
通過socket創建來自客戶端的請求,注冊對應的listen_handler和client_handler事件,對請求進行相應的處理。相關處理函數的函數指針賦值給fn和done,如下圖右下rx_work和rx_main即可知:
3.3 工作隊列初始化
在線程函數worker_routine中將對應請求操作的處理函數work->fn(work)根據不同隊列不同請求執行對應處理函數,執行完后加入完成隊列,再根據不同隊列不同請求執行對應處理函數done()
3.4 事件機制
event_loop函數根據事件觸發機制,等待新事件的到來,觸發epoll_wait,之后相應的句柄函數進行相應處理。
1、listen_handler 偵聽到客戶端有連接請求時,會將該連接 fd 注冊到主線程 efd 中,該 fd 與 client_handler 綁定,當客戶端向該 fd 發送請求時,主線程會及時檢測到並且調用 client_handler 對請求進行處理
2、local_req_handler包括對gateway、cluster、io的相關處理
3、sigfd = signalfd(-1, &mask, SFD_NONBLOCK);
4、sys->local_req_efd = eventfd(0, EFD_NONBLOCK);
4. dog啟動流程
dog部分主要是執行客戶端的命令行請求,然后對命令進行解析,通過指定socket發送請求到sheep端,將請求交sheep端處理。
1、init_commands(&commands)函數將dog支持的命令都初始化在commands中進行調用,包括對vdi、cluster、node的命令操作,
2、setup_commands()函數先比較主命令,然后比較subvommmand,將對應的處理函數賦值給command_fn函數指針,最后調用此函數對命令進行處理
4.2 dog支持的命令
下面給出dog能執行的命令,及操作這些命令的函數
4.2.1 node命令
kill | node_kill | 刪除節點 |
list | node_list | 列舉節點信息 |
info | node_info | 顯示一個節點的信息 |
recovery | node_recovery | 顯示節點的恢復信息 |
md | node_md | 顯示md信息 |
log | node_log | 顯示節點有關日志的信息 |
4.2.2 vdi命令
check | vdi_check | 檢查和修復image的一致性 |
create | vdi_create | 創建一個image |
snapshot | vdi_snapshot | 創建一個快照 |
clone | vdi_clone | 克隆一個image |
delete | vdi_delete | 刪除一個image |
rollback | vdi_rollback | 回滾到一個快照 |
list | vdi_list | 列舉images |
tree | vdi_tree | 以樹的形式顯示images |
graph | vdi_graph | 以圖的形式顯示images |
object | vdi_object | 顯示image里面對象的信息 |
track |
vdi_track | 顯示image里面對象的版本蹤跡 |
setattr |
vdi_setattr | 設置一個vdi的屬性 |
getattr |
vdi_getattr | 獲得一個vdi的屬性 |
resize |
vdi_resize | 重新設置一個image的大小 |
read |
vdi_read | 從一個image里面讀數據 |
write |
vdi_write | 寫數據到一個image里面 |
backup |
vdi_backup | 在兩個快照之間創建一個增量備份 |
restore |
vdi_restore | 從備份里面復原images快照 |
cache |
vdi_cache | 運行dog vdi cache得到更多信息 |
4.2.3 cluster命令
info | cluster_info | 顯示集群信息 |
format | cluster_format | 創建一個sheepdog存儲 |
shutdown | cluster_shutdown | 關閉sheepdog |
snapshot | cluster_snapshot | 為集群建立快照或復原集群 |
recover | cluster_recover | 看dog cluster recover得更多信息 |
reweight | cluster_reweight | reweight集群 |
5. 部分數據結構
5.1 vdi object
struct sd_inode { char name[SD_MAX_VDI_LEN]; // vdi的名稱 char tag[SD_MAX_VDI_TAG_LEN]; // 快照名稱 uint64_t create_time; uint64_t snap_ctime; uint64_t vm_clock_nsec; // 用於在線快照 uint64_t vdi_size; uint64_t vm_state_size; // vm_state的大小 uint8_t copy_policy; // 副本策略 uint8_t store_policy; uint8_t nr_copies; uint8_t block_size_shift; uint32_t snap_id; uint32_t vdi_id; uint32_t parent_vdi_id; // 父對象id uint32_t btree_counter; uint32_t __unused[OLD_MAX_CHILDREN - 1]; uint32_t data_vdi_id[SD_INODE_DATA_INDEX]; struct generation_reference gref[SD_INODE_DATA_INDEX]; };
6. QEMU塊驅動
Open
首先QEMU塊驅動通過getway的bdrv_open()從對象存儲讀取vdi
讀/寫(read/write)
塊驅動通過請求的部分偏移量和大小計算數據對象id, 並向getway發送請求. 當塊驅動發送寫請求到那些不屬於其當前vdi的數據對象是,塊驅動發送CoW請求分配一個新的數據對象.
寫入快照vdi(write to snapshot vdi)
我們可以把快照VDI附加到QEMU, 當塊驅動第一次發送寫請求到快照VDI, 塊驅動創建一個新的可寫VDI作為子快照,並發送請求到新的VDI.
VDI操作(VDI Operations)
查找(lookup)
當查找VDI對象時:
1) 通過求vdi名的哈希值得到vdi id
2) 通過vdi id計算di對象
3) 發送讀請求到vdi對象
4) 如果此vdi不是請求的那個,增加vdi id並重試發送讀請求
快照,克隆(snapshot, cloning)
快照可克隆操作很簡單,
1) 讀目標VDI
2) 創建一個與目標一樣的新VDI
3) 把新vdi的‘'parent_vdi_id''設為目標VDI的id
4) 設置目標vdi的''child_vdi_id''為新vdi的id.
5) 設置目標vdi的''snap_ctime''為當前時間, 新vdi變為當前vdi對象
刪除(delete)
TODO:當前,回收未使用的數據對象是不會被執行,直到所有相關VDI對象(相關的快照VDI和克隆VDI)被刪除.
所有相關VDI被刪除后, Sheepdog刪除所有此VDI的數據對象,設置此VDI對象名為空字符串.
對象恢復(Object Recovery)
epoch
Sheepdog把成員節點歷史存儲在存儲路徑, 路徑名如下:
/store_dir/epoch/[epoch number]
每個文件包括節點在epoch的列表信息(IP地址,端口,虛擬節點個數).
恢復過程(recovery process)
1) 從所有節點接收存儲對象ID
2) 計算選擇那個對象
3) 創建對象ID list文件"/store_dir/obj/[the current epoch]/list"
4) 發送一個讀請求以獲取id存在於list文件的對象. 這個請求被發送到包含前一次epoch的對象的節點.( The requests are sent to the node which had the object at the previous epoch.)
5) 把對象存到當前epoch路徑.
沖突的I/O(conflicts I/Os)
如果QEMU發送I/O請求到某些未恢復的對象, Sheepdog阻塞此請求並優先恢復對象.
協議(Protocol)
Sheepdog的所有請求包含固定大小的頭部(48位)和固定大小的數據部分,頭部包括協議版本,操作碼,epoch號,數據長度等.
between sheep and QEMU
操作碼 |
描述 |
SD_OP_CREATE_AND_WRITE_OBJ |
發送請求以創建新對象並寫入數據,如果對象存在,操作失敗 |
SD_OP_READ_OBJ |
讀取對象中的數據 |
SD_OP_WRITE_OBJ |
向對象寫入數據,如果對象不存在,失敗 |
SD_OP_NEW_VDI |
發送vdi名到對象存儲並創建新vdi對象, 返回應答vdi的唯一的vdi id |
SD_OP_LOCK_VDI |
與SD_OP_GET_VDI_INFO相同 |
SD_OP_RELEASE_VDI |
未使用 |
SD_OP_GET_VDI_INFO |
獲取vdi信息(例:vdi id) |
SD_OP_READ_VDIS |
獲取已經使用的vdi id |
between sheep and collie
操作碼 |
描述 |
SD_OP_DEL_VDI |
刪除VDI |
SD_OP_GET_NODE_LIST |
獲取sheepdog的節點列表 |
SD_OP_GET_VM_LIST |
未使用 |
SD_OP_MAKE_FS |
創建sheepdog集群 |
SD_OP_SHUTDOWN |
停止sheepdog集群 |
SD_OP_STAT_SHEEP |
獲取本地磁盤使用量 |
SD_OP_STAT_CLUSTER |
獲取sheepdog集群信息 |
SD_OP_KILL_NODE |
退出sheep守護進程 |
SD_OP_GET_VDI_ATTR |
獲取vdi屬性對象id |
between sheeps
操作碼 |
描述 |
SD_OP_REMOVE_OBJ |
刪除對象 |
SD_OP_GET_OBJ_LIST |
獲取對象id列表,並存儲到目標節點 |
7. oid到vnodes的映射
/* 調用 */
oid_to_vnodes(oid, &req->vinfo->vroot, nr_copies, obj_vnodes);
/* 首先確定第一個zone的位置,隨后按照zone進行便利 */
/* Replica are placed along the ring one by one with different zones */ static inline void oid_to_vnodes(uint64_t oid, struct rb_root *root, int nr_copies, const struct sd_vnode **vnodes) { const struct sd_vnode *next = oid_to_first_vnode(oid, root); vnodes[0] = next; for (int i = 1; i < nr_copies; i++) { next: next = rb_entry(rb_next(&next->rb), struct sd_vnode, rb); if (!next) /* Wrap around */ next = rb_entry(rb_first(root), struct sd_vnode, rb); if (unlikely(next == vnodes[0])) panic("can't find a valid vnode"); for (int j = 0; j < i; j++) if (same_zone(vnodes[j], next)) goto next; vnodes[i] = next; } }
/* 這里就是按照順時針將oid_hash分配到對應的節點上 */
/* If v1_hash < oid_hash <= v2_hash, then oid is resident on v2 */ static inline struct sd_vnode * oid_to_first_vnode(uint64_t oid, struct rb_root *root) { struct sd_vnode dummy = { .hash = sd_hash_oid(oid), }; return rb_nsearch(root, &dummy, rb, vnode_cmp); }
/* * Create a hash value from an object id. The result is same as sd_hash(&oid, * sizeof(oid)) but this function is a bit faster. */ static inline uint64_t sd_hash_oid(uint64_t oid) { return sd_hash_64(oid); }
/* 64 bit FNV-1a non-zero initial basis */
#define FNV1A_64_INIT ((uint64_t) 0xcbf29ce484222325ULL)
#define FNV_64_PRIME ((uint64_t) 0x100000001b3ULL
static inline uint64_t sd_hash_64(uint64_t oid) { uint64_t hval = fnv_64a_64(oid, FNV1A_64_INIT); return fnv_64a_64(hval, hval); }
1 /* 就是FNV-1a的實現 2 * The result is same as fnv_64a_buf(&oid, sizeof(oid), hval) but this function 3 * is a bit faster. 4 */ 5 static inline uint64_t fnv_64a_64(uint64_t oid, uint64_t hval) 6 { 7 hval ^= oid & 0xff; 8 hval *= FNV_64_PRIME; 9 hval ^= oid >> 8 & 0xff; 10 hval *= FNV_64_PRIME; 11 hval ^= oid >> 16 & 0xff; 12 hval *= FNV_64_PRIME; 13 hval ^= oid >> 24 & 0xff; 14 hval *= FNV_64_PRIME; 15 hval ^= oid >> 32 & 0xff; 16 hval *= FNV_64_PRIME; 17 hval ^= oid >> 40 & 0xff; 18 hval *= FNV_64_PRIME; 19 hval ^= oid >> 48 & 0xff; 20 hval *= FNV_64_PRIME; 21 hval ^= oid >> 56 & 0xff; 22 hval *= FNV_64_PRIME; 23 24 return hval; 25 }
1 static inline void 2 disks_to_vnodes(struct rb_root *nroot, struct rb_root *vroot) 3 { 4 struct sd_node *n; 5 6 rb_for_each_entry(n, nroot, rb) 7 n->nr_vnodes = node_disk_to_vnodes(n, vroot); 8 } 9 10 11 static inline void 12 node_to_vnodes(const struct sd_node *n, struct rb_root *vroot) 13 { 14 uint64_t hval = sd_hash(&n->nid, offsetof(typeof(n->nid), 15 io_addr)); 16 17 for (int i = 0; i < n->nr_vnodes; i++) { 18 struct sd_vnode *v = xmalloc(sizeof(*v)); 19 20 hval = sd_hash_next(hval); 21 v->hash = hval; 22 v->node = n; 23 if (unlikely(rb_insert(vroot, v, rb, vnode_cmp))) 24 panic("vdisk hash collison"); 25 } 26 } 27 28 static inline void 29 nodes_to_vnodes(struct rb_root *nroot, struct rb_root *vroot) 30 { 31 struct sd_node *n; 32 33 rb_for_each_entry(n, nroot, rb) 34 node_to_vnodes(n, vroot); 35 }
參考資料: