SPDK学习笔记

框架(Framework)是一个框子——指其约束性,也是一个架子——指其支撑性,也就是说:
1)框架本身一般不完整到可以解决特定问题;
2)框架天生就是为扩展而设计的;
3)框架里面可以为后续扩展的组件提供很多辅助性、支撑性的方便易用的实用工具(utilities),也就是说框架时常配套了一些帮助解决某类问题的库(libraries)或工具(tools)。

virtio 是针对 Linux 的 I/O 虚拟化框架。virtio 并没有提供多种设备模拟机制(针对网络、块和其他驱动程序),而是为这些设备模拟提供一个通用的前端,从而标准化接口和增加代码的跨平台重用。换句话说,这也为技术人员设计具体的设备加速方案提供了很大的扩展空间。

Guest OS 中集成的半虚拟化驱动 (para-drivers),充当 virtio 框架的前端,调用通用的 virtio 接口,在 Hypervisor 中由对应的模块实现设备模拟,这些模块充当 virtio 框架的后端。

virtio

virtio 模型

virtio 前后端的数据通过虚拟队列(基于共享内存技术)交换,相较于全虚拟化的 trap 模式,减少了昂贵的 VMExit 次数,缓解了虚拟化层上下文切换(context switch)带来的开销。

shm

共享内存

virtio arch

virtio 架构实现

上图中 virtio-*是运行于 Guest OS 中的驱动,充当 virtio 的前端;virtio 和 Transport 也运行于 Guest OS 中,virtio 提供通用的接口给前端调用,Transport 提供虚拟队列,用来传送命令和数据,它是衔接前端和后端的桥梁。virtio back-end drivers 位于 Qemu 进程中,实现对应的设备模拟,并将模拟结果通过 virtio 的虚拟队列返回给前端。

在 virtio 这个半虚拟化 I/O 框架的基础上,衍生出网络 I/O 加速方案、存储 I/O 加速方案,这里只关注 virtio 在虚拟化存储 I/O 加速领域的应用。

virtio 衍生方案。png

virtio 衍生方案

Virtio + Qemu 方案

该方案是 virtio 的经典组合方案,具体实现方式有 virtio-blk,virtio-scsi,以及最新的 virtio-fs。

以一个 virt-blk 为例,对应的 qemu 命令行参数如下所示,-drive指定 virtio 后端,-device指定 virtio 前端,virtio-blk-pci 表示,Guest OS 会看到 Qemu 模拟出的一个 virtio 类型的 PCI 设备,Guest OS 只需要加载对应驱动就能访问这个设备,跟普通的 PCI 设备没有区别。实际上完成设备模拟的任务,由-drive指定的后端来实现。这个后端模拟发生的”地点“和”实现方式“的不同组合形成了多种虚拟 I/O 加速方案。

这里 Virtio + Qemu 方案中,后端模拟完全由 Qemu 模拟硬盘读写行为来完成(上面的例子实际是 Qemu 中 hw/virtio-blk 模块通过对/path/to/disk-file.qcow2 文件的读写完成设备模拟)。由于前后端采用 virtqueue 传送数据,相较于全虚拟化,减少了上下文切换次数和内存拷贝的开销

 /usr/libexec/qemu-kvm ... \
 -drive file=/path/to/disk-file.qcow2,if=none,id=disk-backend \
 -device virtio-blk-pci,bus=pci.0,addr=0x9,drive=disk-backend \
 ...

Virtio + Qemu + vhost 方案

这是第一个方案 Vitio+Qemu 的改进版,把设备模拟的过程搬到了内核空间,具体指的就是 linux 的一个内核模块 vhost。从内核空间访问存储介质(数据源)缩短了 I/O 路径,进一步提高了性能。这中设计的本质思想就是让设备模拟发生在距离”数据源“最近的地方,所以如果数据源不是存储介质而是其他用户空间的进程,那么从内核空间访问就不合适了,于是又衍生出 vhost-user 的方案,即把设备模拟又搬回了用户空间。乍一看,这好像是在折腾无用功,其实不然,因为 Qemu 职责是模拟虚拟机系统,已经很繁忙,把 IO 加速的任务 offload 到其他模块,vhost-user, 减轻 Qemu 的模拟负担,也降低了 IO 流程在 Qemu 中的耦合,开发人员可以在 vhost-user 这一侧专注于 IO 优化(后面所说的 SPDK 就是这么做的)。

下面以 vhost-user 为例,来解释一下数据是如何流动的:

  1. client(qemu)创建共享内存,然后通过 ioctl 与内核通信,告知内核共享内存的信息,这种就是 kernel 作为 server 的 vhost;或者通过 Unix domain 来跟其他的进程通信,这叫做 vhost-user。下面以 Unix domain 为例。

  2. Unix domain 可以使用 sendmsg/recvmsg 来传递文件描述符,这样效率更高一些;client 创建好共享内存,发送描述符到 server,server 直接 mmap 这个描述符就可以了,少了一个 open 的操作。

  3. Client 和 Server 可以有多段共享内存,每段之间不连续。每一段都是一个 vring。
    image

    vring

  4. Client 初始化好每一段共享内存 vring,Server 不用初始化。

  5. Client 发送 vring 的 desc,avail,used 这些地址索引给 server,然后 server 在重新 mmap 之后,可以根据这个地址索引算出 desc,avail,used 这些地址索引在 server 地址空间的地址,然后就可以直接读写了。注意,由于数据指针是 client 地址空间的地址,在 Server 处理读的时候需要转换。

  6. 共享内存存放 desc,avail,used 这些信息,以及 avail->idx, used->idx 这些信息。

  7. 当 client 写的时候,数据放在 vring->last_avail_idx 这个描述符里,注意 last_avail_idx、last_used_idx 这两个值,在 client 和 server 看到的是不一样的,各自维护自己的信息,作为链表头尾。添加 id 到 avail 里面,shared.avail.idx++。注意,client 此处的 last_avail_idx 指向的是描述符的 id,而不是 avail 数组的 id,这个跟 Server 上的 last_avail_idx 的意思不一样。为什么这样做呢?
    last_avail_idx 表示可用的数据项,可以直接拿出来用,用完之后,取当前的 next;

    当回收的时候,也就是 client 在处理 used 的时候,可以直接插入到 last_avail_idx 的前面,类似链表插入到表头;

  8. Server 收到信号后,从自己记录的 last_avail_idx 开始读数据,读到 avail->idx 这里,区间就是 [server.last_avail_idx, shared.avail.idx)。

  9. Server 处理每处理完一条请求,自己的 last_avail_idx ++; 同时插入 id 到 used 里面,插入的位置是 shared.used.idx,然后 shared.used.idx++。used.idx 此时就像 avail->idx 一样,表示最新的 used 的位置。

  10. Server 通知 client 处理完数据后,client 就可以回收 used 的描述符了,可回收的区间是 [client.last_used_idx, shared.used.idx)。

  11. kickfd,callfd 都是在 client 创建,然后通过 unix domain 发送给 server。client 通知 server 叫 kick。

Virtio-blk 与 virtio-scsi

他们都是在 virtio spec 里面定义的两种块设备实现。区别是 virtio-blk 是作为 pci 设备挂在 qemu 里面,所以最多只能有 16 块 virtio-blk 盘。 而 virtio-scsi 作为 scsi 子系统,挂在 scsi 总线上,数量上可以多得多。由于 virtio-scsi 实现了 scsi 的协议 ,所以复杂度来说要高一些。 此时,在 qemu 里面看,这块盘跟普通的 scsi 盘一样,支持 scsi 命令查询,例如 sg3_utils 提供的工具。但是 virtio-blk 盘不支持 scsi 命令。

相关代码:

Kernel
drivers/vhost/vhost.c - common vhost driver code
drivers/vhost/net.c - vhost-net driver
virt/kvm/eventfd.c - ioeventfd and irqfd

Qemu
hw/vhost.c - common vhost initialization code
hw/vhost_net.c - vhost-net initialization

PCI assign/SR-IOV 方案

跟显卡直通的机制一样,不再赘述。关于实现细节的分析,请参考 PCI 透传源码分析。概括来说,就是把物理 PCI 设备的内存资源直接映射到 KVM 的内存域中,期间不需要 Copy,这也是 PCI 透传方式性能高的原因。

  1. Linux 内核中的 pci-stub 驱动生成的 Driver 结构只是与 PCI Bus 产生的 Device 结构进行绑定,没有提供任何用户调用接口,主要作用是防止其他驱动占用这个 Device;
  2. 用户态的 Qemu 访问这个 PCI 设备主要通过这个 PCI BUS 驱动为这个 PCI Device 提供的 SysFS 属性文件进行,通过 config 文件读取和配置 PCI 配置空间,通过 resource 文件获取 PCI BAR 的数量、地址和长度,通过 resource%d 文件进内存映射和中断读写;
  3. Qemu 调用 KVM 的 ioctl 主要是进行这个设备的 IOMMU 配置和 KVM 数据结构初始化,以及中断的注入。这应该是为了弥补 pci-stub 驱动和 SysFS 的不足,感觉这是一种修修补补的写法,原本的框架不支持相关功能,而 VFIO 和 irqfd 在设计之初就是为了解决这个问题,且更加优雅和强大(VFIO 还可以提供 MDev 功能),这应该是其备受推崇的根本原因;
  4. 无论是 PCI-Assign 还是 VFIO,或者 MDev,其实他们真正透传的只有内存区域,对于配置区域和中断的处理,其实都是需要宿主机的软件参与,他们能降低的主要是大量数据的传输,对于中断和配置的性能并没有本质改善。由于设备配置部分原本在设备的访问中占比不高,所以不是性能瓶颈。但对于中断,在高性能场景,应尽可能减少中断的使用,使用查的方式处理状态变化,可以获取更好的性能。

Virtio + Qemu + vhost-user + SPDK 方案 [WIP]

image.png

SPDK Optimization

image.png

image.png
image.png
image.png
image.png
image.png
image.png

主流厂商的 SPDK 实践 [WIP]

SPDK

SPDK NVMe-oF 方案

Why NVMe-oF & SPDK
Simple,Efficiency,E2E

image.png
image.png
History
image.png
image.png
RDMA
image.png
image.png
TCP

总结

virtio 为解决虚拟化 I/O 加速的问题提供了一套通用的模式,这个模式具有两个特点:
1. 前端+后端的半虚拟化结构
2. 基于共享内存的虚拟队列(virtqueue)作为前后端的传输层

基于 virtio 的模式,结合 vhost、SPDK、NVMe SSD、NVMe-oF、UIO(内核 I/O 旁路)等技术,介绍了各主流厂商针对自身业务场景的最佳实践。

如果我们要实现 ICOS 的虚拟存储 IO 加速方案,至少要掌握下面的技术体系。因为虚拟存储 IO 加速技术离不开这些模块的支撑,如果我们不吃透这些技术细节,产品推出市场后,将面临售后和维护困难的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWc9K1AQ-1611717705355)(https://i.loli.net/2020/08/27/OWKnjFRmzuGdeiD.png)]

虚拟存储 IO 加速技术体系

下一步计划

学习 vhost 技术,将虚拟机的 virtio 后端换成 vhost,实测 vhost 技术带来的加速效果,结果应该是下表的形式:

image.png

vhost vs virtio

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值