Когда на один сервер RDMA одновременно дышат сотни клиентов, RC-очереди превращаются в приёмную директора с вечной очередью у двери. Каждая пара QP хранит своё состояние, каждое сообщение ждёт подтверждения, а память под приёмные буферы растёт быстрее самого трафика. У дейтаграммного транспорта UD, помеченного в libibverbs как IBV_QPT_UD, философия совсем иная. Он почти ничего не обещает: ни порядка, ни доставки, ни повторов. Зато и почти ничего не хранит и не тратит. Именно поэтому IPoIB, телеметрия кластеров и широковещательные рассылки опираются на него. Дальше идут команды, расчёт накладных расходов в 60 байт на пакет и методика измерений в perftest, чтобы выбор транспорта опирался на цифры, а не на привычку.
Дейтаграммный транспорт среди четырёх типов QP
В libibverbs у QP четыре классических типа. RC (Reliable Connection) гарантирует доставку, порядок и целостность, подтверждает каждое сообщение и собирает из фрагментов сообщения длиной до 2 ГБ. UC (Unreliable Connection) связан строго с одним парным QP, фрагментацию поддерживает, а подтверждений не шлёт. UD (Unreliable Datagram) живёт по самым свободным правилам: один QP общается с любым другим UD QP фабрики, и в уникаст, и в мультикаст.
Ограничения у UD жёсткие. Таблица опкодов у ibv_post_send разрешает для IBV_QPT_UD только IBV_WR_SEND, IBV_WR_SEND_WITH_IMM и, при поддержке устройством, IBV_WR_TSO. Ни RDMA WRITE, ни RDMA READ, ни атомарных операций. Сообщение целиком укладывается в один пакет, максимум полезной нагрузки задаёт MTU пути, фрагментации нет вовсе. Пакет с испорченным CRC или нарушенным порядком приёмник молча выбрасывает, сеть не возвращает отправителю ни единого байта. Отправитель получает своё завершение, как только пакет ушёл в фабрику, и не знает, долетел ли он.
Плата за такую небрежность окупается экономикой состояния. UD QP не требует ничего согласовывать с парной стороной: ни адресного вектора, ни параметров повторов, ни таймеров. Один QP на процесс вместо тысяч QP по числу клиентов экономит память HCA, а в perftest ключ -q принимает до 16384 QP в одном процессе. Для кластера, где тысяча узлов шлёт метрики одному коллектору, это разница между работающим сервисом и разваливающимся.
Шестьдесят байт overhead в каждом пакете UD и их цена
Каждый UD пакет несёт три заголовка. GRH (Global Routing Header) занимает ровно 40 байт, BTH (Base Transport Header) 12 байт, DETH (Destination Transport Header) 8 байт. В сумме 60 байт служебных данных плюс 4 байта ICRC в хвосте, что в исходниках perftest отражено константами UD_ADDITION (40) и HW_CRC_ADDITION (4). Верхнюю границу полезной нагрузки определяет MTU. В ud_pingpong размер вычисляется как 1 << (port_info.active_mtu + 7), то есть значения enum ibv_mtu идут от 256 до 4096 байт степенями двойки.
Дальше простая арифметика. При MTU 256 полезная часть занимает 256 из 320 байт, всего 80%. При 512 получается 88.9%, при 1024 94.1%, при 2048 97.0%, при 4096 98.5%. На линке 100 Гбит/с потолок при MTU 4096 составит около 3.0 млн пакетов в секунду, из которых на данные придётся примерно 98.5 Гбит/с. А вот мелкие датаграммы по 64 байта займут 64 из 128, ровно половину канала. Вот и цена свободы от состояния соединения.
Приёмная сторона должна помнить про GRH отдельно. В ud_pingpong из состава rdma-core приёмный буфер выделяется размером size + 40, SGE приёма описывает те же size + 40 байт, а буфер отправки начинается со смещения 40: оборудование кладёт GRH в первые 40 байт, payload ложится сразу за ним. У флага wc_flags в завершении приёма есть бита IBV_WC_GRH, по которой видно, что пакет шёл с глобальным роутингом. Об этих 40 байтах спотыкаются ровно один раз: приёмная очередь молча теряет сообщения с локальной ошибкой длины, и без опыта на разгадку уходит вечер.
ibv_devinfo и ibv_ud_pingpong первые команды на двух узлах
Начинают всегда с проверки порта. Команда ibv_devinfo -d mlx5_0 -p 1 -v печатает hca_id, node_guid, состояние порта, link_layer, lid, sm_lid и active_mtu. Именно active_mtu станет потолком payload для UD: path_mtu в ibv_modify_qp валиден только для RC и UC, а дейтаграммный QP ограничен фактическим MTU порта.
Дальше по сети пускают простейший тест. На первом узле ibv_ud_pingpong -d mlx5_0 -g 3 -s 2048 -n 10000 поднимает сервер, на втором та же команда с адресом, например ibv_ud_pingpong -d mlx5_0 -g 3 -s 2048 -n 10000 10.0.0.11, работает клиентом. Обе стороны печатают строки вида local address: LID 0x0002, QPN 0x00a41c, PSN 0x9d1f4c: GID fe80::2. Ключ -g выбирает индекс GID для GRH, -s задаёт размер датаграммы, -n число обменов, -r глубину приёмной очереди (по умолчанию 500), -l уровень сервиса SL, -e переводит опрос CQ в событийный режим, -c включает проверку содержимого буфера. Порт управления 18515 у этого примера общий с perftest.
Проверка связности UD сводится к такой последовательности:
1. ibv_devinfo подтверждает, что порт в состоянии PORT_ACTIVE и active_mtu не меньше планируемого размера датаграммы;
2. сервер ibv_ud_pingpong печатает свой LID, QPN, PSN и GID;
3. клиент повторяет те же ключи и добавляет адрес сервера последним аргументом;
4. обе стороны показывают одинаковый счёт обменов и скорость в Mbit/sec;
5. для RoCE или перехода через маршрутизатор добавляется -g с индексом GID, без него QP общается только внутри одного L2-домена по LID.
PSN в примере генерируется через lrand48() & 0xffffff, а hop_limit в GRH равен 64. Обмен адресами LID, QPN, PSN и GID идёт по обычному TCP-сокету, потому что у UD нет собственного установления связи, адресную книгу стороны ведут сами.
Машина состояний QP минимум атрибутов для IBV_QPT_UD
Перевод QP из состояния в состояние для UD на удивление скуп. У ibv_modify_qp в INIT требуются IBV_QP_STATE, IBV_QP_PKEY_INDEX, IBV_QP_PORT и IBV_QP_QKEY. В RTR достаточно одного IBV_QP_STATE, в RTS добавляется только IBV_QP_SQ_PSN. Сравните с RC: там RTR тянет за собой IBV_QP_AV, IBV_QP_PATH_MTU, IBV_QP_DEST_QPN, IBV_QP_RQ_PSN, IBV_QP_MAX_DEST_RD_ATOMIC и IBV_QP_MIN_RNR_TIMER, а RTS добавляет IBV_QP_RETRY_CNT, IBV_QP_RNR_RETRY и IBV_QP_TIMEOUT. Меньше атрибутов означает меньше мест, где можно ошибиться.
В коде это выглядит так:
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_INIT,
.pkey_index = 0,
.port_num = 1,
.qkey = 0x11111111
};
ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX |
IBV_QP_PORT | IBV_QP_QKEY);
Значение Q_Key 0x11111111 используют и ud_pingpong, и perftest в качестве значения по умолчанию, получатель проверяет его на каждом пакете. Чтобы отправлять пакеты, нужен address handle. Он создаётся один раз на пару порт-адресат через ibv_create_ah и описывает маршрут:
struct ibv_ah_attr ah_attr = {
.is_global = 1,
.grh.hop_limit = 64,
.grh.dgid = dest->gid,
.grh.sgid_index = 3
};
ah = ibv_create_ah(pd, &ah_attr);
Если сеть чисто InfiniBand внутри одного L2, обходятся полем dlid с is_global равным нулю. Отдельная тонкость: при флаге IBV_QPF_GRH_REQUIRED на порту атрибуты AH обязаны заполняться глобально, с GRH, иного не примет ни драйвер, ни оборудование.
ibv_post_send для UD поля wr.ud приём данных и типичные ошибки
Отправка датаграммы собирается из трёх деталей: описание буфера в ibv_sge, сам work request с опкодом IBV_WR_SEND и адресная часть wr.ud. Ниже сокращённый шаблон из ud_pingpong:
struct ibv_sge list = {
.addr = (uintptr_t) buf,
.length = size,
.lkey = mr->lkey
};
struct ibv_send_wr wr = {
.sg_list = &list,
.num_sge = 1,
.opcode = IBV_WR_SEND,
.send_flags = IBV_SEND_SIGNALED,
.wr.ud = {
.ah = ah,
.remote_qpn = dest->qpn,
.remote_qkey = 0x11111111
}
};
ibv_post_send(qp, &wr, &bad_wr);
Флаг IBV_SEND_SIGNALED даёт завершение на стороне отправителя, IBV_SEND_INLINE вкладывает данные прямо в WQE: буфер можно переиспользовать сразу после возврата из вызова, и L_Key даже не проверяется. Зато AH трогать нельзя: изменять или уничтожать address handle до получения завершения грозит непредсказуемыми последствиями, это одно из жёстких правил отправки.
На приёме после ibv_post_recv очередь разгребают через ibv_poll_cq, сверяя wc.status с IBV_WC_SUCCESS. Три ловушки встречаются чаще остальных. Первая, приёмный SGE короче размера пакета плюс 40 байт GRH, даёт IBV_WC_LOC_LEN_ERR и потерю сообщения. Вторая, чужой Q_Key: пакет с неправильным значением получатель отбрасывает без всякой обратной связи, и отправитель узнаёт о проблеме только по пропавшим датаграммам. Третья, преждевременное уничтожение AH, что заканчивается мусорными завершениями или крахом драйвера. Бита IBV_WC_GRH в wc_flags помогает отделить глобально маршрутизируемые пакеты от локальных.
perftest ib_send_bw и ib_send_lat методика измерений на UD
Комплект perftest ставится отдельным пакетом рядом с rdma-core и меряет всё, что нужно. Сервер и клиент запускаются с одинаковыми ключами, расхождение опций меняет картину и обесценивает замер. Версии тоже обязаны совпадать: сборки 5.3 и новее не понимают более старые, а с релиза 5.70 порог совместимости поднят ещё раз.
Сервер: ib_send_bw -c UD -d mlx5_0 -i 1 -x 3 -m 4096 -s 4096 -n 20000 -t 256 -r 600. Клиент повторяет всё и добавляет адрес: ib_send_bw -c UD -d mlx5_0 -i 1 -x 3 -m 4096 -s 4096 -n 20000 -t 256 -r 600 10.0.0.11. Здесь -c UD выбирает транспорт, -m фиксирует MTU (иначе берётся active_mtu из ibv_devinfo), -x указывает индекс GID, -s размер сообщения, -n число итераций, -t и -r глубины очередей отправки и приёма. Отчёт идёт колонками #bytes, #iterations, BW peak, BW average в Гбит/с и MsgRate в Mpps.
Ключ -a прокатывает весь диапазон размеров от 2 до 2^23 байт и мгновенно показывает, где кончается линейный участок. Для замеров по времени есть -D 10 (тест на протяжение 10 секунд) и --run_infinitely с выводом каждые 5 секунд. Один пост-send упирается в программный потолок порядка 10 Mpps, между постами проходит около 500 нс, поэтому ключ -l 64 строит цепочку WQE и высвобождает настоящий темп оборудования. Несколько QP в одном процессе включаются -q 4. Мультикаст проверяется ключами -g 2 -M ff12:401f::1, группа ограничена 56 QP. Двунаправленный замер -b в UD и UC в редких случаях подвисает, с релиза 2.3 детектор прерывает тест через 2 минуты тишины.
Задержку меряет ib_send_lat с теми же ключами. Он печатает t_min, t_typical, t_max, среднее, стандартное отклонение и перцентили 99% и 99.9%; медиана надёжнее среднего при выбросах, а первая выборка почти всегда максимальна из-за прогрева. Ключ -C переводит вывод в такты процессора, -H печатает гистограмму. Есть путь и через librdmacm: ключ -R поднимает QP через rdma_cm по IPoIB-адресу, а функция rdma_post_ud_send оборачивает отправку UD в готовый вызов.
Тонкая настройка MTU глубины очередей и inline данных
С MTU решение простое на бумаге и нетривиальное в жизни. Для перекачки объёмов MTU 4096 почти удваивает полезную долю против MTU 256, но большие пакеты сильнее нагружают буферы коммутаторов и чаще дробят канал при конкуренции потоков. Хороший ориентир даёт IPoIB, работающий поверх UD: его дейтаграммный MTU 4092 получается из 4096 минус 4 байта собственного заголовка. Мелкие датаграммы MTU не замечают вовсе, пока размер сообщения не приблизится к потолку.
Глубины очередей подбирают под характер нагрузки. Дефолты perftest честные: rx-depth 600 в общем случае и 512 в ib_send_bw, tx-depth 128. Ключ -Q 100 включает модерацию CQ: завершения группируются по сто штук, и процессор не горит на пустом опросе, размер пакета опроса в perftest равен 16. Если QP много, память спасает SRQ: одна общая приёмная очередь на все QP, в perftest для UD её глубина начинается со 100 WQE. Грубая прикидка: 1000 QP с rx-depth 600 и буфером 4096 плюс 40 байт съедят порядка 2.4 ГБ, с SRQ цифра падает на порядок.
Inline стоит подбирать отдельно: для UD perftest по умолчанию берёт 188 байт против 236 у RC и UC, а потолок MAX_INLINE_UD равен 1024. Датаграммы до 188 байт уходят без обращения к памяти через MR, что заметно на телеметрии с сообщениями по 64 байта.
Порядок работы со зрелым кластером вырисовывается сам. Сначала ib_send_bw -a строит кривую по размерам, затем пара прогонов с -l 64 и -q 4 отделяет программный потолок от предела фабрики, ib_send_lat с -H показывает хвост задержек, и только после этого MTU, rx-depth и inline крутят по одному параметру за раз. Итоговая картинка бывает отрезвляющей: 60 байт заголовков не исчезнут никогда, при MTU 4096 они почти неощутимы, при датаграммах по 64 байта решают половину бюджета канала. В новых стеках рядом с UD работают DC и SRD, доступные тем же ключом -c в perftest, и прежде чем переписывать приложения под новую транспортную модель, стоит прогнать по ним те же замеры.