谈谈存储软件的无锁设计

  面向磁盘设计的存储软件不需要考虑竞争锁带来的性能影响。磁盘存储软件的性能瓶颈点在于磁盘,磁盘抖动会引入极大的性能损耗。因此,传统存储软件的设计不会特别在意处理器的使用效率。曾经对一个存储虚拟化软件进行性能调优,在锁竞争方面做了大量优化,最后也没有达到性能提升的效果,原因就在于存储虚拟化的性能瓶颈点在于磁盘,而不在于处理器的使用效率。正因为如此,在面向磁盘设计的软件中,很多都采用单线程、单队列处理的方式,一定程度上还可以避免由于并发所引入的磁盘抖动问题。

  在面向 NVMe SSD 设计的存储软件中,这一切正发生着变化。和传统磁盘相比, NVMe SSD 具有极高的 IO 读写性能,不存在传统磁盘所具有的访问寻道、抖动问题。为了发挥 NVMe SSD 的性能,无论在软件还是在硬件上都需要采用多队列技术,通过多队列方式充分发挥 NVMe SSD 的性能。通常 NVMe SSD 控制器可以提供128 个 IO 提交和结束队列, NVMe 驱动会根据处理器的核数利用 SSD 控制器提供的硬件能力,实现 IO 的并发处理。在驱动层面,可以绑定处理器与 IO 队列,达到 IO 并发处理的效果。在这种情况下,业务线程一定要多线程并发利用处理器,才可以将 NVMe SSD 的多队列真正用起来。在很多情况下,业务的软件模型往往会分为计算单元和存储单元两部分。计算单元往往是多线程并发处理的模式;存储单元在传统磁盘上是单线程模式,在 NVMe SSD 上会变成多线程并发处理的方式。这种软件处理模型可以描述如下:

物联网

  在上述软件模型中,如果 Compute 和 IO 单元通过合理的资源切分划分在一起, Compute 和 IO 单元在同一个执行上下文中,那么整个软件的效率将会变得非常高。但是在很多情况下,计算和存储可以是两个软件模块,或者计算和存储资源无法有效的进行划分,那么在这种情况下,计算和存储线程之间需要通过消息队列进行通信。这种消息通信可以抽象成生产者 - 消费者模型。 Compute 单元是生产者; IO 单元是消费者。

  生产者 - 消费者在实现过程中需要进行同步操作。如果采用传统的锁机制实现“生产者 - 消费者”队列,那么存储系统的性能瓶颈将会转移到锁竞争点。锁竞争一方面使得处理器处于长时间等待或者睡眠调度的状态;另一方面由于大量的缓存无效操作使得CPU 的访存效率大大降低。总的来说,锁竞争将会导致 CPU 使用效率的降低,从而可以看到处理器的 IPC 指标变得很低。

  那么在上述软件模型中,如何提升处理器效率,充分发挥 NVMe SSD 的性能呢?这就需要考虑采用无锁设计。在多线程并发处理的场景下,难免需要在线程之间进行数据交互,为了提高 CPU 的效率,这种大量的线程交互可以采用无锁的方式进行同步。无锁设计需要采用特殊的算法与处理器提供的特殊指令,常用的无锁算法有采用CAS 指令实现的无锁队列,该种类型的算法需要一个 Dummy 节点,因此存在动态分配内存的问题。为了避免动态内存分配,可以采用固定长度的无锁队列 Lock-free Ring,但是会存在队列深度固定的问题。在存储系统设计中,关键路径上最好需要避免内存资源的频繁分配,另外还需要实现队列深度的可变与 IO 数量的流控。因此,需要根据存储系统的特征,需要设计符合自身特点的无锁算法。在最近一个存储项目中,我们创造了一种无锁生产者 - 消费者及无锁流控算法,采用这种算法之后,我们可以发现 IO 的性能可以随着 CPU 核数线性变化。当 CPU 核数越多, IO 性能越高。下图是实际测试的性能与 IO 线程数量之间的关系图:

物联网
物联网

  上图我们可以看出当 IO 线程数量增加到 28 之后,随机读性能变化不明显,并且出现了一定的性能下降。在该测试平台上,一共具备 28 个 CPU 硬核,通过超线程的方式,达到 56 个 CPU 核。从上面的实验结果来看,超线程对 IO 性能贡献不大。随机读性能在 IO Thread 数量达到 8 个之后,性能达到峰值。这是由于后端 NVMe SSD 盘性能达到了峰值。