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 }
参考资料:
1. 分布式存储系统sheepdog
2. 分布式系统sheepdog之sheep启动流程
3. centos7下sheepdog的简单使用