Очередь отправки, Send Queue, в RDMA это буфер, где приложение ставит работы, Work Requests, перед передачей в адаптер. Размер очереди, число WQE, Work Queue Entries, глубина цепочки SGE, Scatter Gather Elements, и механизм кредитов определяют, сколько пакетов может быть в полёте и как быстро адаптер их выкачивает. Недостаточная глубина заставляет процессор ждать свободного слота, избыточная раздувает память и увеличивает задержку при очистке. Ниже разобраны настройка max_send_wr, max_sge, inline threshold, управление кредитами через параметры QP и rate limiter, а также примеры подбора для ConnectX и EFA.
Анатомия Send Queue и параметры создания QP
При создании Queue Pair через ibv_create_qp структура ibv_qp_init_attr задаёт параметры очереди отправки:
struct ibv_qp_init_attr attr = {
.cap.max_send_wr = 1024,
.cap.max_send_sge = 16,
.cap.max_inline_data = 256,
.qp_type = IBV_QPT_RC,
.send_cq = cq,
.recv_cq = cq,
};
Поле max_send_wr задаёт максимальное число Work Requests в очереди, значение включает и текущий WR, поэтому реальная глубина на единицу меньше. На ConnectX-5 и ConnectX-6 оптимально 1024-4096 для высоконагруженных путей и 256-512 для латентностных. На AWS EFA, Elastic Fabric Adapter, лимит 8192, но большие значения увеличивают время очистки при ошибке связи.
Поле max_send_sge задаёт число Scatter Gather Elements в одном WR, типичные значения от 1 до 16. Каждое SGE описывает буфер: адрес, длина, ключ памяти lkey. Большие цепочки SGE полезны для векторного ввода-вывода через iovec, но каждый SGE требует обработки адаптером, поэтому для мелких сообщений выгоднее собирать данные в один буфер и использовать inline.
Поле max_inline_data задаёт порог в байтах, ниже которого данные копируются прямо в WQE, а не ссылаются через SGE. Inline быстрее для мелких сообщений, до 256 байт на ConnectX-5 и до 512 на ConnectX-6, так как экономит чтение SGE и отдельный DMA. Но inline раздувает WQE: при inline 256 и заголовке WR в 64 байта слот занимает 320 байт вместо 64. При глубине 1024 это 320 КБ против 64 КБ памяти на QP. Порог подбирают экспериментально: для RPC-пакетов по 64-128 байт ставят inline 64-128, для больших блоков inline 0.
Постановка работ поддерживает пакетирование: несколько WR связываются в список через указатель next и передаются одним вызовом ibv_post_send, что даёт один doorbell на всю пачку вместо отдельных для каждой работы. При глубокой очереди пакетирование по 16-32 WR сокращает число записей в doorbell области на порядок и снижает загрузку шины PCIe. Отдельное внимание заслуживают unsignaled отправки: флаг IBV_SEND_SIGNALED порождает CQE на каждый WR, и при миллионах отправок очередь завершения переполняется быстрее самих отправок. Практика ставит подтверждение на каждую N-ю работу, например на 64-ю, а при ошибке связи адаптер сам генерирует CQE со статусом ошибки для конкретного WR, так что пропуск сигналов не прячет проблемы.
Управление кредитами, RNR таймер и QP Rate Limiter
Адаптер не отправляет пакеты быстрее, чем сеть их проглатывает. В RC, Reliable Connected, кредитная система работает автоматически: получатель возвращает ACK и освобождает слот окна отправки. Размер окна задаётся при создании QP через cap.max_send_wr и параметрами перехода в состояние RTS:
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_RTS,
.max_rd_atomic = 16,
.min_rnr_timer = 12,
.retry_cnt = 7,
.rnr_retry = 7,
};
Поле max_rd_atomic ограничивает число неквитированных RDMA Read и Atomic операций. Для чистой отправки SEND это не влияет, но для смешанных нагрузок ограничивает давление на получателя, типичные значения от 4 до 16.
Поле min_rnr_timer задаёт таймер RNR, Receiver Not Ready, в степенях двойки микросекунд. Значение 12 соответствует 4 мс. Если получатель переполнен, отправитель ждёт этот интервал перед повтором. На нагруженных кластерах ставят 8-10, от 256 мкс до 1 мс, чтобы быстрее реагировать на освобождение приёмных буферов.
Поля retry_cnt и rnr_retry задают число повторов при потере пакета и при RNR. Значение 7 максимальное, что даёт до 128 попыток, и для устойчивой работы при кратковременных перегрузках ставят именно его.
Rate limiter на ConnectX-5, ConnectX-6 и EFA ограничивает пропускную способность конкретного QP и включается модификацией QP с флагом IBV_QP_RATE_LIMIT:
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_RTS,
.rate_limit = 125000000,
};
Значение rate_limit задаётся в байтах в секунду, 125000000 примерно соответствует гигабиту. Механизм полезен в мультитенантности, чтобы шумный арендатор не съел полосу соседей. На EFA ограничитель работает на уровне порта, на ConnectX на уровне QP.
Inline threshold, глубина очереди и память под WQE
Экспериментальный подбор inline порога выполняется циклом:
for inline in 0 64 128 256; do
./bench --inline=$inline --size=64 --count=100000
done
Типичные результаты на ConnectX-6 при RC и MTU 4096 складываются в такую картину: для сообщений 64 байта задержка падает с 1.25 мкс при inline 0 до 0.90 мкс при inline 256; для 256 байт с 1.30 до 1.02 мкс; для 1 КБ разница исчезает, 1.45 против 1.40 мкс. Отсюда правило: inline 64-128 оптимален для RPC-подобных нагрузок, для сообщений больше MTU inline бесполезен, фрагментация всё равно пойдёт через SGE.
Память под Send Queue выделяется при создании QP. Размер кольца примерно равен произведению max_send_wr на сумму размера WQE и inline. На ConnectX-6 базовый WQE занимает 64 байта, при inline 128 выходит 192 байта на слот. При глубине 4096 это 786 КБ на QP, а при 1000 QP только под очереди отправки уходит 786 МБ.
Оптимальная глубина зависит от паттерна: для bursty-нагрузок порциями по сотне тысяч сообщений нужна глубина 4096-8192, для равномерного потока достаточно 1024-2048. Слишком большая глубина удлиняет очистку при ошибке связи: при разрыве адаптер вычищает все WQE, и при 8192 это занимает миллисекунды.
Мониторинг заполненности ведётся через счётчики в каталоге /sys/class/infiniband/mlx5_0/ports/1/counters/ или через ibv_query_qp. Полезный счётчик swqe_avail показывает свободные слоты в SQ: если он регулярно падает до нуля, глубину увеличивают или уменьшают темп постановки.
Цепочки SGE, векторный ввод-вывод и кредиты в UD
Для scatter-gather ввода-вывода приложение готовит массив struct ibv_sge. Пример отправки заголовка вместе с телом:
struct ibv_sge sge[2];
sge[0] = (struct ibv_sge){.addr = (uintptr_t)hdr, .length = sizeof(hdr), .lkey = mr->lkey};
sge[1] = (struct ibv_sge){.addr = (uintptr_t)body, .length = body_len, .lkey = mr->lkey};
struct ibv_send_wr wr = {
.wr_id = id,
.sg_list = sge,
.num_sge = 2,
.opcode = IBV_WR_SEND,
.send_flags = IBV_SEND_SIGNALED,
};
Максимальное число SGE в WR ограничено значением max_send_sge при создании QP, обычно от 16 до 32. На ConnectX-6 до 32 SGE на WR. Для длинных iovec, например при записи файла, цепочка SGE может быть длинной, но каждый SGE это запись в WQE, и длинная цепочка удлиняет постановку. Смежные буферы выгоднее объединять в один SGE, если они лежат в одной зарегистрированной области памяти MR.
Регистрация памяти выполняется ibv_reg_mr с флагами доступа и кешируется: lkey подставляется в SGE, rkey передаётся удалённой стороне для RDMA операций. Регистрация дорогая, она пинит страницы и строит таблицы трансляции в адаптере, поэтому буферы регистрируют один раз при старте и переиспользуют пулом. Большие области регистрируют на hugepages, что сокращает число записей в таблицах трансляции и стабилизирует задержку постановки. Кеш lkey в приложении экономит вызовы ibv_reg_mr при каждом сообщении, иначе накладные расходы легко превышают саму отправку.
В UD, Unreliable Datagram, автоматического окна нет, контроль потока полностью на приложении. Приёмник должен иметь достаточно приёмных WQE, иначе пакеты молча отбрасываются. Отправитель реализует собственный кредитный протокол: получатель в каждом пакете возвращает число свободных RQE, отправитель не превышает доступные кредиты. Для UD важен и параметр QKEY, общий для всех участников группы, стандартное тестовое значение 0x11111111.
Примеры оптимизации для типичных нагрузок
Для RPC-сервиса с мелкими сообщениями и высокой частотой подбирают: max_send_wr 2048, max_send_sge 1, max_inline_data 128, max_rd_atomic 4, min_rnr_timer 8, retry_cnt 7. Такой профиль даёт задержку около 0.9 мкс на сообщении 64 байта и порядка полутора миллионов сообщений в секунду на ядро.
Для хранилища с большими блоками и высокой полосой: max_send_wr 4096, max_send_sge 16, max_inline_data 0, max_rd_atomic 16, min_rnr_timer 12, retry_cnt 7. Профиль выжимает около 95% пиковой полосы на блоках 256 КБ.
Для целевых систем NVMe-oF профиль отличается: глубина SQ 8192 без inline с max_sge 32 и подтверждением каждой 32-й операции даёт максимум полосы на блоках 4 КБ. Пакетирование постановки по 32 WR за один вызов ibv_post_send сокращает число doorbell записей в 32 раза, а привязка потока отправки к ядру на том же NUMA-узле, что и адаптер, убирает пересылку дескрипторов между узлами. На ядре 5.10 и новее io_uring с поддержкой RDMA позволяет объединить постановку отправки и обработку завершения в одной связке системных вызовов, что сокращает накладные расходы на 15-20%.
Для мультитенантного кластера ставят rate limiter на каждом QP арендатора, отдельные CQ на арендатора, глубину очереди по квоте от 1024 у базового тарифа до 4096 у премиального и лимиты полосы от гигабита до десяти.
Дополнительные приёмы для сверхбыстрых путей включают отдельный поток опроса CQ, вынесенный с рабочих ядер, экономию CQE через unsignaled отправки с подтверждением каждую N-ю операцию и IBV_WR_SEND_WITH_IMM вместо отдельного WR подтверждения, что сокращает число WQE вдвое. На ConnectX-7 и BlueField-3 работает Adaptive Routing, включаемый через mlxconfig AdaptiveRouting=1: адаптер сам выбирает менее загруженный путь, и задержка в перегрузке падает на 30-40%.
Типичные ошибки и диагностика очередей отправки
- max_send_wr слишком мало, приложение блокируется в ibv_post_send с ENOMEM и swqe_avail на нуле, лечение в увеличении глубины или добавлении backoff;
- inline слишком большой, WQE перестаёт помещаться в кеш и задержка растёт, лечение в снижении порога до 64-128 байт;
- max_send_sge равен единице при scatter-gather нагрузке, приложение гоняет лишние memcpy, лечение в поднятии лимита до 4-8;
- RNR таймер слишком большой, при переполнении получателя отправитель ждёт секунды, лечение в min_rnr_timer 8-10;
- rate limiter не включён, шумный арендатор съедает полосу соседей, лечение в IBV_QP_RATE_LIMIT на каждом QP.
Диагностика опирается на ibv_query_qp, который показывает текущие параметры QP, и на счётчики портов: swqe_avail, rnr_nak, retry. Профилирование отправки на ядрах 5.10 и новее выполняется через perf record -e rdma:rdma_send. Для продакшена счётчики swqe_avail и rnr_nak выносят в мониторинг с алертами: заполнение очереди выше 90% и растущие RNR предупреждают о перегрузке раньше, чем начнут расти задержки.
Отдельный класс проблем связан с очередью завершения: CQ обязана вмещать все неподтверждённые отправки, иначе возникает переполнение, и адаптер переходит в состояние ошибки. Если все WR сигнализированы, глубина CQ должна быть не меньше суммы глубин SQ всех привязанных QP, при экономии сигналов достаточно доли. Проверка состояния CQ через ibv_poll_cq со статусами IBV_WC_CQ_ERR и IBV_WC_WR_FLUSH_ERR показывает переполнение и вымывание очереди при разрыве связи.
Итог простой: глубина очереди, порог inline и таймеры QP подбираются под профиль нагрузки за один вечер тестов, а кредитная дисциплина и rate limiter превращают кластер RDMA в предсказуемую систему, где шумный сосед не влияет на критичный путь.