Сетевой поток, который целиком приходит на одно ядро процессора, ограничивает пропускную способность сервера сильнее, чем узкий канал: один процессор занят обработкой прерываний, а остальные стоят рядом без дела. В Linux раскладкой этих событий управляют две силы: ядро с файлами smp_affinity внутри /proc/irq и служба irqbalance, которая пересматривает раскладку по собственному расписанию. Между ними часто возникает тихий конфликт: администратор закрепляет прерывание за ядром, а служба через десять секунд возвращает его туда, откуда он был выведен. Статья разбирает путь прерывания от контроллера до ядра, дает разбор файла /proc/interrupts, файлов affinity_hint и effective_affinity, объясняет политику hintpolicy, приводит расчет нагрузки для типичного потока и ловушки виртуальных машин. После прочтения раскладку прерываний на своей машине можно будет проверить, закрепить и обосновать числами.
Путь прерывания от контроллера до ядра процессора и разбор файла /proc/interrupts
Прежде чем крутить маски, полезно понять механику. Каждый аппаратный источник прерываний описан в ядре структурой irq_desc, а драйвер устройства просит вектор вызовом request_irq и отдает обработчик, который ядро запускает при срабатывании линии. Контроллер, будь то классический IO-APIC или MSI-X вектор PCIe-устройства, предоставляет набор примитивов вроде mask, unmask и eoi, а тип линии задает сценарий обработки: level, edge, fasteoi или percpu для событий, живущих на одном конкретном ядре. Для балансировки важны два поля дескриптора: маска affinity с разрешенными ядрами и effective_affinity с фактическим набором, потому что часть контроллеров умеет доставлять вектор только на один процессор, и записанная широкая маска при программировании контроллера сужается.
Взгляд на текущую картину дает файл /proc/interrupts: слева номер вектора, затем по столбцу на каждое ядро счетчики срабатываний, дальше тип контроллера и имя устройства. Фрагмент четырехъядерной машины с очередями сетевой карты выглядит примерно так:
CPU0 CPU1 CPU2 CPU3
33: 2214673 15 22 4 PCI-MSI 32768-edge enp3s0-TxRx-0
34: 18 1876542 11 7 PCI-MSI 32769-edge enp3s0-TxRx-1
35: 21 13 1954321 9 PCI-MSI 32770-edge enp3s0-TxRx-2
LOC: 402118 398220 401530 399875 Local timer interrupts
RES: 118770 122415 117203 120988 Rescheduling interrupts
Строки LOC, RES, TLB и CAL внизу относятся к служебным событиям процессора: локальный таймер, перекидывание задач между ядрами, сброс буферов трансляции и межпроцессорные вызовы. Устройств они не касаются, зато говорят о состоянии планировщика. Рост одного из векторов карты при нулевых соседних столбцах прямо показывает, где сидит нагрузка, а повторный просмотр через секунду дает скорость: разность счетчиков, деленная на число секунд, и есть интенсивность в прерываниях за секунду. Наблюдать динамику удобнее постоянно обновляемым экраном:
watch -n1 'cat /proc/interrupts'
Ручная привязка прерывания к ядру через файлы smp_affinity и smp_affinity_list
У каждого вектора в каталоге /proc/irq/NN лежат файл smp_affinity с шестнадцатеричной маской и файл smp_affinity_list с перечнем ядер в человеческой форме. Маска читается по битам: значение 2 означает CPU1, значение 8 - CPU3, значение c объединяет CPU2 и CPU3. Запись в файл меняет привязку немедленно и требует прав суперпользователя, а форма списка удобнее: в smp_affinity_list пишут номер ядра, диапазон вроде 0-3 или перечисление через запятую.
Порядок закрепления одного вектора за выбранным ядром выглядит так:
1. найти номер вектора устройства в /proc/interrupts;
2. прочитать подсказку драйвера из файла /proc/irq/33/affinity_hint;
3. записать номер ядра в /proc/irq/33/smp_affinity_list;
4. оградить вектор от службы irqbalance ключом --banirq=33 в ее настройках;
5. убедиться по повторному чтению /proc/interrupts, что счетчик нужного ядра растет.
echo 2 > /proc/irq/33/smp_affinity_list
cat /proc/irq/33/affinity_hint
cat /proc/irq/33/effective_affinity
Файл affinity_hint ядро показывает только для чтения: драйвер через irq_update_affinity_hint сообщает желательный набор, но саму привязку не меняет, решение остается за службой irqbalance или за администратором. Файл effective_affinity отвечает на вопрос, куда вектор доставляется фактически, и незаменим там, где контроллер сузил широкую маску до одного процессора. Для будущих векторов действует умолчание из файла /proc/irq/default_affinity: запись в него маски f на четырехъядерной машине заранее разрешает новые векторы всем ядрам. Перемещаемы не все строки: у служебных прерываний таймера и межпроцессорных вызовов запись в smp_affinity заканчивается ошибкой Invalid argument, и это осознанное поведение ядра, а не сбой прав.
Служба irqbalance, политика hintpolicy и подсказки affinity_hint
Служба irqbalance приходит одноименным пакетом: на Debian-семействе его ставит apt install irqbalance, на RHEL-семействе dnf install irqbalance, а на большинстве готовых серверов она уже установлена и запущена. Раз в десять секунд она пересматривает раскладку: смотрит счетчики нагрузки от векторов, учитывает топологию кэшей и NUMA-узлов, старается держать прерывание рядом с обработчиком и памятью устройства, а при простое части ядер уводит векторы с них ради энергосбережения. Порог включения энергосберегающего режима задает опция --powerthresh, полный запрет на один вектор - --banirq, разовый прогон без фоновой работы - ключ --oneshot. Живут настройки в файле /etc/default/irqbalance на Debian-семействе и в /etc/sysconfig/irqbalance на RHEL-семействе:
IRQBALANCE_OPTS="--hintpolicy=subset"
IRQBALANCE_BANNED_CPULIST="0-3"
Переменная IRQBALANCE_BANNED_CPULIST перечисляет ядра, куда служба не поставит ни один вектор: это главный инструмент там, где часть ядер изолирована. Политика hintpolicy решает судьбу подсказок драйверов. Значение exact приказывает применять подсказку буквально и такие векторы не трогать, значение subset разрешает двигать вектор внутри подсказанного набора, значение ignore заставляет подсказки не читать вовсе. По умолчанию действует ignore: подсказки драйвера служба не читает и раскладывает перемещаемые векторы по своей нагрузочной логике, поэтому на современных картах, где драйвер ставит по подсказке на каждую очередь, начальный порядок задает ядро в момент создания векторов, а дальше его перестраивает уже irqbalance.
systemctl enable --now irqbalance.service
systemctl status irqbalance.service
Практический вывод из цикла службы: ручная запись в smp_affinity переживет не более одного прохода. Через десять секунд irqbalance увидит разбаланс и вернет вектор по своему усмотрению. Поэтому закрепление и балансировку совмещают одним из двух способов: либо вектор заносят в запрет через --banirq и закрепляют вручную, либо отдают его службе целиком и не трогают руками. Устойчивого третьего состояния нет.
Очереди сетевой карты и пример расчета потока прерываний на ядро
Современная карта с поддержкой MSI-X создает по вектору на каждую очередь приема и передачи, и число очередей не обязано совпадать с числом ядер. Посмотреть текущую раскладку и поменять ее умеет ethtool:
ethtool -l enp3s0
ethtool -L enp3s0 combined 8
ethtool -C enp3s0 adaptive-rx on
Команда -L задает восемь объединенных очередей, и ядро при пересоздании векторов растащит их по процессорам по порядку. Зачем это нужно, покажет расчет. Линия десять гигабит на мелких пакетах несет порядка 14,9 миллиона пакетов в секунду; если у карты одна очередь и драйвер без объединения вызывает вектор на каждый пакет, единственное ядро получает около 14,9 миллиона прерываний в секунду и почти ничего другого делать не успевает. С восемью очередями та же нагрузка делится до 1,86 миллиона на ядро, а с адаптивным объединением, когда драйвер собирает пакеты пачками, частота векторов падает еще на порядок ценой микроскопической задержки. Числа всегда свои, но порядок расчета одинаков: пакетов в секунду делят на число очередей и проверяют, помещается ли доля в бюджет ядра.
Оценить дефицит помогает mpstat из пакета sysstat: столбец %irq показывает долю процессорного времени на аппаратных прерываниях, столбец %soft - на мягких. Ядро, у которого %soft подолгу держится выше трети при растущем трафике, сигналит о необходимости раскладки:
mpstat -P ALL 1
Программное распределение приема через RPS и RFS на картах с одной очередью
Когда железо второй очереди не дает, выручает программная раскладка. Механизм RPS позволяет ядру передавать обработку приема другим процессорам: в файл rps_cpus конкретной очереди пишется маска, и мягкое прерывание NET_RX выполняется уже там, куда указывает маска, а не там, где сработал вектор. Битовый счет тот же, что у smp_affinity: значение e означает ядра с первого по третий.
echo e > /sys/class/net/enp3s0/queues/rx-0/rps_cpus
sysctl -w net.core.rps_sock_flow_entries=32768
echo 8192 > /sys/class/net/enp3s0/queues/rx-0/rps_flow_cnt
Вторая и третья команды включают RFS, развитие идеи: ядро ведет таблицу потоков и доставляет пакет на процессор, где живет открывший сокет процесс, что экономит прогрев кэша и лишние блокировки. Глобальный размер таблицы в сокетных записях делят между очередями поровну: при 32768 записях и четырех очередях каждая получает по 8192. Плата за RPS - межпроцессорный вызов и лишний проход по очереди, поэтому на картах с богатыми аппаратными очередями его выключают, а на одноочередных гигабитных чипах он поднимает фактическую пропускную способность в разы. Для исходящего направления существует симметричный механизм XPS с файлами xps_cpus в тех же каталогах очередей.
Изолированные ядра, списки исключений и неперемещаемые служебные прерывания
На серверах реального времени и на машинах, где часть ядер отдана под конкретные задачи, раскладка прерываний превращается в обратную задачу: не распределить по всем, а держать подальше от выбранных. Ядра изолируют параметром ядра isolcpus=2-3 в паре с nohz_full=2-3, а службе irqbalance перечисляют те же номера в IRQBALANCE_BANNED_CPULIST, как в примере конфигурации выше. Без второй части изоляция остается декларацией: первое же громкое прерывание соберет очередь на изолированном ядре, и весь смысл затеи исчезнет.
Служебные строки внизу /proc/interrupts - LOC, RES, CAL, TLB - привязаны к процессорам по определению: таймер и межпроцессорные вызовы существуют на каждом ядре отдельно, и единого вектора, который можно перепривязать, у них нет. Запись в их smp_affinity возвращает ошибку, и это не дефект. Каждое ядро в маске обязано быть в состоянии online: попытка записать номер ядра, выключенного горячим извлечением, даст Invalid argument. Горячая смена процессоров переписывает маски и сама: при выводе ядра из строя его векторы переносятся на оставшиеся, и вернувшийся в строй процессор получит долю обратно только через службу балансировки.
Виртуальные машины, горячая смена ядер и границы разумной ручной настройки
В виртуальной машине раскладка выглядит иначе. Гость видит свои vCPU, но расписанием на физические ядра управляет гипервизор, поэтому точная привязка вектора к vCPU номер три не обещает физического ядра: планировщик унесет vCPU в любой момент. На типичных машинах с паравиртуальными устройствами virtio служба irqbalance внутри гостя чаще суетится без пользы, и ее отключают, доверяя раскладку хосту. Исключение - проброс устройства через SR-IOV или VFIO: вектор физической карты попадает в гостя напрямую и там ведет себя как настоящее аппаратное прерывание, со всеми файлами smp_affinity и правом на ручное закрепление.
Общий свод правил короткий. Сначала измерение: mpstat, /proc/interrupts, скорость прироста счетчиков. Потом одна правка: число очередей, маска одного вектора или политика службы, но не все сразу. Затем новое измерение на том же потоке. Ручное закрепление оправдано в трех случаях: одно ядро упирается в потолок по %soft, ядра изолированы под приоритетную задачу, служба балансировки отсутствует или мешает замерам. Во всех прочих сценариях честнее признать, что irqbalance справляется сам, и потратить время на объединение прерываний: вместо перетасовки масок эта настройка снижает саму частоту векторов, а именно частота, а не только место, чаще всего оказывается настоящим узким местом.