[转]virtiofs
转自 https://virtio-fs.gitlab.io/howto-qemu.html
https://heychenbin.github.io/post/kata_with_virtiofs/
https://kernel.taobao.org/2019/11/virtio-fs-intro-and-perf-optimize/
1. 当前安全容器存储领域的问题
在介绍virtio-fs之前,我们先来了解一下当前安全容器存储领域遇到的问题,因为只有在理解了所要解决的问题才能更好的理解解决问题的方案。
在当前安全容器领域,Kata Containers可以说是最被广泛应用的容器技术了。Kata Containers使用轻量级虚拟机和硬件虚拟化技术来提供更强隔离,以构建安全的容器运行时。但也正是因为使用了虚拟机,容器的根文件系统(容器rootfs)无法像runc那样直接使用主机上构建好的目录,而需要有一种方法把host上的目录共享给guest。
目前基本上只有两种流派的方法能够透传host目录/数据给guest,一种是基于文件的方案,一个是基于块设备的方案。而这两种流派又都有各自有自身的优缺点,这里分别以9pfs和devicemapper为例来说明。
- 基于文件的9pfs
- 优点:因为使用host上overlayfs,能充分利用host pagecache;部署简单,不需要额外的组件
- 缺点:基于网络的协议,性能差;POSIX语义兼容性方面不好(主要体现在mmap(2)的支持上,在特殊情况下有数据丢失的风险)
- 基于块设备的devicemapper
- 优点:良好的性能;很好的POSIX语义兼容性
- 缺点:无法充分利用host pagecache,部署运维复杂(需要维护lvm volume)
可以看到,这两种流派优缺点基本互补。那么有没有一种解决方案能够结合两者的优点,同时能够克服两者的缺点呢?virtio-fs正是这种新的方案,为我们带来了新的曙光。
2. virtio-fs介绍
2.1 基本信息
virtio-fs是红帽在2018年12月10号在kata社区提出的一种在guest之间共享文件系统的方案。 项目主页:https://virtio-fs.gitlab.io/ 代码仓库:https://gitlab.com/virtio-fs 邮件列表:https://www.redhat.com/mailman/listinfo/virtio-fs
其主要设计目标为:
- 在不同guest之间,以快速、一致、安全的方式共享同一个host目录树结构
- 拥有较好的性能和跟本地文件系统(如ext4)一样的语义
主要使用场景:
- 在kata-container场景中替换9p,作为容器rootfs
- 为虚拟机在host和guest之间共享数据 (shared file system for virtual machines)
- File-system-as-a-Service,更安全
2.2 原理与架构设计
virtio-fs方案使用FUSE协议在host和guest之间通信。在host端实现一个fuse server操作host上的文件,然后把guest kernel当作fuse client在guest内挂载fuse,server和client之间使用virtio来做传输层来承载FUSE协议,而不是传统结构上的/dev/fuse设备。为了支持在不同guest中同时mmap(MAP_SHARED)同一个文件,virtio-fs把文件mmap进qemu的进程地址空间并让不同guest使用DAX访问该内存空间,这样就绕过了guest pagecache达到不同guest都访问同一份数据的目的,同时也在多个guest之间共享了内存,节省了内存资源。
简要架构图:
从图中我们可以了解到,virtio-fs主要由以下几个组件组成:
- guest kernel:作为fuse client来挂载host上导出的目录
- qemu:新添加的vhost-user-fs-pci设备用于在guest kernel和virtiofsd之间建立起vhost-user连接
- virtiofsd(同样在qemu仓库中):host上运行的基于libfuse开发的fuse daemon,用于向guest提供fuse服务
下图是更详细的架构图
virtio-fs跟其他基于文件的方案(比如9pfs或者NFS)相比,其独特之处在于virtio-fs充分利用了虚拟机和hypervisor同时部署在一个host上的特点以避免昂贵的VMEXITS。具体来说就是,DAX数据访问和元数据的共享内存访问都是通过共享内存的方式避免不必要的VM/hypervisor之间通信(在元数据没有改变的情况下),而共享内存访问也比基于网络文件系统协议访问要更轻量级也有更好的本地文件系统语义和一致性。这也是基于FUSE设计virtio-fs而不是基于其他网络文件系统协议做改进的原因。
了解virtio-fs原理与设计架构之后,我们可以总结出virtio-fs的几条设计要点,看其是如何同时拥有9pfs和devicemapper方案的优点,同时克服它们的缺点的:
- FUSE协议而不是基于网络的协议:更快的性能,更好的POSIX语义兼容性
- 独立的virtiofsd进程:更安全,且不需要维护单独的块设备(相比9pfs多了一个组件,但维护成本比devicemapper还是要小很多)
- Host/guest之间共享内存(DAX):进一步提升性能,同时节省内存资源
整体架构
virtio-fs整体架构如下图所示:
总体来说, virtio-fs主要依赖于FUSE文件系统, FUSE文件系统本来是作为用户态文件系统的内核实现, 但是virtio-fs对其进行了一些改造, 在此基础上拓展成了virtio-fs文件系统.
在主机端, virtio-fs使用libfuse实现了一个用户态文件系统, 该程序名为virtiofsd, 每个虚拟机容器对应一个该进程. virtiofsd与GuestOS中的virtio-fs使用vhost-user协议进行通信. vhost-user包含两条通道, 一条通道为控制通道, 一条通道为数据通道. 控制通道通过Unix Socket进行通信, 数据通道按照virtio方式, 使用共享内存的方式进行通信.
为了提高效率, 在GuestOS中, virtio-fs还实现了DAX(Direct Access), 将用户态空间中的文件通过内存映射的方式映射到GuestOS中的块设备地址中, 达到提升读取效率的目的. 在测试数据中也可以看到, 读取效率还是有一定提升, 特别是对小文件的读写相比9pfs有了比较大的提高.
GuestOS端实现
在GuestOS端主要是对内核的修改, 主要代码都集中在fs/fuse/文件夹下, 初始化代码为fs/fuse/virtio_fs.c:
1. virtio-fs在执行probe操作时, 调用virtio_fs_setup_vqs初始化virtqueue, 还会注册virtio中断处理函数, 用于接收由virtfsd中发来的数据.
1 static int virtio_fs_probe(struct virtio_device *vdev) 2 { 3 struct virtio_fs *fs; 4 int ret; 5 6 fs = devm_kzalloc(&vdev->dev, sizeof(*fs), GFP_KERNEL); 7 if (!fs) 8 return -ENOMEM; 9 vdev->priv = fs; 10 11 ret = virtio_fs_read_tag(vdev, fs); 12 if (ret < 0) 13 goto out; 14 15 ret = virtio_fs_setup_vqs(vdev, fs); 16 if (ret < 0) 17 goto out; 18 19 /* TODO vq affinity */ 20 /* TODO populate notifications vq */ 21 22 ret = virtio_fs_setup_dax(vdev, fs); 23 if (ret < 0) 24 goto out_vqs; 25 26 /* Bring the device online in case the filesystem is mounted and 27 * requests need to be sent before we return. 28 */ 29 virtio_device_ready(vdev); 30 31 ret = virtio_fs_add_instance(fs); 32 if (ret < 0) 33 goto out_vqs; 34 35 return 0; 36 37 out_vqs: 38 vdev->config->reset(vdev); 39 virtio_fs_cleanup_vqs(vdev, fs); 40 out: 41 vdev->priv = NULL; 42 return ret; 43 }
2. 执行mount -t virtiofs …, 系统调用最终走到vfs_kern_mount,会去调用指定文件系统的mount函数,要做的事有:
创建该文件系统的超级快对象 fill superblock,返回该文件系统的根目录(root)的dentry实例,最后将创建的超级快对象赋值给新创建的vfsmount结构所指的超级快,同时vfsmount所指的mnt_root点赋值为超级快所指的根dentry.
virtio_fs_mount(struct file_system_type *fs_type , int flags, const char *dev_name, void *raw_data)
-------mount_nodev(fs_type, flags, raw_data, virtio_fs_fill_super)
-------------struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL);// set_anon_super : Filesystems which don't use real block devices can call this function to allocate a virtual block device.
--------------error = fill_super(s, data, flags & SB_SILENT ? 1 : 0) //fill_super即 virtio_fs_fill_super
1 static struct dentry *virtio_fs_mount(struct file_system_type *fs_type, 2 int flags, const char *dev_name, 3 void *raw_data) 4 { 5 return mount_nodev(fs_type, flags, raw_data, virtio_fs_fill_super); 6 }
主要初始化代码位于函数virtio_fs_fill_super中, 该函数会调用fuse_fill_super_common, 创建和初始化fuse connection. fuse connection在原始的FUSE文件系统中, 本来是用于连接Kernel FUSE与用户态驱动程序的通道, 在virtio-fs中对这里做了修改, 将该通道替换为virtio通道直接与用户态的virtiofsd进行通信.
1 int fuse_fill_super_common(struct super_block *sb, 2 struct fuse_mount_data *mount_data) 3 { 4 // ...... 5 fc = kmalloc(sizeof(*fc), GFP_KERNEL); 6 err = -ENOMEM; 7 if (!fc) 8 goto err; 9 10 fuse_conn_init(fc, sb->s_user_ns, mount_data->dax_dev, 11 mount_data->fiq_ops, mount_data->fiq_priv); 12 fc->release = fuse_free_conn; 13 14 if (mount_data->dax_dev) { 15 err = fuse_dax_mem_range_init(fc, mount_data->dax_dev); 16 if (err) { 17 pr_debug("fuse_dax_mem_range_init() returned %d\n", err); 18 goto err_free_ranges; 19 } 20 } 21 22 fud = fuse_dev_alloc_install(fc); 23 if (!fud) 24 goto err_put_conn; 25 26 fc->dev = sb->s_dev; 27 fc->sb = sb; 28 err = fuse_bdi_init(fc, sb); 29 if (err) 30 goto err_dev_free; 31 // ...... 32 }
在fuse_fill_super_common函数中, 还判断了mount参数中是否有dax参数, 如果有dax参数, 则初始化dax. dax原理暂时还没有分析.
Qemu
在qemu中, qemu扩展了vhost-user协议, 增加了三条和virtio-fs相关的消息:
1 * VHOST_USER_SLAVE_FS_MAP 2 Id: 4 3 Equivalent ioctl: N/A 4 Slave payload: fd + n * (offset + address + len) 5 Master payload: N/A 6 7 Requests that the QEMU mmap the given fd into the virtio-fs cache; 8 multiple chunks can be mapped in one command. 9 A reply is generated indicating whether mapping succeeded. 10 11 * VHOST_USER_SLAVE_FS_UNMAP 12 Id: 5 13 Equivalent ioctl: N/A 14 Slave payload: n * (address + len) 15 Master payload: N/A 16 17 Requests that the QEMU un-mmap the given range in the virtio-fs cache; 18 multiple chunks can be unmapped in one command. 19 A reply is generated indicating whether unmapping succeeded. 20 21 * VHOST_USER_SLAVE_FS_SYNC 22 Id: 6 23 Equivalent ioctl: N/A 24 Slave payload: n * (address + len) 25 Master payload: N/A 26 27 Requests that the QEMU causes any changes to the virtio-fs cache to 28 be synchronised with the backing files. Multiple chunks can be synced 29 in one command. 30 A reply is generated indicating whether syncing succeeded.
vhost_user_backend_init()
---------........
---------vhost_setup_slave_channel(dev)
------------------.........
------------------qemu_set_fd_handler(u->slave_fd, slave_read, NULL, dev)
------------------.......
//hw/virtio/vhost-user.c中 slave_read() { ...... #ifdef CONFIG_VHOST_USER_FS case VHOST_USER_SLAVE_FS_MAP: ret = vhost_user_fs_slave_map(dev, &payload.fs, fd[0]); break; case VHOST_USER_SLAVE_FS_UNMAP: ret = vhost_user_fs_slave_unmap(dev, &payload.fs); break; case VHOST_USER_SLAVE_FS_SYNC: ret = vhost_user_fs_slave_sync(dev, &payload.fs); break; #endif ……. }
新增的三条消息都是内存映射相关的, 在qemu端, 收到内存映射消息会将用户空间文件映射到虚拟机virtio-fs缓存中, 加速GuestOS的读写速度.
在增加了virtiofsd后, qemu会作为vhost-user的Master端, 启动时, 会创建一个unix socket, virtiofsd会作为客户端连接该socket. 在qemu参数中会新增参数:
|
|
virtiofsd和qemu之间的这条通道就是作为vhost-user的控制通道, 用于在GuestOS和Host端virtio数据交互的通知通道.
一个比较简单的数据交互流程如下所示:
在GuestOS中访问文件等操作, 会调用到vfs, vfs会根据注册的驱动程序选择virtio-fs文件系统进行处理, virtio-fs基于FUSE文件系统, 将访问请求转发到virtiofsd中, 在virtiofsd中进行处理, 处理数据会通过virtio的virtqueue进行传递, 同时通过vhost user协议, 通知GuestOS接收数据.
2.3 DAX分析
关于DAX的讨论list : https://lists.syncevolution.org/hyperkitty/list/linux-nvdimm@lists.01.org/thread/MOCBHA2HTZCPTLFH6Y3YGOQJZQRJ3NZ5/
This patch series adds DAX support to virtiofs filesystem. This allows bypassing guest page cache and allows mapping host page cache directly in guest address space. When a page of file is needed, guest sends a request to map that page (in host page cache) in qemu address space. Inside guest this is a physical memory range controlled by virtiofs device. And guest directly maps this physical address range using DAX and hence gets access to file data on host. This can speed up things considerably in many situations. Also this can result in substantial memory savings as file data does not have to be copied in guest and it is directly accessed from host page cache.
.....
virtio_fs_probe()的时候 virtio_fs_setup_dax(vdev, fs)
2.4 ireg分析
.....
其他
1.
2.
2.linux文件系统
1、抽象vfs layer,成为io体系的controller,其中包含file(文件)、dentry(目录)、inode(索引)、mount point(安装点)四个entity。屏蔽了下层文件系统的差异,将下层能力封装成文件概念,使得应用层能通过操作文件,从而操作底层的块设备。
- 应用层用户可以通过树型目录来存取文件,符合用户的思维习惯,方便管理和获取,如/home/xiaoju/work。
- 通过开放file_operation、dentry_operation、inode_operation、address_space_operation接口,集成file system layer的能力,屏蔽底层的复杂性,使得底层file system的扩展和切换对于操作层无感。
- 抽象block(块)作为操作的基本单位。硬盘操作的做小单位扇区,但扇区太小,只有512byte,块的大小=512byte * 2的n次方,一般为1k或4k,就好像RMB最小单位是分,但我们现在都用元。磁盘管理器负责逻辑块号到具体扇区的映射,所以kernal只存储逻辑块号。
2、抽象file system layer,是io体系的service层,实现了文件系统的核心逻辑,包括数据的索引、查询、存储、缓存。其可以支持接入多种file system以满足用户不同的存储诉求,比如nfs。每种file system提供多种io mode,方便用户在存储空间利用率、性能、可靠性方面做选择。
- buffered io:普通文件操作,对性能、吞吐量没有特殊要求,由kernel通过page cache统一管理缓存,page cache的创建和回收由kernel控制。其默认是异步写,如果使用sync,则是同步写,保证该文件所有的脏页落盘后才返回(对于db transaction很重要,通过sync保证redo log落盘,从而保证一致性)。
- mmap:对文件操作的吞吐量、性能有一定要求,且对内存使用不敏感,不要求实时落盘的情况下使用。mmap对比buffered io,可以减少一次从page cache -> user space的内存拷贝,大文件场景下能大幅度提升性能。同时mmap将把文件操作转换为内存操作,避免lseek。通过msync回写硬盘(此处只说IO相关的应用,抛开进程内存共享等应用)。
- direct io:性能要求较高、内存使用率也要求较高的情况下使用。适合DB场景,DB会将数据缓存在自己的cache中,换入、换出算法由DB控制,因为DB比kernel更了解哪些数据应该换入换出,比如innodb的索引,要求常驻内存,对于redo log不需要重读读写,更不要page cache,直接写入磁盘就好。
3、抽象generic block layer,类似于dao层,适配内存和硬盘之间存储数据的gap,包括存储单位和存储地址(ps:不包括存储速度的匹配)。
4、抽象io scheduler layer,类似于dao层采用的写入方式是异步的,主要是解决io吞吐量的问题,提升整体数据处理(包括read和write)/ 寻道次数的占比,对于单次io延时略有提升,但从整体提升了磁盘的数据处理效率。也是在这一层调用块设备驱动(块设备在设备注册环节注册自己request_fn)。
转自 https://zhuanlan.zhihu.com/p/61123802