2021SC@SDUSC
Seafile对象存储
一些Seafile对象需要持久化至硬盘。它们的存储方式有两种,一种是非关系型存储,另一种是关系型存储。前者要求对象必须含有id。
非关系型存储
非关系型存储主要被Seafile文件管理系统所使用。因为Seafile文件管理系统是分布式的,而非关系型存储能为分布式存储提供良好的基础。
相关代码的结构如下:
与块系统类似,并且有多种实现方式。
使用这种方式存储的的Seafile对象包括:commit、seafile、seafdir、seafdirent等。
-
对象的逻辑位置
只考虑Seafile文件管理系统中的Seafile对象,我们可以通过以下信息定位一个Seafile对象:
-
repo_id
仓库存储id。与块的store_id相同。
-
version
仓库的seafile版本。
-
obj_id
对象id。是一个UUID。
-
-
对象序列化
要将对象以非关系的形式存储到硬盘,则需要先进行序列化,将其转化为字节流。每个使用非关系存储的对象必须实现序列化与反序列化。实现方式有以下两种:
-
结构体字节对齐
这是seafile版本0中的方法。使用如下标识,告诉编译器将结构体字节对齐:
__attribute__ ((packed)) update_pack_t;
然后用虚指针和sizeof运算符可以直接获取对象字节流。
-
json字符串
这是seafile版本1中的方法。使用jansson库生成对象对应的json对象,然后转化为json字符串。
-
-
对象存储操作
obj-backend:规定了对象进行非关系型存储所需要的抽象操作。
操作 说明 读 根据仓库id、版本、对象id,获取对象的字节流。 写 根据仓库id、版本、对象id,写入对象的字节流。支持备份与同步。 存在 根据仓库id、版本、对象id,判断是否存在。 删除 根据仓库id、版本、对象id,删除对象字节流。 遍历仓库 根据仓库id、版本,遍历仓库中的所有对象字节流。 复制 给定对象id,将其从某个版本的某个仓库复制到另一个仓库。 清空 移除某个仓库内的所有对象字节流。
对象存储器
obj-store:对象存储器是对对象后台的进一步封装。
目前基于的是文件系统实现的后台。可由SeafileSession和对象类型生成对象存储器。
基于文件系统实现
项目中实现了一个基于文件系统的非关系型存储系统。
-
对象的物理位置
对象的物理存储位置如下:
[obj_dir] / [repo_id] / [obj_id[:2]] / [obj_id[2:]]
其中
obj_dir
由对象存储器给定,格式如下:[seaf_dir] / storage / [obj_type]
- seaf_dir:即配置文件中的
seafile_dir
; - obj_type:Seafile对象类型。(例如
fs
表示文件系统相关对象)
- seaf_dir:即配置文件中的
-
对象存储操作的实现
主要利用的是文件系统调用,略。
基于riak实现
Riak是一个分布式NoSQL数据存储系统,使用键值存储。
关系型存储
部分Seafile对象采用的是关系型存储方式。相关代码的结构如下:
支持的数据库有:sqlite、mysql。编译时只能选择一个,默认为sqlite。(源码中还定义了pgsql,但未实现)
使用这种方式存储的的Seafile对象包括:branch、group等。
关系化对象
-
对象的数据库和表
-
数据库
依照对象从属的系统,规定对象被存储的数据库。整个项目有两个数据库:seafile(用于seafile系统)、ccnet(用于ccnet系统)。
-
表
依照对象类型,创建表。如SeafBranch对象对应了Branch表。
-
-
对象关系映射
读写对象本质上就是读写数据库。相关对象中借助数据库操作的抽象封装,对自身的各个属性实现了双向数据流动。
项目中整个部分是面向过程的,没有统一的ORM。结构体中通过一个私有指针指向数据库上下文(SeafDB),每次读写都对应了一个SQL和一个数据库操作。
数据库操作
-
上下文
- SeafDB:Seafile数据库上下文。
- SeafRow:Seafile数据库行上下文。
- SeafTrans:Seafile数据库事务上下文。
- CcnetDB、CcnetRow、CcnetTrans:对应Ccnet数据库。
-
抽象操作
操作 说明 获取连接 (根据配置)连接数据库并获取SeafDB。 释放连接 断开连接并释放SeafDB。 非预编译执行SQL 直接执行SQL。 执行SQL 输入SQL、参数列表,然后预编译并执行。 遍历行 输入SQL、参数列表,然后预编译并执行;随后遍历各个行,传递给回调函数 获取列数 获取一行中的列的数量 获取字符串属性 给定行,列的索引,获取字符串属性 获取整型属性 给定行,列的索引,获取整型属性 获取长整型属性 给定行,列的索引,获取长整型属性 -
操作封装
seaf-db:对数据库抽象操作进行进一步的封装,以适配本项目的需求。
seaf-utils:提供了一些实用接口,包括根据会话配置创建数据库连接(SeafDB)。
基于Mysql实现
seaf-db.c中通过Mysql API实现了数据库抽象操作。
关于常规数据库操作的实现略,因为这些操作大都是对API进行的一定程度的封装。Mysql实现中主要关注的是多连接管理。因为当一个连建长时间无操作时,有可能断线,所以需要去反复发出ping信号,防止连接断开。项目中的实现方式是通过线程池进行线程调度。每个连接占有一个线程,这些线程通过一个互斥锁来争夺ping的资源。默认每30s进行一次ping。Mysql实现中不需要考虑数据库内的并发,因为Mysql已经实现了锁机制。
基于Sqlite实现
seaf-db.c中通过sqlite API实现了数据库抽象操作。
关于常规数据库操作的实现略,主要关注数据库并发处理。sqlite提供了一个解锁通知的接口sqlite3_unlock_notify
,每次sqlite数据库解锁时,会调用用户提供的回调函数。因此并发机制主要靠用户实现。
项目中主要通过wait_for_unlock_notify
函数进行互斥并发,每次执行SQL前将调用该方法等待sqlite解锁:
typedef struct UnlockNotification { // 通知上下文
int fired; // 是否已经通知
pthread_cond_t cond; // 条件变量
pthread_mutex_t mutex; // 条件变量的互斥锁
} UnlockNotification;
static void
unlock_notify_cb(void **ap_arg, int n_arg) // 回调函数
{
int i;
for (i = 0; i < n_arg; i++) { // 遍历用户参数
UnlockNotification *p = (UnlockNotification *)ap_arg[i];
pthread_mutex_lock (&p->mutex);
p->fired = 1; // 表示已经通知
pthread_cond_signal (&p->cond); // 条件变量发送信号
pthread_mutex_unlock (&p->mutex);
}
}
static int
wait_for_unlock_notify(sqlite3 *db)
{
UnlockNotification un;
un.fired = 0;
pthread_mutex_init (&un.mutex, NULL);
pthread_cond_init (&un.cond, NULL);
int rc = sqlite3_unlock_notify(db, unlock_notify_cb, (void *)&un); // 并发通知
if (rc == SQLITE_OK) {
pthread_mutex_lock(&un.mutex);
if (!un.fired) // 若已经通知,则跳过等待
pthread_cond_wait (&un.cond, &un.mutex); // 否则等待条件变量发送信号
pthread_mutex_unlock(&un.mutex);
}
pthread_cond_destroy (&un.cond);
pthread_mutex_destroy (&un.mutex);
return rc;
}