Кластерный обмен по RDMA опирается на тихий, но прожорливый механизм: каждая очередь QP и каждый блок зарегистрированной памяти MR привязаны к protection domain, и от раскладки доменов зависит и скорость старта приложения, и расход ресурсов адаптера. Схема "домен на каждое соединение" выглядит аккуратно в коде, но на узле с сотнями соединений превращается в сотни однотипных объектов, сотни регистраций и повторный расход времени на каждом переподключении клиента. Общий домен собирает все очереди вокруг одного пула памяти: домен в единственном экземпляре, регистраций несколько, буферы переиспользуются всеми соединениями сразу. Ниже разобрано, как устроена эта граница доступа, что она экономит на реальных числах, чем проверяется командами и где проходит предел, за которым делить домены все же нужно.

Роль protection domain как границы доступа для QP и MR

Домен защиты создает вызов ibv_alloc_pd для контекста устройства, и дальше все ключевые объекты привязываются именно к нему: очередь пар QP, общий приемник запросов SRQ, область памяти MR, окна MW и адресные обработчики AH для дейтаграмм. Смысл границы простой: объекты одного домена имеют право ссылаться друг на друга, а через границу адаптер не пускает. Очередь, получившая в рабочий запрос lkey буфера из чужого домена, остановится с ошибкой, которую очередь завершений покажет строкой local protection error, и искать причину нужно именно в связке QP и MR. Удаленный доступ проверяется отдельно через rkey, ключ, который вторая сторона получает заранее. Домен принадлежит контексту одного устройства: сквозь два порта одной карты он работает, а между mlx5_0 и mlx5_1 общий домен построить нельзя, понадобится по одному на каждое устройство.

Сам домен почти ничего не стоит, и в этом ловушка: завести его по щелчку легко, а вычищать потом сложно. Порядок разбора жесткий: сначала ibv_destroy_qp для всех очередей, затем ibv_dereg_mr и ibv_destroy_ah, и только после пустого домена вызов ibv_dealloc_pd завершается успешно. Полезно помнить и про общий приемник SRQ: он тоже привязан к домену, и одна приемная очередь на несколько QP внутри одного домена работает без проблем, дополнительно экономя буферы.

Формальный лимит задает поле max_pd в атрибутах устройства, и печатает его команда ibv_devinfo с флагом -v:

ibv_devinfo -d mlx5_0 -v | grep -i max_pd

На адаптерах NVIDIA семейства ConnectX это число порядка 2^24, то есть больше шестнадцати миллионов, и формально домены не кончаются никогда. Практика упирается не в лимит, а в издержки, которые каждый лишний домен приносит вместе со своими областями памяти.

Цена отдельного домена на каждое соединение при сотнях QP

Типовой сценарий проясняет масштаб. Узел хранения обслуживает 200 клиентских сессий, на каждой открыты две очереди RC, одна для данных, вторая для управления, итого 400 QP. Схема "домен на соединение" дает 400 доменов, 400 регистраций памяти и по 8 ГиБ закрепленных буферов на каждую сессию, то есть 3200 ГиБ физической памяти, которую адаптер держит при себе и не отдает файлам. Регистрация восьми гигабайт на страницах по 4 КиБ порождает 2 097 152 записи трансляции, на весь узел выходит 838 860 800 записей, и каждое переподключение клиента повторяет путь заново: снять регистрацию и зарегистрировать буферы снова. Вдобавок сотни объектов MR и PD живут в ядре до конца работы и усложняют диагностику: в листинге ресурсов десятки одинаковых строк без опознавательных знаков.

Тот же узел с общим доменом выглядит иначе: один домен, пул на 32 ГиБ, пара областей памяти под разные классы буферов. Закрепленной памяти в 100 раз меньше, 32 ГиБ вместо 3200, а число регистраций падает с 400 до 2. Числа говорят сами за себя, и они же объясняют, почему в кластерных подсистемах вроде NVMe-oF и iSER один домен на узел стал привычной практикой.

Второй сценарий добавляет стимул. Сервис с автоподъемом соединений переживает сотни переподключений в сутки после коротких перебоев на сети, и в схеме раздельных доменов каждое из них тянет за собой регистрацию и отвязку восьми гигабайт. Регистрация такого объема на страницах по 4 КиБ занимает на типичном сервере порядка единиц секунд, и клиенты в эти секунды видят именно задержку подключения, а не быстрый отказ. При общем домене переподключение создает только пару QP и укладывается в доли секунды.

Общий домен в коде libibverbs и регистрация одного пула

Базовая последовательность вызовов libibverbs короткая, и общий вариант отличается от раздельного ровно тем, что указатель домена один и тот же для всех очередей:

struct ibv_pd *pd = ibv_alloc_pd(ctx);
struct ibv_mr *mr = ibv_reg_mr(pd, pool, POOL_BYTES,
        IBV_ACCESS_LOCAL_WRITE |
        IBV_ACCESS_REMOTE_WRITE |
        IBV_ACCESS_REMOTE_READ);

Флаги доступа задают, что можно делать с буфером: LOCAL_WRITE нужен самой карте для записи входящих данных, REMOTE_WRITE и REMOTE_READ открывают буфер чужим очередям, причем первый работает только вместе с LOCAL_WRITE. Очередь создается из того же домена, а в рабочих запросах используется lkey области:

struct ibv_qp_init_attr attr = {0};
attr.qp_type = IBV_QPT_RC;
attr.send_cq = cq;
attr.recv_cq = cq;
attr.cap.max_send_wr = 256;
attr.cap.max_recv_wr = 256;
attr.cap.max_send_sge = 4;
struct ibv_qp *qp = ibv_create_qp(pd, &attr);

В рабочем запросе буфер описывается элементом списка sge, и на месте ключа стоит именно lkey общей области:

struct ibv_sge sge = { .addr = (uintptr_t)buf, .length = len, .lkey = mr->lkey };

Ни адресат, ни удаленная сторона этот ключ не видят, он нужен только локальному адаптеру, поэтому один и тот же lkey спокойно живет во всех 400 очередях узла.

Быструю проверку живучести схемы дает утилита ibv_rc_pingpong, она внутри создает ровно один домен и одну область на соединение:

ibv_rc_pingpong -d mlx5_0 -g 0 -s 4096 -n 1000
ibv_rc_pingpong -d mlx5_0 -g 0 -s 4096 -n 1000 192.168.10.10

Первая команда стартует на сервере, вторая на клиенте с адресом сервера в конце, и по выводу видно время установки соединения и скорость на выбранных 4096 байтах.

Экономия на записях трансляции и большие страницы вместо мелких

Теперь про память адаптера. Каждая страница 4 КиБ в зарегистрированном буфере требует собственной записи трансляции, поэтому пул на 32 ГиБ на мелких страницах это 8 388 608 записей, и таблицы под них занимают заметные ресурсы адаптера, ограниченные запасом таблиц и параметром max_mr. Когда пул один и выделяется одним куском, его легко посадить на большие страницы по 2 МиБ, через mmap с флагом MAP_HUGETLB или прозрачные огромные страницы, и тогда те же 32 ГиБ уместятся всего в 16 384 записи, сокращение ровно в 512 раз. Побочный бонус: регистрация на крупных страницах проходит быстрее, и старт приложения перестает растягиваться на секунды ожидания закрепления страниц.

Проверить запас больших страниц в системе можно одной строкой:

grep -i huge /proc/meminfo

Если значение HugePages_Total нулевое, их заводят на ходу, и число 16384 при размере страницы 2 МиБ резервирует ровно 32 ГиБ под пул:

sysctl vm.nr_hugepages=16384

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

Раздельная схема лишает этого бонуса: 400 маленьких областей выделяются в разные моменты жизни приложения, куски не смежные, и выровнять их под большие страницы почти невозможно. Плюс экономия на самих буферах: один пул переиспользуется между сессиями, тогда как при домене на соединение каждому клиенту нужна своя порция, даже если работает он вполсилы.

Подсчет доменов, очередей и MR командой rdma resource show

В ядрах начиная с 4.16 утилита rdma из состава iproute2 умеет показывать ресурсы адаптера напрямую из ядра, без выгрузки статистики из приложения:

rdma resource show pd
rdma resource show qp | wc -l
rdma resource show mr | wc -l

Первая команда печатает строки вида:

pd 1 dev mlx5_0 ctx 1 cqs 3 qps 402 xrcds 0 mrs 2 pid 18925 comm ibv-storage

Читать ее стоит по полям qps и mrs: в примере на один домен приходится 402 очереди и всего 2 области памяти, это и есть рабочая картина общего домена на узле с 200 сессиями. Числа в хвосте строки показывают, сколько объектов других типов сидит внутри домена, а pid и comm называют владельца. Замер до перевода приложения и после дает объективную пару значений вместо споров на словах: если qps осталось прежним, а mrs упали с сотен до единиц, цель достигнута. Полезно и сравнение по устройствам: два порта одной карты отобразятся в одном контексте, а второй адаптер потребует отдельного домена, что видно по номеру ctx.

Посчитать домены конкретной карты помогает конвейер с grep:

rdma resource show pd | grep -c mlx5_0

Строка, где comm заключен в квадратные скобки, принадлежит ядру, например накопителю, работающему через iSER, и эти ресурсы в оптимизацию пользовательского приложения не входят, зато мешают читать листинг, поэтому их отфильтровывают по имени владельца.

Границы изоляции и выдача rkey за пределы узла

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

Отдельная история с XRC: расширение разделяемых очередей придумано как раз для совместного использования приемных ресурсов между процессами, и целевые очереди XRC одного домена принимают запросы от соседей, что исторически и подсказало инженерам идею общего домена.

Тонкий момент с rkey стоит проговорить прямо. Домен защищает локальные проверки: lkey чужого домена локальная очередь не примет. Ключ rkey после передачи второй стороне работает на адаптере без оглядки на домены очереди получателя, поэтому выдавать его стоит только узлам, которым доверяют доступ к пулу, и менять вместе с перерегистрацией. Для одного приложения из нескольких процессов в свежих версиях rdma-core есть ibv_import_pd, чтобы соседи по узлу пользовались общим доменом, а ibv_fork_init спасает закрепленную память при вызове fork. Освобождение домена завершится ошибкой, пока жив хоть один привязанный объект, и это свойство удобно как детектор утечек: если ibv_dealloc_pd не проходит, где-то осталась незакрытая очередь или область.

Порядок перехода на общий домен и типичные ошибки при внедрении

Перевод обычного приложения укладывается в пять шагов:

  1. снять стартовую статистику командой rdma resource show и зафиксировать числа pd, qps и mrs;
  2. выделить единый пул памяти под все соединения и зарегистрировать его в единственном домене с нужными флагами доступа;
  3. передавать указатель домена во все вызовы ibv_create_qp и хранить lkey пула в шаблонах рабочих запросов;
  4. крупные буферы перевести на страницы по 2 МиБ и пересчитать записи трансляции;
  5. прогреть соединения, сравнить rdma resource show с зафиксированными числами и закрыть проверку парой ibv_rc_pingpong или утилитами perftest.

Ошибки при внедрении повторяются из проекта в проект. lkey из области, зарегистрированной в другом домене, дает local protection error, и лечится это сверкой указателей домена, а не перезапуском. Чужой rkey, прилетевший по старому конфигу от предыдущей версии протокола, даст на удаленной стороне remote access error, и искать нужно в обмене ключами, а не в сетевом стеке. Наконец, часть команд проходит, а очередь не поднимается: обычно это перепутанные флаги доступа, когда REMOTE_WRITE включен без LOCAL_WRITE, и адаптер честно отказывает. Отдельно проверяют размер: пул, посаженный поверх всей памяти узла, закрепит столько, что ядру не останется страниц, и сервер начнет уходить в подкачку на ровном месте, так что размер пула выбирают с запасом, но не более половины оперативной памяти.

Общий домен не универсальный ключ: при гигантских пулах, доверенных соседях и сотнях однотипных очередей он выигрывает по всем показателям сразу, от секунд старта до записей трансляции, а на стыке недоверенных задач честнее отдать каждой группе свой домен. Правильная раскладка начинается с замера rdma resource show, продолжается одним пулом и заканчивается проверкой чисел, и именно она отличает выверенный кластерный код от программы, которая просто работает, пока соединений мало.