Обработка 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

  1. сигналинг каждой операции, CQ переполняется, CPU жжётся на обработке, решение в сигналинге каждую N-ю;
  2. одиночная постановка вместо пакетной, 32 звонка PCIe на пачку, решение в связанных WR через next;
  3. перерегистрация памяти на каждом сообщении, ibv_reg_mr на миллионах вызовов, решение в пуле буферов с кешем lkey;
  4. polling CQ без таймаута, ядро горит на простое, решение в гибриде с event channel;
  5. глубина CQ меньше глубины SQ, переполнение и состояние ошибки, решение в выравнивании глубин;
  6. обработка завершений по одному, лишние проходы по элементам, решение в сборе пачки из ibv_poll_cq;
  7. буферы на удалённом 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-сервис из узкого места в конвейер, где карта загружена, а процессоры свободны.