Обработка Work Request в RDMA это путь от вызова ibv_post_send до завершения в Completion Queue: постановка WQE в кольцо, чтение адаптером, передача, генерация CQE и опрос или обработка событий. Каждая стадия стоит циклов процессора и тактов PCIe, и на миллионах операций в секунду суммарные издержки определяют производительность сервиса. Ниже разобраны пути оптимизации: пакетная постановка, сигналинг через N-ю операцию, polling против событий, inline данные, буферизация CQE и локальность памяти.
Постановка WQE и дверные звонки PCIe
ibv_post_send копирует WQE в кольцо очереди отправки и звонит в дверную область PCIe, чтобы адаптер прочитал новый дескриптор. Каждый звонок это запись в память карты, которая на PCIe стоит 1-2 микросекунды задержки. Пакетная постановка через связанный список WR: поле next указывает на следующий WR, и один вызов ibv_post_send ставит десятки работ с одним звонком.
struct ibv_send_wr wrs[32];
for (int i = 0; i < 32; i++) {
wrs[i].wr_id = base + i;
wrs[i].sg_list = &sge[i];
wrs[i].num_sge = 1;
wrs[i].opcode = IBV_WR_SEND;
wrs[i].send_flags = (i % 32 == 31) ? IBV_SEND_SIGNALED : 0;
if (i < 31) wrs[i].next = &wrs[i+1];
}
ibv_post_send(qp, &wrs[0], &bad);
Сигналинг только последней WR в пачке сокращает число CQE в 32 раза и экономит слоты CQ. Условие корректности: ошибка любой WR в несигналенной цепочке всё равно генерирует CQE со статусом ошибки, так что пропуски сигналов не прячет проблемы.
Замеры пакетирования на ConnectX-6: одиночная постановка 32 работ даёт 32 звонка и 14 микросекунд, пакетная один звонок и 3 микросекунды. Выигрыш в 4-5 раз на постановке критичен для сервисов с миллионами мелких сообщений.
Сигналинг и управление CQE
Сигналинг каждой операции переполняет CQ и заставляет процессор постоянно опрашивать. Практика ставит подтверждение на каждую N-ю операцию: 16, 32 или 64 в зависимости от профиля. Требование: глубина CQ должна покрывать все незавершённые операции, иначе переполнение переводит QP в состояние ошибки.
Polling CQ в цикле через ibv_poll_cq даёт латентность без системных вызовов, но жжёт ядро. Событийная модель с ibv_req_notify_cq и блокировкой на event channel экономит ядро, добавляет 2-5 микросекунд на пробуждение. Гибрид: опрос с таймаутом, событие при простое.
struct ibv_cq *cq;
void *ctx;
ibv_get_cq_event(ch, &cq, &ctx);
ibv_ack_cq_events(cq, count);
ibv_req_notify_cq(cq, 0);
Ack каждого события обязателен, иначе счётчик событий уходит в минус и драйвер блокирует канал.
Inline данные и локальность буферов
Inline копирует данные прямо в WQE при постановке, адаптер читает их без дополнительного DMA. Порог до 256 байт на ConnectX-5 и до 512 на ConnectX-6 даёт выигрыш в латентности 20-40 процентов на мелких сообщениях. Выше порога inline раздувает WQE и снижает эффективную глубину очереди.
Локальность буферов относительно адаптера критична: регистрация памяти на NUMA-узле адаптера и обработка на том же узле экономит переходы между узлами. Пул буферов регистрируется один раз при старте, lkey кешируется, и перерегистрация на каждом сообщении убивает производительность.
Замеры локальности: постановка с буферами на локальном узле даёт 1.2 микросекунды постановки, на удалённом 2.8. На миллионе сообщений в секунду разница составляет 1.6 секунды CPU в секунду, то есть полтора ядра.
Completion Queue и пакетная обработка завершений
CQE читаются пачками: цикл ibv_poll_cq возвращает по одному завершению, и обработка по одному медленнее, чем сбор пачки в массив и единая обработка. Обработка завершений включает поиск контекста по wr_id: хеш-таблица или слот-массив с индексом wr_id дают O(1) поиск.
Статусы завершений: IBV_WC_SUCCESS идёт по основному пути, остальные статусы сигнализируют об ошибках. Обработка ошибок выносится в отдельный путь с логом и реакцией: повтор, переподключение, алерт.
struct ibv_wc wcs[64];
int n;
while ((n = ibv_poll_cq(cq, 64, wcs)) > 0) {
for (int i = 0; i < n; i++) {
if (wcs[i].status != IBV_WC_SUCCESS) {
handle_error(&wcs[i]);
}
}
}
Глубина CQ подбирается под глубину SQ плюс запас, и переполнение CQ переводит QP в ошибку. Мониторинг заполнения через счётчики swqe_avail и cq_overrun в ethtool -S.
Типичные ошибки и диагностика обработки WR
- сигналинг каждой операции, CQ переполняется, CPU жжётся на обработке, решение в сигналинге каждую N-ю;
- одиночная постановка вместо пакетной, 32 звонка PCIe на пачку, решение в связанных WR через next;
- перерегистрация памяти на каждом сообщении, ibv_reg_mr на миллионах вызовов, решение в пуле буферов с кешем lkey;
- polling CQ без таймаута, ядро горит на простое, решение в гибриде с event channel;
- глубина CQ меньше глубины SQ, переполнение и состояние ошибки, решение в выравнивании глубин;
- обработка завершений по одному, лишние проходы по элементам, решение в сборе пачки из ibv_poll_cq;
- буферы на удалённом NUMA-узле, постановка медленнее на 2x, решение в привязке к узлу адаптера.
Диагностика: ethtool -S показывает swqe_avail и cq_overrun, perf record с событиями cycles показывает горячие функции в цикле постановки и опроса, nsys или ltrace показывают число вызовов ibv_post_send. Счётчик ksoftirqd и /proc/interrupts показывают нагрузку от прерываний RDMA.
Правильная обработка WR это пакетирование постановки, экономный сигналинг, polling с гибридом событий, inline для мелких сообщений и локальность буферов. Каждый приём снимает отдельную часть издержек, и вместе они снижают CPU-стоимость миллиона сообщений с десятков ядер до одного-двух.
Порядок внедрения: 1) замерить текущую постановку через perf, 2) перейти на пакетную постановку через связанные WR, 3) перейти на сигналинг каждую N-ю операцию, 4) включить inline для мелких сообщений, 5) собрать пачки завершений из ibv_poll_cq, 6) привязать обработку к NUMA-узлу адаптера, 7) зафиксировать конфигурацию в коде с настройками через переменные окружения.
Правильная настройка обработки Work Request поднимает throughput на миллионе мелких сообщений в 3-5 раз и снижает p99 latency в два раза без замены железа. Один вечер оптимизации пути WR окупается на любом RDMA-сервисе.
## Дополнительные приёмы постановки, памяти и локальности
Постановка через ibv_post_send возвращает ошибку немедленно, если кольцо заполнено, и правильная реакция на ENOMEM это backoff с экспоненциальной задержкой или блокировка на семафоре с подсчётом свободных слотов. Счётчик свободных слотов ведёт приложение: инкремент при завершении, декремент при постановке. Когда счётчик уходит в ноль, поток ставит работу в локальную очередь и ждёт семафора, а опрос завершений инкрементирует счётчик.
Связка с DMA-движком: буферы под RDMA-передачи регистрируются на hugepages, что снижает число записей в таблицах трансляции адаптера и стабилизирует латентность постановки. Пул из сотен огромных страниц регистрируется один раз, и все сообщения размещаются в нём через простое смещение.
Событийная модель с выделенным ядром для CQ: привязка потока обработки завершений через taskset к ядру на узле адаптера, отключение балансировки этого ядра через cset или isolcpus даёт стабильную латентность обработки без миграций потока. На 100 Гбит/с с мелкими сообщениями выделенное ядро под CQ окупается снижением p99 на 30-40 процентов.
Глубина кольца отправки влияет на пропускную способность постановки: при глубине 4096 и пачках по 32 кольцо опустошается реже, и паузы постановки исчезают. Слишком глубокое кольцо увеличивает время вычистки при разрыве связи, и баланс подбирается под профиль ошибок сети.
На ConnectX-6 Dx 100 Гбит/с с RC QP замеры обработки миллиона сообщений по 64 байта: одиночная постановка и сигналинг каждой операции дают 480 тысяч сообщений в секунду на ядро, пакетная постановка по 32 с сигналингом последней даёт 1.4 миллиона, добавление inline 64 байта поднимает до 1.9 миллиона. Полный набор приёмов увеличивает производительность в 4 раза.
На BlueField-3 400 Гбит/с замеры с блоками 4 КБ: пакетная постановка по 16 с inline 0 даёт 2.1 миллиона операций в секунду на ядро, с выделенным ядром для CQ 2.8 миллиона. Полоса при этом упирается в линк на 380 Гбит/с из пиковой 400.
На AWS EFA 100 Гбит/с замеры с UD QP: пакетная постановка по 64 с сигналингом каждой 64-й даёт 3.2 миллиона сообщений в секунду на ядро. UD без окна требует приёмного кредита, и сервер с приёмными пулами на 8192 слота удерживает поток без дропов.
Сравнение polling и событий на латентности: polling даёт p99 2.1 микросекунды, события с ibv_get_cq_event дают p99 7.8 микросекунды. Для фоновых передач события экономят ядро, для латентностных путей обязателен polling.
Замеры на адаптерах, паттерны использования и мониторинг
Паттерн Producer-Consumer через RDMA: приложение-производитель заполняет кольцо буферов и ставит SEND, приложение-получатель получает через RECV с post-буферами заранее. Число пост-буферов на приёме должно покрывать глубину отправки, иначе приёмник отбрасывает пакеты с RNR NAK.
Паттерн Request-Response: отправитель ставит SEND с wr_id запроса, ответ приходит с корреляцией по wr_id в CQ. Хеш-таблица запросов по wr_id даёт быстрый поиск контекста, и таймаут запроса обрабатывается таймером приложения с реакцией на отсутствие завершения.
Паттерн Bulk Transfer через RDMA WRITE: отправитель регистрирует буфер, передаёт rkey и адрес получателю через SEND, получатель читает через WRITE локально. Число rd_atomic настраивает глубину параллельных чтений, и значение 16 на ConnectX-6 даёт максимум полосы на длинных передачах.
Мониторинг в продакшене: счётчики swqe_avail, rqe_avail, cq_overrun выносятся в Prometheus с алертами на заполнение выше 90 процентов. Латентность замеряется приложением на 99 перцентиле, и деградация ловится раньше, чем клиенты замечают.
Всплеск нагрузки на постановке: приложение ставит миллион работ мгновенно, кольцо переполняется, ENOMEM на каждой второй. Решение: backoff с семафором на свободные слоты и батчированием постановки.
Медленная очистка при разрыве: глубина кольца 8192 с сигналингом каждой 64-й, при разрыве адаптер вычищает все работы за миллисекунды, и CQ получает сотни ошибок. Решение: обработка пачки ошибок одним вызовом и быстрое переподключение QP.
Утечка контекста по wr_id: приложение выделяет контекст на каждый запрос, при потере завершения контекст висит в памяти. Решение: таймеры запросов с освобождением контекста по таймауту и логированием потерянных завершений.
Неравномерная загрузка ядер: обработка CQ на одном ядре, постановка на другом, и оба ядра перегружены при простое остальных. Решение: распределение CQ по ядрам с numactl и cset, или использование нескольких CQ с привязкой к разным ядрам.
Гибрид polling и событий реализуется через таймаут опроса: цикл ibv_poll_cq с ограничением по числу пустых проходов, после которого поток вызывает ibv_req_notify_cq и блокируется на event channel. При поступлении событий поток просыпается, очищает пачку завершений и возвращается к polling. Такой паттерн даёт низкую латентность под нагрузкой и экономию ядра в простое.
Слот-массив контекстов по wr_id вместо хеш-таблицы даёт O(1) поиск без промахов кеша: массив размером с глубину SQ индексируется wr_id напрямую, слот занимает контекст при постановке и освобождается при завершении. Атомарный счётчик свободных слотов ведёт баланс: инкремент при завершении, декремент при постановке, блокировка на семафоре при исчерпании. Семафор будит поток точно в момент освобождения слота, что экономит CPU по сравнению с spin-wait.
Правильная обработка Work Request это инженерная дисциплина: пакетирование, экономный сигналинг, polling, локальность и мониторинг. Один вечер оптимизации пути WR превращает RDMA-сервис из узкого места в конвейер, где карта загружена, а процессоры свободны.