Очередь пар, QP, сердце RDMA-коммуникации: через неё проходят все операции отправки и приёма, и её состояние определяет, что разрешено делать приложению в каждый момент. Машина состояний QP строго формализована в спецификации InfiniBand, и переходы между состояниями управляются вызовом ibv_modify_qp с битовыми масками атрибутов. Ошибочный порядок переходов даёт статус EINVAL, и разбор этих переходов это половина обучения работе с libibverbs.

Разберём полный набор состояний, последовательность переходов для базовых типов транспорта, работу с атрибутами на каждом этапе, диагностику типичных ошибок, механизм сброса и практические приёмы работы. Материал собран с прицелом на разработчика, который пишет код на libibverbs и встречает EINVAL при переходах, не понимая, где именно последовательность нарушена. Материал собран с прицелом на разработчика, который пишет код на libibverbs и встречает EINVAL при переходах, не понимая, где именно последовательность нарушена.

Полный набор состояний QP

Спецификация определяет восемь состояний. Reset начальное состояние после создания: очередь пуста, все внутренние структуры обнулены, никаких операций не разрешено. Init конфигурационное состояние: принимаются атрибуты partition key, номер очереди приёма, доступ к портам, но передача и приём запрещены. Ready to Receive RTR: очередь приёма подключена к удалённой стороне, входящие пакеты принимаются, отправка ещё запрещена. Ready to Send RTS: очередь отправки активна, операции передаются в сеть, полный рабочий режим. SQD Send Queue Drained: очередь отправки сливается, новые работы не принимаются, текущие завершаются. SQE Send Queue Error: ошибка в очереди отправки, все работы завершены с ошибкой, очередь требует сброса. ERR Error: фатальная ошибка на любой стороне, очередь не работает. Unknown неиспользуемое значение для совместимости. Восемь состояний образуют полный граф жизненного цикла, и каждое имеет свои разрешённые операции и атрибуты.

Жизненный цикл типичного RC-соединения: Reset, затем Init, затем RTR, затем RTS, и после завершения работы обратно через Reset для переиспользования или destroy для уничтожения. Каждый переход разрешён только в определённом направлении, и попытка перескочить несколько состояний сразу отвергается драйвером. Граф переходов ациклический с циклом через Reset, и его нарушение мгновенно видимо в EINVAL.

Переходы состояний для RC-соединений с полными атрибутами

Для Reliable Connection полный путь от создания до готовности выглядит так. После ibv_create_qp QP находится в Reset. Первый переход в Init задаёт partition key, порт и флаги доступа:

struct ibv_qp_attr attr = {0};
attr.qp_state = IBV_QPS_INIT;
attr.pkey_index = 0;
attr.port_num = 1;
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ;
ibv_modify_qp(qp, &attr,
    IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS);

Флаги доступа определяют, какие удалённые операции разрешены на этом QP: запись, чтение, атомики. Второй переход в RTR требует информации об удалённой стороне: номер очереди, глобальный идентификатор, локальный идентификатор порта:

attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_4096;
attr.dest_qp_num = remote_qp_num;
attr.rq_psn = remote_psn;
attr.ah_attr.dlid = remote_lid;
attr.ah_attr.grh.dgid = remote_gid;
attr.ah_attr.port_num = 1;
attr.max_dest_rd_atomic = 16;
attr.min_rnr_timer = 12;
ibv_modify_qp(qp, &attr, /* маска RTR атрибутов */);

Третий переход в RTS добавляет параметры отправителя: таймаут, счётчик повторов, начальный PSN, глубина атомиков:

attr.qp_state = IBV_QPS_RTS;
attr.timeout = 14;
attr.retry_cnt = 7;
attr.rnr_retry = 7;
attr.sq_psn = local_psn;
attr.max_rd_atomic = 16;
ibv_modify_qp(qp, &attr, /* маска RTS атрибутов */);

После этого QP готов к передаче данных, и ibv_post_send кладёт работы в очередь. Параметры надёжности в RTS определяют поведение при потерях: timeout задаёт интервал ожидания подтверждения, retry_cnt число повторов доставки, rnr_retry поведение при исчерпании буферов приёмника. Значение timeout в единицах 4.096 микросекунды, типичное 14 даёт около 65 миллисекунд, и для тяжёлых нагрузок его поднимают. Счётчик retry_cnt до 7 даёт семь попыток доставки до фатального отказа, и на сетях с дрожанием задержки большее число повторов спасает от ложных обрывов. Параметр rnr_retry контролирует поведение при Not Ready to Receive: значение 7 означает бесконечные повторы, ноль немедленную ошибку.

Переходы для UD и стеков на их основе

Ненадёжный датаграммный режим UD имеет упрощённый путь: Reset, Init, RTR, RTS, и все переходы одинаковы по механике, но атрибуты отличаются. UD не требует параметров надёжности вроде retry и timeout, но требует QKey для аутентификации отправителей. QKey задаётся в Init и проверяется на стороне приёмника: пакет с неверным ключом отбрасывается, и это простой механизм доступа к группе датаграмм. Установленное значение QKey в QP видно через ibv_query_qp, и на фермах его сверяют между узлами при разборе пропавших пакетов. После Init QP готов к приёму с прикреплённым SRQ или собственными буферами, после RTS готов к отправке. Для UD переход RTR требует минимум атрибутов: только QKey и опционально pkey_index, без данных об удалённой стороне, потому что UD не устанавливает соединение, а готовится принимать датаграммы от любых отправителей с верным ключом.

UD используется как транспорт для верхних стеков: RoCE, iWARP и пользовательские протоколы поверх. В таких случаях прикладной слой управляет состояниями сам, и логика переходов инкапсулирована в библиотеке вроде rdma_cm или в собственном коде поверх libibverbs. При использовании rdma_cm переходы скрыты, и приложение работает с готовым QP, а при ручном управлении разработчик отвечает за корректность последовательности. Выбор пути определяет, кто диагностирует ошибки переходов: библиотека или собственный код.

Ошибки состояний и диагностика

Типичные ошибки при переходах: переход в состояние из неверного источника, пропуск обязательных атрибутов, установка атрибутов из чужого состояния. Все они дают EINVAL из ibv_modify_qp, и диагностика сводится к проверке порядка переходов и полноты атрибутов. Типичный сценарий из практики: разработчик забывает перейти в Init перед RTR и получает EINVAL, недоумевая, ведь все атрибуты заполнены. Причина в порядке, и журнал переходов мгновенно показывает пропущенный шаг. Ядро возвращает конкретную маску атрибутов, которые отсутствуют, и разбор этого сообщения быстрее перебора.

Фатальные переходы в SQE и ERR происходят под нагрузкой при проблемах сети или прошивки. QP в ERR не восстанавливается автоматически, и путь один: сброс через Reset, пересоздание или переход обратно в Init с повторным прохождением цепочки. Приложение ловит событие IBV_EVENT_QP_FATAL в асинхронном канале и запускает процедуру восстановления. Причиной фатального перехода бывают проблемы сети, обрыв линка, ошибка прошивки, или внутренняя ошибка драйвера, и понимание источника через сетевые счётчики сокращает разбор. Состояние SQE отличается от ERR тем, что очередь отправки повреждена, но очередь приёма ещё работает, и в некоторых протоколах принимающая сторона продолжает получать данные, пока отправляющая застряла в ошибке.

Сброс QP через переход в Reset очищает все очереди и счётчики, и QP возвращается в исходное состояние. После сброса можно пройти цепочку Init, RTR, RTS заново с новыми атрибутами, и QP работает как только что собранный. Это дешевле, чем уничтожение через ibv_destroy_qp и создание через ibv_create_qp, потому что ресурсы ядра и память очередей сохраняются.

На горячих путях с частым переподключением, например кластеры с нестабильной сетью, схема Reset плюс повторное Init RTR RTS работает быстрее пересоздания. Разница в сотни микросекунд на цикл переподключения, и для сценариев с переподключением раз в секунду на тысячах соединений это заметный процент времени восстановления кластера. При сбросе PSN и счётчики обнуляются, и обе стороны обязаны обменяться новыми значениями через механизм обмена вроде rdma_cm или собственный протокол, иначе рассинхрон PSN приведёт к отбрасыванию пакетов после формальной готовности.

Взаимодействие машины состояний с rdma_cm

Библиотека rdma_cm автоматизирует переходы: rdma_create_qp создаёт QP, rdma_connect или rdma_accept проводят через Init, RTR, RTS в правильном порядке, и приложение работает с готовым QP. Под капотом rdma_cm вызывает ibv_modify_qp с правильными атрибутами, и прикладной код не касается состояний напрямую. Для приложений, где тонкий контроль не нужен, rdma_cm скрывает всю механику. При этом rdma_cm сам обрабатывает обмен параметрами между сторонами через протокол соединения, и разработчик получает готовый QP с согласованными PSN и атрибутами без ручного обмена.

Ручное управление через ibv_modify_qp даёт контроль над каждым атрибутом, но требует понимания машины состояний. Выбор между rdma_cm и ручным управлением решается уровнем контроля: для стандартных соединений rdma_cm проще, для экзотических сценариев с нестандартными параметрами ручной путь единственный. Пример экзотики: соединение с асимметричными параметрами надёжности, где стороны имеют разные retry_cnt, или переход в SQD для контролируемого слива очереди перед обновлением.

Проверка текущего состояния QP делается через утилиту ibv_qp_state или чтение из sysfs:

cat /sys/class/infiniband/mlx5_0/qp/*/state
ibv_devinfo -v | grep -A10 'qp'

Вывод показывает состояние каждого QP, и по нему видно, где застряло соединение. Дополнительно perfquery и ibdiagnet дают телеметрию фабрики, и сопоставление состояния QP с сетевыми счётчиками локализует проблему до уровня приложения или сети. Счётчик локальных ошибок порта указывает на проблему в драйвере или прошивке, счётчик удалённых на проблему у партнёра, и такой разбор сокращает круг подозреваемых вдвое.

Для отладки переходов полезно писать в журнал каждое изменение состояния с атрибутами, и по логу видно, где цепочка разорвалась. В продакшене журнал переходов сокращает разбор инцидентов с часов до минут. Формат записи с временными метками, номером QP, старым и новым состоянием и маской атрибутов даёт полную картину каждого перехода, и по логу видно, какие атрибуты отсутствовали при ошибке.

Практический порядок работы с состояниями QP

Работа с состояниями QP в проекте укладывается в шесть шагов:

  1. выбрать транспорт: RC для надёжных соединений, UD для датаграмм, и уточнить последовательность переходов для выбранного типа;
  2. написать обёртку вокруг ibv_modify_qp, которая валидирует порядок переходов и логирует каждый шаг с атрибутами;
  3. обработать фатальные состояния через подписку на асинхронные события и процедуру восстановления через Reset и повторный проход;
  4. для горячих путей с переподключением использовать схему Reset плюс повторный проход вместо пересоздания;
  5. выбрать rdma_cm для стандартных сценариев, ручное управление для нестандартных;
  6. добавить мониторинг состояний QP через sysfs в общий дашборд и алертинг на QP в ERR, а также на QP в SQE, который предвещает фатальный отказ.

Итог у машины состояний QP строгая и предсказуемая: восемь состояний, чёткая последовательность переходов, полная диагностика через sysfs и асинхронные события. Понимание этой машины отличает разработчика, который пишет рабочий RDMA-код, от того, кто копирует примеры без понимания. Настройка правильной процедуры восстановления после фатальных состояний превращает приложение в устойчивое к сбоям сети. Настоящая ценность понимания состояний раскрывается на большой ферме: когда сотни соединений работают одновременно, один застрявший QP в Init или SQE становится точкой отказа для всего кластера, и только ясное представление о машинке состояний позволяет найти его за минуты, а не часы.