ViveNAS - 一个基于LSM tree的文件存储实现 (一)

news/2025/3/28 16:15:55/

 1. ViveNAS (GitHub - cocalele/ViveNAS)

ViveNAS 是一个开源分布式的网络文件系统(NAS), 具有下面的特点:
 - 通过不同存储介质的结合,在高性能、低成本间寻找动态的平衡
 - 解决数据的长期、低成本存储问题,支持磁带,SMR HDD等低成本介质,以及EC
 - 为CXL内存池、SCM等新技术的应用做好准备,并应用这些新技术产生澎湃性能,服务热点数据
 - 解决小文件存储难题
 - 为企业存储提供受控的分布式策略,以解决传统分布式存储在扩容、均衡、故障恢复时面临的各种难题
 - 绿色存储,充分利用数据中心超配而不能充分利用的内存、CPU资源提供服务,降低能源消耗

ViveNAS目前版本提供标准NFS接口。


ViveNAS提供上述能力的核心技术依赖于下列两项:

核心1,PureFlash 分布式SAN存储

   PureFlash 提供了我们这个存储系统所有跟分布式有关的特性,包括高可用机制、故障恢复机制、存储虚拟化、快照、克隆等。

   PureFlash是一个分布式的ServerSAN存储系统,他的核心思想继承自NetBRIC S5,一个以全FPGA硬件实现的全闪存储系统,因此PureFlash拥有一个极度简单的IO栈,最小的IO开销。

   区别与以hash算法为基础的分布式系统,PureFlash的数据分布是完全人为可控的,这提供了企业存储在运行时所需的稳定能力,因为数据分布的掌控权最终在“人”而不在“机器”。更多细节请参看github.com/cocalele/PureFlash

   PureFlash支持在一个集群里管理不同的存储质,包括从NVMe SSD、HDD、磁带,以及AOF文件访问,

   上述的这些为ViveNAS提供了坚实的数据存储保障。

核心2,以LSM tree为基础的ViveFS

   LSM tree有两个重要特点,一是多层级,二是每个层级都是顺序写。

   ViveFS将level 0 放在内存或者CXL内存池里,将其他层级放在PureFlash提供的不同存储介质里,而且每个层级都是分布式且具有高可靠性。

   顺序写这个特性对磁带/smr hdd介质非常的友好,这样ViveNAS就可以将比较低层级的数据放到这些低成本介质上进行长期存储。这也是ViveNAS最重要的目标,解决冷数据的长期存储成本与访问复杂难题,
   同时顺序写的AOF文件对ec也是很友好的,通过大因子EC可以进一步降低存储成本。
   

2. ViveNAS的 层级架构


 从上图可以看到,从一个最底层的SSD介质到方便使用的NAS文件服务中间需要很多层级的协作。

本文重点介绍里面的libvivefs的实现原理。正如libvivefs在图中的位置,他的下层是LSM tree, 在当前的实现里面我直接选用了rocksdb。他的上层对接到ganesha FSAL,提供标准的文件语义接口。标准文件语义,但并不是标准Posix接口,也没有对接在linux VFS之下。libvivefs提供的API包括:


inode_no_t vn_lookup_inode_no(struct ViveFsContext* ctx, inode_no_t parent_inode_no, const char* file_name, /*out*/struct  ViveInode** inode);
inode_no_t vn_create_file(struct ViveFsContext* ctx, inode_no_t parent_inode_no, const char* file_name, int16_t mode, int16_t uid, int16_t gid, /*out*/ struct ViveInode** inode_out);
struct ViveFile* vn_open_file(struct ViveFsContext* ctx, inode_no_t parent_inode_no, const char* file_name, int32_t flags, int16_t mode);
struct ViveFile* vn_open_file_by_inode(struct ViveFsContext* ctx, struct ViveInode* inode, int32_t flags, int16_t mode);
size_t vn_write(struct ViveFsContext* ctx, struct ViveFile* file, const char* in_buf, size_t len, off_t offset);
size_t vn_writev(struct ViveFsContext* ctx, struct ViveFile* file, struct iovec in_iov[], int iov_cnt, off_t offset);
size_t vn_read(struct ViveFsContext* ctx, struct ViveFile* file, char* out_buf, size_t len, off_t offset);
size_t vn_readv(struct ViveFsContext* ctx, struct ViveFile* file, struct iovec out_iov[] , int iov_cnt, off_t offset);
struct vn_inode_iterator* vn_begin_iterate_dir(struct ViveFsContext* ctx, int64_t parent_inode_no);
struct ViveInode* vn_next_inode(struct ViveFsContext* ctx, struct vn_inode_iterator* it, char* entry_name, size_t buf_len);
int /*as bool*/ vn_iterator_has_next(struct vn_inode_iterator* it);
void vn_release_iterator(struct ViveFsContext* ctx, struct vn_inode_iterator* it);
int vn_fsync(struct ViveFsContext* ctx, struct ViveFile* file);
int vn_close_file(struct ViveFsContext* ctx, struct ViveFile* file);
int vn_unlink(struct ViveFsContext* ctx, int64_t parent_ino, const char* fname);
int vn_rename_file(struct ViveFsContext* ctx, inode_no_t old_dir_ino, const char* old_name, inode_no_t new_dir_ino, const char* new_name);
struct ViveFsContext* vn_mount(const char* db_path);
int vn_umount(struct ViveFsContext* ctx);

里面的open/close, read/write类函数想必大家都不陌生,只是换了个名字,毕竟跟系统的标准API重名会自找麻烦。还有一些函数,包括lookup_inode, iterator, unlink这些函数,研究过linux内核文件系统实现的朋友也一定都见到过。

上面的这组接口就是libvivefs想要提供出来能力。他的下层rocksdb提供的接口也是相当的简洁:put/get, prefix_iterator, merge, transaction commit。

put/get已经能够实现数据的写入、读出。但是put/get是KV操作,是对目标对象整体的访问。对于文件语义必须要提供随机IO能力,也就是指定任意Offset, 操作任意长度的数据。libvivefs的作用就是弥合这二者间的差异。

3. libvivefs的数据布局

看一个文件系统的原理一定要看他的磁盘布局,磁盘布局决定了需要什么样的算法。磁盘布局跟介质密切相关,一定要按介质特点设计,因此针对机械盘,NAND flash, pmem, 磁带等有各种适配文件系统。

libvivefs在rocksdb里创建两个column family, 分别储存文件系统的元数据和数据。数据CF (column family)顾名思义就是文件的内容了,其K-V是这样的:

Value是文件内容按64KB大小切割的数据块加上extent_head,Key是extent的标识。

Key 由16Byte的binary构成,结构如下:

struct vn_extent_key{union { struct {_le64 extent_index;_le64 inode_no;};char keybuf[16];}
}

`vn_extent_key` 结构里的两个关键变量:

inode_no: 也就是Inode号,这和其他文件系统对inode号的定义一样,唯一标识一个文件,并且不会随着文件改名而变化。

extent_index:表示这个数据块是文件的第几个数据块。也就是说这些数据块顺次保存了文件的内容。

extent_head的构成如下:

struct vn_extent_head {int8_t flags;int8_t pad0;union {uint16_t data_bmp;uint16_t merge_off;};char pad1[12];
};

有了extent_head的存在就能够支持任意长度的写入,而不必等凑满整个extent才能写入。这个能力也是借助了rocksdb的merge操作实现的。

反正LSM-tree是需要对数据进行compaction,因此写入的时候没必要每次都进行RMW(Read-Modify-Write),只需要将改动的部分数据写入。在compaction或者read的时候,会触发merge操作将同一个key的不同数据分片、版本合并成一个完整的extent。extent_head里面的信息在merge操作时会被用到。

4. write操作的实现

有了上面的数据布局知识,write操作的实现就很明显了,将数据按照在文件中的位置找到对应的extent,按照写入是否写满了整个extent分别调用mege或者put操作。

size_t vn_write(struct ViveFsContext* ctx, struct ViveFile* file, const char * in_buf, size_t len, off_t offset )
{int64_t start_ext = offset / file->inode->i_extent_size;int64_t end_ext = (offset + len + file->inode->i_extent_size - 1) / file->inode->i_extent_size;void* buf = malloc(file->inode->i_extent_size + PFS_EXTENT_HEAD_SIZE);S5LOG_DEBUG("call vn_write, len:%ld off:%ld", len, offset);DeferCall _1([buf]() {free(buf); });struct vn_extent_head* head = (struct vn_extent_head*)buf;Transaction* tx = ctx->db->BeginTransaction(ctx->data_opt);DeferCall _2([tx]() {delete tx; });Cleaner _c;Status s;_c.push_back([tx]() {tx->Rollback(); });int64_t buf_offset = 0;for (int64_t index = start_ext; index < end_ext; index++) {//string ext_key = format_string("%ld_%ld", file->i_no, index);vn_extent_key ext_key = { {{extent_index: (__le64)index, inode_no : (__le64)file->i_no}} };int64_t start_off = (offset + buf_offset) % file->inode->i_extent_size; //offset in extentsize_t segment_len = std::min(len - buf_offset, (size_t)file->inode->i_extent_size - start_off);*head = { 0 };memcpy((char*)buf + PFS_EXTENT_HEAD_SIZE, in_buf + buf_offset, segment_len);Slice segment_data((char*)buf, segment_len + PFS_EXTENT_HEAD_SIZE);if(segment_len != file->inode->i_extent_size){//不是满条带,就执行Merge操作head->merge_off = (uint16_t)start_off;s = tx->Merge(ctx->data_cf, Slice((const char*)&ext_key, sizeof(ext_key)), segment_data);} else {//满条带,就执行Put操作head->data_bmp = (uint16_t)start_off; s = tx->Put(ctx->data_cf, Slice((const char*)&ext_key, sizeof(ext_key)), segment_data);}buf_offset += segment_len;}file->inode->i_mtime = time(NULL);file->dirty = 1;if (offset + len > file->inode->i_size) {//如果文件长度也发生了改变,就更新长度。 否则会现在lazy 模式更新元数据,也就是上面的修改时间不会每次更新。file->inode->i_size = offset + len;s = _vn_persist_inode(ctx, tx, file->inode);file->dirty = 0;}s = tx->Commit();_c.cancel_all();return len;
}

read操作的实现逻辑和write相似且更简单。


http://www.ppmy.cn/news/55773.html

相关文章

使用docker搭建RocketMQ(非集群搭建官方镜像)

之前在使用 RocketMQ 官方的包在搭建的时候&#xff0c;发现好多问题&#xff0c;什么修改内存大小&#xff0c;然后启动 broker 报错&#xff0c;类似 service not available now, maybe disk full 等等… 最后决定还是重新用 docker 搭建下&#xff0c;感觉这样子玩坏了&…

LeetCode第160题——相交链表(Java)

题目描述&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交**&#xff1a;** 题目数据 保证 整个链式结构中不存在环。 注意&…

Python 3 实现给定的DNA 序列转换为其蛋白质等价物

相关Python源码和数据集下载:https://download.csdn.net/download/tianqiquan/87738198 DNA ⇒ RNA ⇒ 蛋白质 生命取决于细胞存储、检索和翻译遗传指令的能力。这些指令是制造和维持活生物体所必需的。很长一段时间,都不清楚哪些分子能够复制和传递遗传信息。我们现在知道…

【Auto-GPT云部署】

部署自己的Auto-Gpt 先说说什么是Chat-Gpt Chat-GPT (Generative Pretrained Transformer)是由OpenAI提出的一种自然语言处理技术&#xff0c;是基于Transformers和预训练机制的大规模语言模型。与传统的基于规则或基于统计的自然语言处理方法不同&#xff0c;Chat-GPT使用深…

Linux下的常用命令

ls 列出目前工作目录所含之文件及子目录mkdir -p 创建多级文件夹&#xff0c;例如&#xff1a; test01/test02/test03 mkdir -p test01/test02/test03top 查看运行的所有进程 top之后按c 可以查看具体的位置&#xff0c; 按1&#xff0c;可以查看cupps aux --sort rss 按照正序…

Java入门教程||Java 封装||Java 接口

Java 封装 在面向对象程式设计方法中&#xff0c;封装&#xff08;英语&#xff1a;Encapsulation&#xff09;是指&#xff0c;一种将抽象性函式接口的实作细节部份包装、隐藏起来的方法。 封装可以被认为是一个保护屏障&#xff0c;防止该类的代码和数据被外部类定义的代码…

2023有潜力的新药都有哪些?新药筛选方法总结

2023有潜力的新药都有哪些&#xff1f;这是一道蕴含无限可能性和未知挑战的问题。新药研发是制药公司不断追求的目标&#xff0c;每一次成功都可以带来巨额利润&#xff0c;改善患者生命质量&#xff0c;成就公司声誉。但与此同时&#xff0c;新药研发风险也是极大的&#xff0…

ESP8266使用SDK软硬件定时执行函数

1、软件定时 以下接口使用的定时器由软件实现&#xff0c;定时器的函数在任务中被执行。因为任务可能被中断&#xff0c;或者被其他高优先级的任务延迟&#xff0c;因此以下os_timer系列的接口并不能保证定时器精确执行。 注意&#xff1a; ①对于同一个 timer&#xff0c;os…