如何消除 mdadm raid0 上的文件写入延迟峰值?

如何消除 mdadm raid0 上的文件写入延迟峰值?

高级问题摘要

我们正在开发一款需要长时间保持 RAID0 高吞吐量的应用程序。最多有 8 个独立的 5 GB/s 数据流被写入专用 RAID(每个数据流 1 个 RAID)。这在大多数情况下都运行良好,但是显然存在不可预测的文件写入延迟峰值,这会导致流缓冲区溢出,从而导致数据丢失。

有人遇到过类似的问题吗?如果是这样,我应该对软件进行哪些更改以防止发生这种情况?

违规代码

以下是在我们的文件 IO 线程上运行的代码。请注意,由于我们应用程序其余部分所基于的平台的限制,我们只能向此函数传递一个参数,这就是为什么我们必须在函数顶部解包它。还请注意,我们最关心的是该pwritev()行。

void IoThreadFunction(IoThreadArgument *argument)
{
    /////////////// Unpack the argument ///////////////
    rte_ring* io_job_buffer = argument->io_job_buffer;
    rte_ring* job_pool = argument->job_pool;
    bool* stop_signal = argument->stop_signal;
    int fd = argument->fd;
    std::shared_ptr<bool> initialized = argument->initialized;
    ///////////////////////////////////////////////////

    int status;
    Job* job;

    spdlog::trace("io thread servicing fd {0} started on lcore {1}.", fd, rte_lcore_id());

    *initialized = true;

    while(!*stop_signal || rte_ring_count(io_job_buffer))
    {
        /////   This part of the code receives data from
        /////   other parts of the app. And creates an IOVEC
        /////   array that will be used for vectorized
        /////   file IO. It is not suspected to be the
        /////   root of the problem.
        /////   START SECTION
        
        // Poll io job queue
        if(rte_ring_mc_dequeue(io_job_buffer, (void**)&job) == -ENOENT) continue;

        // Populate iovecs
        job->populate_iovecs();
        
        ///// END SECTION

        // Write the data to file
        pwritev2(fd, job->iovecs, job->num_packets, job->file_offset, RWF_HIPRI);

        // Free dpdk packets
        rte_pktmbuf_free_bulk(job->packets, job->num_packets);
        
        // Restore job to pool
        rte_ring_mp_enqueue(job_pool, job);
    }
}

系统硬件

  • 运行我们应用的计算机是一台拥有 2 个AMD EPYC 764348 核处理器的服务器。超线程被有意禁用。
  • 每个 RAID0 使用两个 NVMe 构建,每个 NVMe 能够实现 3.5 GB/s 的持续写入速度,因此理论上我们应该能够获得高达 7 GB/s 的写入速度。
  • 我们所有的硬件都安装了最新的固件。

系统软件

  • 我们正在使用在Linux内核Ubuntu 22.04 LTS上运行5.15.0.86-generic
  • 以下启动参数用于优化我们应用程序的软件环境:isolcpus=0-39,48-87 rcu_nocbs=0-39,48-87 processor.max_cstate=0 nohz=off rcu_nocb_poll audit=0 nosoftlockup amd_iommu=on iommu=pt mce=ignore_c。请注意,这些启动参数中的一些是应用程序其他部分所必需的,这些部分与将数据写入磁盘无关。isoclcpus设置该参数是为了隔离我们的应用程序用于将数据传输到磁盘的核心,从而最大限度地减少可能导致更多延迟的系统中断。

其他相关细节

  • 我们的应用程序具有 NUMA 感知能力,因此源自给定 NUMA 节点的数据将始终位于属于同一 NUMA 节点的 RAID 上。
  • 应用程序对每个数据流使用最多 4 个专用线程来进行文件 IO。我们曾尝试过每个数据流使用最少 2 个线程,但为了实现 IO 吞吐量的可靠性,需要 4 个线程。
  • 每个流都被写入一个可能增大至 5 TB 的单独文件。
  • 我们使用同步pwritev()调用将数据写入 RAID。请注意,我们已经彻底试验了其他方法,例如iouring,但由于数据流的性质,同步pwritev()调用为我们带来了最高、最可靠的吞吐量。
  • 数据以 8192 字节宽的数据包形式到达,我们每次使用矢量化写入 1024 个数据包以充分利用pwritev()
  • 所有数据都是页面对齐的,并且所有文件写入都使用该标志绕过 Linux 页面缓存O_DIRECT
  • 每个文件的空间都是使用以下方法预先分配的fallocate()
  • RAID 配置有mdadm以下选项:mdadm --create /dev/md0 --chunk=256 --level=0 --raid-devices=2 /dev/nvme[n]n1 /dev/nvme[n+1]n1
  • 所有 RAID 都具有XFS使用以下选项创建的文件系统:mkfs.xfs -b size=4096 -d sunit=512,swidth=1024 -f /dev/md[n]。文件系统的配置最适合 RAID 几何结构。

相关内容