Asymmetric io_uring в Seastar
Интересный pull request в seastar – выносит весь ввод-вывод на dedicated CPU cores.
Основная фишка seastar – симметричность и локальность исполнения. Все ядра одинаковые и каждое ядро СУБД делает всё – и обработку данных, и сетевой и дисковый ввод-вывод. В Picodata, например, вводом-выводом занимаются обособленные потоки, а tx thread изолированно занимается только обработкой транзакций.
Планировщик Tokio в Rust может продолжить обработку таски на другом треде, что похоже на Go, но плохо подходит для задач типа СУБД, где локальность кэшей CPU крайне важна (*).
Это не значит что у seastar до последнего MR была какая-то неправильная архитектура. Напротив, я бы сказал что в плане эффективного использования ядер seastar – best of the best.
Неправильная архитектура скорее не у seastar, а у сетевого стэка Linux и NIC firmware. Старые сетевые карточки умели доставлять и забирать пакетики из 1 области DMA и отправлять прерывания об этом на одно ядро. Новые поддерживают так называемые RX/TX queues – очереди прерываний, с каждой из которых связана своя область DMA. В общем, шардируют входящую очередь по ядрам.
Проблема в том что нет никакого способа выстроить сетевой стэк так, чтобы в нужный тебе, то есть локальный для твоего ядра, RX/TX queue прилетали нужные тебе пакеты. С одной стороны маппинг на RX/TX очередь делается по src/dst IP address/port, то есть пакеты одного соединения всегда прилетают в одну и ту же очередь, с другой стороны сама хэш функция определяется сетевой картой (RSS – Receive Side Scaling), и на прикладном уровне не получится “настроить” входящий и исходящий порты конкретного соединения так чтобы добиться локальности прерываний.
В общем случае доставка данных из сетевой карточки требует как минимум одного hop’а от ядра-получателя прерывания к ядру-обработчику данных.
При этом seastar поставляется с тулзой perftune.py, которая может настроить отображение RX/TX queue и IRQ на ядра по одной из базовых схем: равномерно по всем либо выделенно на несколько (например, по 1 ядру на NUMA ноду).
В отсутствие perftune.py, который работает через запись в /proc/irq/<N>/smp_affinity, задача отдаётся на откуп irqbalance – это такой демон в Linux, который следит за равномерностью прерываний и перераспределяет их между ядрами.
perftune.py также включает режим Receive Flow Steering. В этом режиме ядро Linux отслеживает, на каком CPU core последним вызывался recvmsg() для данного потока, и перенаправляет будущие пакеты этого потока на то же CPU core.
Так вот, в идеальном мире прерывание должно приходить в то ядро, в которое нужно доставить пакет. В реальном мире прерывания могут приводить к load spikes, то есть паузам в исполнении прикладного кода. Вот чтобы максимально отделить прикладной код от сетевого ввода-вывода и предлагается сделать asymmetric io_uring backend, который будет обрабатывать ввод-вывод на выделенных ядрах.
Очень похоже на то что было сделано в Tarantool 15 лет назад и сейчас работает в Picodata.
(*) Похоже ближайшим аналогом seastar в Rust является glommio, который написал Glauber Costa – бывший разработчик из московского Parallels, затем разработчик в ScyllaDB, а сейчас CTO Turso :) Возможно, Picodata когда-нибудь выкинет свой runtime, и переедет на glommio.