sheepdog调研学习
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 }
参考资料: