Сколько времени требуется процессору, чтобы добраться до нужного байта памяти? В идеальном мире этот вопрос имел бы единственный ответ. Реальность многопроцессорных систем сложнее. Память перестает быть единым пулом с равномерным доступом, превращаясь в распределенный ресурс с топологическими особенностями. Процессор получает данные из локального банка за десятки наносекунд, а обращение к памяти соседнего процессора может растянуться вдвое. Эта неоднородность доступа становится фундаментальной характеристикой современных серверов.

Путь от симметричной архитектуры к неоднородной прошли все крупные производители процессоров. AMD первой реализовала концепцию через технологию HyperTransport в процессорах Opteron в 2003 году. Intel последовала примеру с появлением QuickPath Interconnect в платформе Nehalem 2008 года. Казалось бы, шаг назад от унифицированного доступа. На практике единственный способ масштабировать системы до десятков и сотен ядер без превращения шины памяти в узкое горлышко.

Эволюция архитектурных решений

Симметричная многопроцессорная архитектура, известная как UMA, строилась вокруг единого контроллера памяти и общей шины. Все процессоры конкурировали за доступ к централизованному банку памяти через Front-Side Bus. При двух-четырех процессорах такая схема работала приемлемо. С ростом количества ядер ситуация ухудшалась катастрофически.

Шина представляет собой разделяемую среду, где одновременно может передавать данные только один участник. Остальные ожидают освобождения канала. Даже с применением арбитража и очередей приоритетов, пропускная способность делится между всеми процессорами. При восьми процессорах каждый получает лишь восьмую часть теоретического максимума. Длина проводников тоже вносит вклад: сигналы распространяются со скоростью около 15 сантиметров за наносекунду в материале FR-4, и каждый дополнительный дециметр трассы добавляет задержку.

Технология Gunning Transceiver Logic, использовавшаяся в FSB, требовала низких напряжений сигнала и терминирования на обоих концах линии. Intel развивала эту концепцию через GTL+ и AGTL+, доводя частоты до 1600 МГц с quad-pumping, обеспечивающим четыре передачи данных за такт. Однако фундаментальные ограничения оставались: физическая длина шины, количество нагрузок на линию, электрические паразиты. Централизованный контроллер памяти в северном мосту превращался в единую точку отказа и производительности.

NUMA разрешает эту проблему радикально. Каждый процессор получает собственный контроллер памяти и локальный банк DRAM. Процессоры связываются между собой через выделенные двунаправленные каналы: HyperTransport у AMD, QuickPath Interconnect у Intel, позже заменённый на Ultra Path Interconnect. Топология становится распределённой сетью, где каждый узел обслуживает запросы к своей памяти независимо.

Физика межпроцессорных соединений

HyperTransport реализован как пакетная последовательная шина с несколькими физическими линиями данных. Минимальная конфигурация использует 8-битную ширину канала, стандартная версия оперирует 16 битами, максимальная достигает 32 бит. Частота работы варьируется от 800 МГц до 3,2 ГГц в спецификации HT 3.1. При максимальных параметрах пропускная способность достигает 25,6 гигабайт в секунду в каждом направлении, суммарно 51,2 ГБ/с.

Протокол HyperTransport оперирует пакетами переменной длины. Каждый пакет содержит управляющую информацию, адресные данные и полезную нагрузку. Поддерживаются два типа записи: posted и non-posted. Posted-записи не требуют ответа от получателя, что снижает задержки для операций с высокой пропускной способностью типа DMA. Non-posted записи требуют подтверждения через сообщение "target done". Операции чтения всегда ожидают ответ с данными.

QuickPath Interconnect использует иную топологию. Каждый QPI состоит из двух 20-битных линий данных, работающих одновременно в противоположных направлениях. Передача происходит на фронтах тактового сигнала в обоих направлениях, удваивая эффективную скорость. Данные упаковываются во флиты по 80 бит, из которых 64 бита несут полезную информацию, остальные служат для управления и коррекции ошибок.

Тактовая частота QPI эволюционировала от 2,4 ГГц в Nehalem до 4,8 ГГц в Haswell. При частоте 3,2 ГГц односторонняя пропускная способность составляет 12,8 ГБ/с, двусторонняя достигает 25,6 ГБ/с. Intel удваивает это значение в спецификациях, учитывая одновременную работу обоих направлений. Важно понимать, что эти цифры представляют теоретический максимум для полезных данных без учёта служебной информации.

Ultra Path Interconnect, пришедший на смену QPI в платформе Skylake-SP 2017 года, сохранил базовые принципы, улучшив протокол и энергоэффективность. Латентность между сокетами через UPI составляет около 100-200 наносекунд в зависимости от конфигурации и расстояния.

Цена удалённого доступа

Обращение к локальной памяти начинается с проверки кэшей процессора. Промах в L3 инициирует запрос к контроллеру памяти, интегрированному в процессор. Контроллер формирует команды для модулей DIMM, получает данные и передаёт их обратно в кэш-иерархию. Типичная задержка локального доступа к DRAM после промаха L3 составляет 80-100 наносекунд для DDR4.

Удалённый доступ проходит дополнительные этапы. Процессор отправляет запрос через межпроцессорный канал к владельцу памяти. Пакет проходит через физический уровень, буферы приёма, маршрутизацию. Удалённый контроллер памяти обрабатывает запрос, извлекает данные, формирует ответный пакет. Данные возвращаются по тому же пути. Каждый переход через QPI или HyperTransport добавляет 40-80 наносекунд задержки.

Измерения показывают конкретные цифры. На двухсокетной системе с процессорами Intel Xeon локальный доступ демонстрирует задержку около 82 наносекунд. Удалённый доступ к памяти соседнего сокета растягивается до 164 наносекунд. Разница ровно вдвое, что совпадает с условными значениями в SLIT, где локальное расстояние обозначается как 10, удалённое как 20.

Пропускная способность страдает ещё сильнее. Локальная память на Sandy Bridge обеспечивает 65 ГБ/с при чтении всеми ядрами одного сокета. Удалённая память через QPI даёт лишь 15 ГБ/с. Падение более чем в четыре раза. Два QPI-линка между процессорами просто не справляются с полной утилизацией пропускной способности удалённого контроллера памяти.

Для систем AMD с HyperTransport картина ещё драматичнее. Процессоры Opteron использовали внутрипакетные HT-соединения между отдельными кристаллами. Latency между dies внутри одного процессора составляла заметную величину. Переход между сокетами через HT добавлял ещё больше задержки. В четырёхсокетных конфигурациях возникала необходимость многократных переходов, каждый из которых накапливал латентность.

Протокол когерентности в распределённой системе

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

Intel реализует home-snoop протокол. Каждый адрес памяти имеет "домашний" узел, где физически расположены данные. Запрос на чтение сначала направляется к домашнему узлу. Если данные существуют в модифицированном состоянии в кэше другого процессора, домашний узел инициирует snoop-запрос к владельцу. Процессор-владелец либо отправляет данные обратно запросившему, либо записывает их в память.

Протокол MESI расширяется состоянием Forward в реализации Intel. Строка кэша в состоянии Forward может быть передана другому процессору напрямую, без обращения к памяти. Это снижает задержки для shared read-only данных, часто запрашиваемых несколькими ядрами.

AMD применяет broadcast-based протокол с фильтрацией через HT Assist. Этот механизм отслеживает, какие строки кэша находятся в каких процессорах, используя дополнительную структуру данных. Запрос может быть направлен непосредственно к владельцу строки, избегая широковещательных сообщений. В четырёхсокетных системах без HT Assist каждый запрос потребовал бы опроса всех процессоров, создавая избыточный трафик.

Инклюзивный L3 кэш в архитектуре Sandy Bridge упрощает когерентность. L3 содержит копии всех данных из L1 и L2 нижестоящих ядер. Проверка когерентности требует только просмотра L3, без опроса индивидуальных кэшей ядер. Это снижает количество транзакций и сокращает латентность snoop-операций.

Интеллект операционной системы

Ядро Linux начало поддерживать NUMA с версии 2.5, но базовые механизмы оставались примитивными. Версия 3.8 ввела переработанный фундамент, позволивший развивать продвинутые стратегии. Версия 3.13 принесла Automatic NUMA Balancing, радикально изменивший подход к управлению размещением процессов и данных.

Механизм работает через искусственные страничные исключения. Периодически планировщик задач сканирует адресное пространство процесса порциями по 256 мегабайт. Виртуальные страницы временно помечаются как недоступные через установку флага PROT_NONE в записях таблиц страниц. Атрибут MM_CP_PROT_NUMA отличает эти служебные маркировки от реальных запретов доступа.

Попытка обращения к помеченной странице вызывает исключение доступа. Обработчик do_page_fault распознаёт NUMA hinting fault по специфической комбинации битов в коде ошибки. Страница присутствует в памяти, но доступ запрещён программно. Обработчик определяет, на каком NUMA-узле физически расположена страница и на каком узле выполняется процессор.

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

Миграция страниц запускается квадратичным фильтром. Однократное обращение к удалённой странице не вызывает перемещения. Только повторное обращение инициирует процедуру migrate_misplaced_page. Это предотвращает излишние копирования для данных, к которым обращались случайно. Миграция означает выделение новой физической страницы на локальном узле, копирование содержимого, обновление таблиц страниц, освобождение старой страницы.

Планировщик процессов тоже учитывает NUMA. Алгоритм пытается запускать задачу на том же узле, где размещена большая часть её памяти. Если память разбросана, планировщик может инициировать миграцию самого процесса к узлу с максимальной концентрацией данных. Задачи, активно обменивающиеся данными, группируются на одном узле через механизм task grouping.

Параметры /proc/sys/kernel/numa_balancing_scan_period_min_ms и numa_balancing_scan_period_max_ms контролируют частоту сканирования. Минимальный период 1000 миллисекунд по умолчанию, максимальный может достигать нескольких секунд. Период динамически регулируется в зависимости от статистики обращений. Высокая локальность увеличивает интервал, снижая overhead. Частые удалённые обращения сокращают период, ускоряя реакцию системы.

Инструменты ручной настройки

Утилита numactl предоставляет детальный контроль над размещением. Опция membind принудительно выделяет память на указанных узлах. Опция cpunodebind ограничивает выполнение процесса определёнными узлами. Опция preferred задаёт предпочтительный узел, допуская распределение на другие при нехватке ресурсов.

Команда numactl --hardware отображает топологию системы. Вывод содержит количество узлов, распределение процессорных ядер по узлам, объём памяти каждого узла. Матрица расстояний показывает относительную стоимость доступа между узлами. Значения 10 для локального доступа, 20 для прямого соединения между сокетами, большие числа для многопролётных путей.

Системный вызов mbind позволяет программно устанавливать политику памяти для конкретных диапазонов адресов. Политика MPOL_BIND жёстко привязывает выделение к узлам. MPOL_INTERLEAVE чередует страницы между узлами, распределяя нагрузку. MPOL_PREFERRED предпочитает узел, но допускает альтернативы.

Структура bitmask определяет наборы узлов через битовые маски. Функции numa_alloc_onnode и numa_alloc_interleaved из библиотеки libnuma упрощают выделение с явным указанием размещения. Эти интерфейсы критичны для приложений, чувствительных к латентности, таких как базы данных реального времени.

Файловая система /proc/PID/numa_maps показывает фактическое распределение памяти процесса по узлам. Каждая виртуальная область отображается с указанием количества страниц на каждом узле. Метрики N0, N1 обозначают страницы на узлах 0 и 1 соответственно. Статистика помогает диагностировать проблемы размещения.

Подводные камни реального мира

База данных Redis использует fork для создания снимков. Дочерний процесс получает разделяемое адресное пространство через Copy-on-Write. Если родительский процесс продолжает модифицировать данные, происходят COW-копирования страниц. В NUMA-системе новые страницы могут выделяться на узле, где выполняется родитель в момент записи, не обязательно там, где находились оригинальные данные. Результат: память дочернего процесса фрагментируется между узлами, снижая производительность сохранения.

Oracle Database интенсивно использует shared memory для SGA. В системах с включённым автоматическим балансированием ядро пытается мигрировать часто используемые страницы ближе к процессам. Размер SGA достигает десятков гигабайт, и миграция становится дорогостоящей. Процессы Oracle много времени проводят в sleep_on_page, ожидая завершения копирования страниц. Накладные расходы перевешивают потенциальную выгоду. Рекомендация: отключать numa_balancing через /proc/sys/kernel/numa_balancing = 0.

Виртуализация добавляет уровень сложности. Гипервизор управляет собственным NUMA-размещением виртуальных машин. Гостевая ОС видит псевдо-NUMA топологию, созданную гипервизором. Несоответствие между физической и виртуальной топологией порождает неоптимальное размещение. VMware ESXi и KVM предоставляют механизмы NUMA-affinity для VM, позволяющие привязать виртуальную машину к конкретному физическому узлу.

Дисбаланс заполнения DIMM создаёт асимметричные узлы. Один сокет имеет 256 гигабайт памяти, другой только 128 гигабайт. Процессы на втором узле вынуждены чаще обращаться к удалённой памяти. Производительность неравномерна между сокетами. Правильная конфигурация требует симметричного заполнения: одинаковое количество и тип DIMM на каждом процессоре.

Смешивание DPC-регионов усугубляет проблему. Один узел использует конфигурацию 1 DIMM per Channel, другой 2 DPC. Пропускная способность различается вдвое. Доступ к данным становится непредсказуемым по производительности в зависимости от того, на каком узле физически лежит страница. Консистентность требует единообразной конфигурации памяти.

Горизонт развития

Sub-NUMA clustering расширяет концепцию внутрь процессора. Один физический сокет логически разделяется на несколько NUMA-узлов, каждый с подмножеством ядер и контроллеров памяти. Это позволяет оптимизировать локальность внутри чипа для процессоров с десятками ядер. Latency между кластерами внутри сокета меньше, чем между сокетами, но больше, чем внутри кластера.

Persistent memory вводит дополнительный уровень иерархии. Intel Optane DC Persistent Memory подключается к тем же слотам DIMM, что и DRAM, но обладает другими характеристиками: большая ёмкость, меньшая стоимость за гигабайт, выше латентность, ограниченная выносливость записи. В NUMA-системах persistent memory размещается на конкретных узлах, создавая многоуровневую топологию памяти.

Heterogeneous Memory Management позволяет операционной системе управлять разнородной памятью: быструю DRAM как кэш для медленных persistent memory или CXL-attached memory. Страницы автоматически мигрируют между уровнями на основе частоты доступа. Hot pages остаются в быстрой DRAM локального узла. Cold pages выгружаются в медленную память или удалённые узлы.

Compute Express Link создаёт новую парадигму. Устройства, подключённые через CXL, могут выделять когерентную память, доступную процессору. Это расширяет ёмкость памяти без добавления DIMM, но с различными характеристиками латентности. NUMA-топология становится трёхмерной: локальная DRAM, удалённая DRAM других сокетов, CXL-память с промежуточными параметрами.

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