XFS имеет репутацию файловой системы для серьёзных нагрузок. Разработанная в начале 1990-х годов в Silicon Graphics для IRIX, она создавалась под требования высокопроизводительных вычислений и больших файлов, когда слова "параллелизм" и "масштабируемость" ещё не стали обязательным маркетинговым набором. Сегодня XFS живёт на серверах с NVMe-дисками, и выжать из этого сочетания максимум, задача, требующая понимания нескольких ключевых архитектурных механизмов. Причём понимания не поверхностного, а того уровня, который позволяет принимать осмысленные решения при форматировании и монтировании.

По умолчанию XFS ведёт себя разумно на большинстве конфигураций. Но NVMe, с его субмиллисекундными задержками и возможностью параллельной обработки очередей команд, способен обнажить ограничения, которые на HDD были незаметны. Именно здесь начинается разговор про allocation groups и log stripe units.

Allocation Groups превращают XFS в параллельную машину

Ключевая идея архитектуры XFS состоит в следующем: файловая система делится на независимые секции, allocation groups (AG), каждая из которых имеет собственный набор структур метаданных, собственные блоки inode и собственный аллокатор свободного пространства. По сути, каждый AG работает как маленькая файловая система внутри большой, и разные потоки могут работать с разными AG одновременно, не блокируя друг друга.

На HDD это давало прирост за счёт снижения contention на локах. На NVMe с его возможностью параллельной обработки сотен I/O-операций одновременно преимущество становится более существенным: правильно настроенные AG позволяют полностью задействовать параллелизм устройства. Бывает, что администратор, получив disappointing результаты fio на свежеформатированном XFS-разделе, оставляет стандартные 4 AG и списывает проблему на XFS. Тогда как реальная причина, именно эти 4 AG, ставшие узким местом под параллельную нагрузку.

# Создание XFS с явным числом AG под количество CPU-потоков
# Правило: agcount = число аппаратных потоков (logical CPUs)
mkfs.xfs -f -d agcount=16 /dev/nvme0n1

# Просмотр текущей структуры AG
xfs_info /dev/nvme0n1

Рекомендация по числу AG: соотносить её с числом аппаратных потоков процессора. Если сервер имеет 2 сокета по 8 ядер с HyperThreading, это 32 логических CPU, что и является целевым значением для agcount. Слишком маленькое количество AG создаёт лишний contention, слишком большое, увеличивает накладные расходы на управление метаданными.

Важный нюанс при настройке: размер AG не должен быть кратен stripe width. Это приводит к тому, что все AG выравниваются по одним и тем же дисковым позициям, что при RAID-конфигурациях порождает "горячие" точки. XFS предупреждает об этом при форматировании и предлагает скорректированное значение.

Ещё один параметр, заслуживающий внимания: lazy-count. При lazy-count=1 (включено по умолчанию в современных версиях) суперблок не обновляется и не журналируется при каждом изменении счётчиков (свободных inode, блоков). Вместо этого актуальные данные восстанавливаются из других структур при монтировании. Это снимает суперблок как точку сериализации при интенсивных метадата-операциях.

Почему выравнивание журнала по stripe boundary меняет картину на записи

Журнал XFS, это последовательный поток транзакций с изменениями метаданных. На дисках с понятием "stripe" (RAID, NVMe с несколькими внутренними очередями) выравнивание записей журнала по stripe boundary даёт значительный прирост производительности: вместо read-modify-write цикла выполняются полные записи.

Параметр log stripe unit (LSU) задаётся как при форматировании, так и косвенно через logbsize при монтировании:

# Форматирование с явным log stripe unit (пример для RAID или NVMe)
mkfs.xfs -f \
  -d su=64k,sw=4 \
  -l su=64k \
  /dev/nvme0n1

# Монтирование с оптимизированным logbsize
# Для интенсивно модифицируемых FS рекомендуется максимум 256k
mount -o logbsize=256k /dev/nvme0n1 /mnt/data

Параметр logbsize управляет размером каждого буфера журнала в памяти. По умолчанию, max(32k, log_stripe_unit). Увеличение до 256k сокращает число I/O-операций журнала при интенсивной модификации метаданных: меньше fsync-вызовов, меньше прерываний цикла записи. Компромисс: при сбое может быть потеряно больше незафиксированных операций. На серверах с батарейным кэшем контроллера или UPS это приемлемо.

Выравнивание под внутреннюю геометрию NVMe без RAID

Для одиночного NVMe-диска без RAID понятие stripe технически отсутствует, но задавать su и sw при форматировании всё равно имеет смысл. Внутренняя архитектура современных NVMe-дисков с несколькими каналами и банками памяти фактически и является параллельной структурой, и выравнивание по 64K или 128K экстентам снижает количество неоптимальных обращений.

# Для одиночного NVMe: выравнивание под внутреннюю геометрию
mkfs.xfs -f -d su=64k,sw=1 /dev/nvme0n1

# Для RAID10 из 4 NVMe: 2 пары зеркал, stripe width = 2
mkfs.xfs -f -d su=64k,sw=2 /dev/md0

# Проверка результата
xfs_info /dev/nvme0n1 | grep -E "sunit|swidth|agcount"

Если XFS видит корректную геометрию от программного RAID (md), он автоматически подстраивается. При hardware RAID параметры нужно задавать вручную, так как контроллер скрывает физическую структуру от операционной системы.

Какие mount-опции реально сдвигают производительность XFS на NVMe

Несколько mount-опций, которые ощутимо влияют на производительность XFS на NVMe:

# /etc/fstab: полный набор рекомендуемых опций для NVMe
/dev/nvme0n1 /data xfs defaults,noatime,logbsize=256k,allocsize=64m 0 0

Разбор ключевых параметров. noatime убирает обновление времени доступа при каждом чтении: на XFS это менее критично, чем на ext4, так как XFS исторически использовал relatime, но на высоконагруженных системах разница всё равно заметна в счётчиках метаданных. allocsize задаёт размер "окна" при отложенной аллокации: большое значение (64M) позволяет XFS зарезервировать смежные блоки для растущего файла, снижая фрагментацию при параллельных записях.

Размер inode и преаллокация пространства под тяжёлые файловые нагрузки

XFS по умолчанию создаёт inode размером 512 байт. Для нагрузок с большим числом расширенных атрибутов или xattr (контейнерные среды, системы с ACL) имеет смысл увеличить до 2048:

mkfs.xfs -f -i size=2048 /dev/nvme0n1

Для workload с очень крупными файлами (медиа-архивы, резервные копии, научные данные) XFS поддерживает преаллокацию через fallocate. Это резервирует смежное дисковое пространство без записи данных, устраняя фрагментацию при последующем заполнении:

# Преаллокация 10 ГБ для файла перед записью
fallocate -l 10G /data/large_dataset.bin

Как читать сигналы насыщения журнала и фрагментации в XFS

Понять, насколько эффективно работает настроенный XFS, позволяет xfs_perf и встроенная статистика через /proc/fs/xfs/stat:

# Просмотр статистики XFS (alloc, inode, log операции)
cat /proc/fs/xfs/stat

# Онлайн-диагностика allocation group utilization
xfs_db -r /dev/nvme0n1 -c "agf 0" -c "print"

# Проверка фрагментации файловой системы
xfs_db -r /dev/nvme0n1 -c "frag"

Особого внимания заслуживает статистика log секции: высокое число pushbuf и noiclogs указывает на насыщение журнала, что решается увеличением logbsize или выносом журнала на отдельное устройство (внешний лог на отдельном быстром NVMe). Внешний лог создаётся при форматировании через опцию -l logdev=/dev/nvme1n1, и при правильном подборе объёма лог-устройства снимает журнал как возможное узкое место полностью.

XFS и NVMe, сочетание, которое при правильной настройке раскрывает потенциал обоих. XFS создавался для параллелизма в эпоху, когда параллелизм был привилегией дорогих серверов. NVMe принёс этот параллелизм в commodity-хардвер. Результат их взаимодействия, при осознанной конфигурации, трудно превзойти на задачах с интенсивными параллельными записями и большими объёмами данных.

Важно понимать: тюнинг XFS под NVMe, это не однократный набор команд, а итеративный процесс. Правильный подход включает измерение текущей производительности через fio и xfs_perf, изменение одного параметра, повторное измерение. Только так можно отличить реальный эффект настройки от случайного шума в числах. Файловые системы, как и любая сложная система, отвечают на вмешательство по-разному в зависимости от конкретного паттерна нагрузки.