Как устранить скачки задержки записи файлов на mdadm RAID0?
Краткое описание проблемы высокого уровня
Мы работаем над приложением, которое требует высокой пропускной способности RAID0 в течение длительных периодов времени. На выделенные RAID-массивы записывается до 8 независимых потоков данных со скоростью 5 ГБ/с (1 RAID на каждый поток данных). Большую часть времени это работает нормально, однако, по-видимому, возникают непредсказуемые скачки задержки записи файлов, которые приводят к переполнению буферов потока и, следовательно, к потере данных.
Кто-нибудь видел подобные проблемы? Если да, то какие изменения я могу внести в свое программное обеспечение, чтобы этого не произошло?
Нарушающий код
Ниже приведен код, который выполняется в наших потоках файлового ввода-вывода. Обратите внимание, что из-за ограничений платформы, на которой построена остальная часть нашего приложения, мы ограничены передачей в эту функцию одного аргумента, поэтому мы должны распаковать его в верхней части функции. Также обратите внимание, что нас больше всего беспокоит линия.
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);
}
}
Системное оборудование
- Компьютер, на котором работает наше приложение, представляет собой сервер с двумя
AMD EPYC 7643
48-ядерные процессоры. Гиперпоточность намеренно отключена. - Каждый RAID0 построен с использованием двух NVM, каждый из которых способен поддерживать постоянную скорость записи 3,5 ГБ/с, поэтому теоретически мы должны иметь возможность получить скорость записи до 7 ГБ/с.
- На все наше оборудование установлена последняя версия прошивки.
Программное обеспечение
- Мы используем
Ubuntu 22.04 LTS
бег по5.15.0.86-generic
Ядро Linux. - Следующие аргументы загрузки используются для оптимизации программной среды для нашего приложения:
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, всегда будут попадать на RAID, принадлежащий тому же узлу NUMA.
- Приложение использует до 4 выделенных потоков на каждый поток данных для ввода-вывода файлов. Мы пытались использовать всего два потока на поток, но для надежности пропускной способности ввода-вывода требуется 4.
- Каждый поток записывается в один файл, размер которого может достигать 5 ТБ.
- Мы используем синхронные вызовы для записи данных на RAID. Обратите внимание, что мы тщательно экспериментировали с другими подходами, такими как
iouring
, но из-за характера потока данных синхронные вызовы обеспечивают для нас самую высокую и надежную пропускную способность. - Данные поступают пакетами шириной 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.