[toc]
TierFS特点:TierFS是一个学习性质的项目。具有多个加速层,一个持久化层。本质上是在持久化层上面加上一个不需要一致性保证的缓存系统。性能来源是基于DRAM的缓存加速机制。 并发支持:考虑支持多个线程同时对一个文件进行读,不支持多个线程对一个文件同时写。不支持一个线程读正在写的文件。
TierFS在实现上:
-
默认加速层足够大,不会出现缓存溢出。
-
不考虑崩溃一致性。
-
不进行实时的设备压力测试,按固定比例分配CXL-Mem和DIMM-DRAM。
实现难点
-
多级缓存缓存的管理。
-
后台异步刷新可能会引起的线程同步问题。
-
libcapstone,TierFS在拦截系统调用时使用了Intel的syscall_intercept,libcapstone是syscall_intercept的依赖。
-
glib2, TierFS在用户态使用C语言实现,由于C语言功能太弱,缺少高级语言自带的很多标准库,TierFS在实现上依赖glib2提供的基础的数据结构和并发控制。运行TierFS前需要确保系统正确安装了glib2。
复用底层成熟文件系统的目录管理,在持久化层没有新的目录机制。意味着不拦截SYS_mkdir, SYS_rmdir, SYS_rename 这些系统调用。
用户态文件类型nufs_file_t,每一个nufs_file 表示一个文件,由于一个文件可能会有多个文件描述符fd,所以在TierFS中,文件和文件描述符fd单独管理,TierFS的文件使用GTree *nufs_files;进行维护。当文件第一次打开时创建对应的nufs_file_t,并放到nufs_files中,文件的最后一个文件描述符被关闭时,从nufs_files 中删除对应的nufs_file_t。
在nufs_file_t中保存有文件大小,文件的缓存数据,文件的引用计数cnt。文件相关的锁GMutex mutex;
typedef struct {
char *name;
size_t size; // File size
GMutex mutex;
int is_idle;
int cnt;
// todo 读的时候要保证不进行cache的状态转换。
volatile int read_hint;
GCond read_cond;
nufs_dram_cache_t *updated_write_cache;
nufs_dram_cache_t *staged_write_cache;
} nufs_file_t;用户态文件描述符,nufs_fd_t。文件描述符使用数组维护,在TierFS由一个全局的数组nufs_fd_t nufs_fds[NUFS_FD_MAX]; 来维护,方便直接根据fd 查找到对应的nufs_fd_t。
typedef struct {
int fd;
int is_nufs; // 判断是否是nufs的fd
int ready_to_close;
// 后台刷新可能会占用文件描述符, 刷新过程要阻塞close。
volatile int fd_busy_hint;
GMutex fd_mutex;
GCond fd_free_cond;
nufs_file_t *file;
off_t offset;
} nufs_fd_t;Tier的写缓存使用基于红黑树的区间树( https://github.com/markfasheh/interval-tree )来维护,在Tier中有一个nufs_dram_cache_t 结构体来表示写缓存,其中包括两个字段,区间树的根节点和更新的大小,每个文件nufs_file_t有两个写缓存,分别是updated_write_cache 和staged_write_cache :
typedef struct {
// 基于RBtree的区间树,区间都是闭区间,左闭右闭
struct rb_root root;
size_t updated_size;
} nufs_dram_cache_t;
typedef struct {
....
nufs_dram_cache_t *updated_write_cache;
nufs_dram_cache_t *staged_write_cache;
....
} nufs_file_t;TierFS的所有写操作先进入到updated_write_cache写缓存。在updated_write_cache 数据写入足够多的数据并且此时这个文件没有staged_write_cache时,会将updated_write_cache 转化成该文件的staged_write_cache 并在线城池中创建后台持久化的任务。
TierFS使用了两个线程池来处理后台任务, 其中persistent_task_thread_pool 处理缓存到持久化设备的持久化任务,close_task_thread_pool 处理对文件描述符的关闭任务,其中persistent_task_thread_pool 的大小可以根据需要配置,但是为了保证所有的close操作顺序执行close_task_thread_pool 的大小必须固定是1。
把close操作单独拿出来是因为文件在调用close时,可能会存在一部分数据正在刷新到PMEM,或者还未开始刷新到PMEM的情况。
由于后台刷新的过程可能和文件读写并行进行,后台刷新和状态的过程和读写过程会相互影响,需要做同步控制,TierFS的同步机制使用glib2中的条件变量进行控制。在TierFS中的读写需要满足:
-
读的过程不能进行cache的状态转换,即读会阻塞write cache到staged cache的转换
-
写的过程不能进行状态转换,即写会阻塞write cache到staged cache的转换
-
状态转换时不能进行读写,即write cache到staged cache的转换会阻塞读写
需要特别强调的是,由于一个文件可能被打开多次,可能会对应多个fd,为了简单起见,只考虑所有的fd有相同的读写权限,这个时候分两种情况(1)close的fd并不是这个文件最后的fd。(2)这个fd是这个文件对应的最后一个fd。不管是那种情况,将fd 关闭(close)后,fd就不能使用,这个时候如果后台使用这个fd进行的所有刷新任务都会失败。而第(2)种情况,需要将所有缓存中的数据刷新到持久化设备中。为了保证close不会阻塞应用程序,并且不会出现错误,Tier的close使用单独的具有1个线程的线程池完成,使用1个线程的线程池可以保证所有的close都是异步并且顺序执行的。在对close的同步控制上满足:
-
有后台任务正在使用某个fd进行刷新时不能进行这个fd的close。
-
如果这个fd是该文件的最后一个fd,需要创建后台刷新任务,等所有任务顺利执行后再进行close。
-
所有的close异步并且顺序执行。
GThreadPool *persistent_task_thread_pool;
GThreadPool *close_task_thread_pool;
void nufs_g_persistent_task_func(gpointer data, gpointer user_data) {
nufs_persistent_task_info_t *info = (nufs_persistent_task_info_t *)data;
int fd = info->fd;
g_mutex_lock(&nufs_fds[fd].fd_mutex);
nufs_fds[fd].fd_busy_hint = 1;
g_mutex_unlock(&nufs_fds[fd].fd_mutex);
nufs_dram_cache_t *cache = info->cache;
nufs_cache_persist(cache, fd);
g_mutex_lock(&nufs_fds[fd].fd_mutex);
nufs_fds[fd].fd_busy_hint = 0;
g_cond_signal(&nufs_fds[fd].fd_free_cond);
g_mutex_unlock(&nufs_fds[fd].fd_mutex);
g_mutex_lock(&nufs_fds[fd].file->mutex);
while (nufs_fds[fd].file->read_hint) {
g_cond_wait(&nufs_fds[fd].file->read_cond, &nufs_fds[fd].file->mutex);
}
nufs_fds[fd].file->staged_write_cache = NULL;
g_mutex_unlock(&nufs_fds[fd].file->mutex);
free_interval_tree(&cache->root);
free(info);
}
void nufs_g_close_task_func(gpointer data, gpointer user_data) {
nufs_close_task_info_t *close_info = (nufs_close_task_info_t *)data;
nufs_fd_t *nufs_fd = &nufs_fds[close_info->fd;
g_mutex_lock(&nufs_fd->fd_mutex);
while (nufs_fd->fd_busy_hint) {
g_cond_wait(&nufs_fd->fd_free_cond, &nufs_fd->fd_mutex);
}
if (!close_info->is_file_clear) {
original_close(nufs_fd->fd);
} else {
cache_persist(nufs_fd->file->staged_write_cache, nufs_fd->fd);
nufs_fd->file->staged_write_cache = NULL;
cache_persist(nufs_fd->file->updated_write_cache, nufs_fd->fd);
nufs_fd->file->updated_write_cache = NULL;
original_close(nufs_fd->fd);
nufs_free_file(close_info->file);
}
g_mutex_unlock(&nufs_fd->fd_mutex);
}fsync: 阻塞式接口,手动把对应fd的所有cache刷新到底层持久设备中。
文件系统的Hot/Cold识别和处理可以分成两种,1. 将文件分成Hot/Cold文件,不同的文件放到不同的设别或把Hot文件进行缓存。2. 将一个文件内数据块做Hot和Cold的区分,Hot/Cold块放到不同的设备或把Hot块进行缓存。
TierFS的Hot/Cold为第二种,对同一个文件中的Hot/Cold块进行区分并且缓存Hot块。
为了实现的简单,TierFS只的Hot缓存只关注读过程的缓存,TierFS的Hot/Cold主要机制:
-
Hot/Cold主要优化读性能,TierFS的Hot/Cold统计和识别只关注读的过程。
-
使用简单阈值动态识别Hot/Cold页面。
-
Hot/Cold独立于两层基于区间树的A-B write缓存,Hot/Cold缓存使用哈希表维护。
-
使用TierFS读文件时会统计块的读次数,如果该块的读次数超过一定阈值,将其放入Hot缓存。读文件数据时先走Hot 缓存。
-
写文件数据时清理对应Hot缓存。
-
调用底层 original_open 打开文件,获取真实 fd。
-
在全局
nufs_files树中查找文件元数据(nufs_file_t),没有则新建并插入。 -
增加文件引用计数。
-
在
nufs_fds表中记录 fd 相关信息(如偏移、是否为 nufs 文件等)。
-
加锁,设置
read_hint,防止并发的写缓存状态转换。 -
统计访问页数,若某页访问次数超过阈值,则建立影子页缓存(热点页)。
-
优先从影子页读取数据,剩余部分再从 DRAM 缓存和底层存储读取。
-
读完后解锁,清除
read_hint。
-
写入 DRAM 缓存(
updated_write_cache)。 -
若缓存达到阈值且没有正在持久化的缓存,则触发缓存分阶段持久化(异步写回底层存储)。
-
写入后更新文件大小。
-
写入时会清除对应的影子页,保证一致性。
-
文件引用计数减一。
-
若引用计数为零,移除元数据并异步持久化缓存、释放资源。
-
所有关闭操作通过线程池异步执行,避免阻塞主线程。
SSD上的测试,底层使用ext4文件系统,O_DIRECT方式的读写性能:
num73@num73-pclab:~/TierFS/gtest$ ./test1
Creating test file of size 1073741824 bytes...
Create Success!
Sequential read throughput: 28.02 MB/s
Sequential write throughput: 123.32 MB/s
Random read throughput: 21.15 MB/s
Random write throughput: 136.85 MB/s使用了TierFS中间层的结果:
num73@num73-pclab:~/TierFS/gtest$ ./run_nufs.sh ./test1
NUFS: initialized.
Creating test file of size 1073741824 bytes...
Create Success!
Sequential read throughput: 2866.99 MB/s
Sequential write throughput: 2084.52 MB/s
Random read throughput: 2581.76 MB/s
Random write throughput: 1897.85 MB/s
NUFS: destructing...对比可以看到提升大很,对比原有ext4文件系统提升了13x-122x