MAATRIX / Блог / Миллион мелких файлов: почему это убивает диск

Миллион мелких файлов: почему это убивает диск

MAATRIX

Директория весит 40 гигабайт, а сервер еле дышит: ls думает пять секунд, бэкап идёт всю ночь, du перебирает диск как будто там не сорок гигабайт, а сорок терабайт. Знакомая картина, если в этой директории лежит не сто файлов, а полтора миллиона. Проблема не в объёме данных — с ним диск справился бы за минуты. Проблема в количестве файлов, и дальше разберём, почему это два разных узких места.

Куда девается объём: миллион файлов — это не миллион байт, это миллион операций

Интуиция подсказывает, что скорость работы с диском зависит от объёма данных: чем больше гигабайт, тем дольше. Это верно для передачи данных, но не для работы с файлами как с сущностями. У каждого файла в файловой системе есть метаданные — запись в каталоге (имя → номер inode), сам inode (права, владелец, время изменения, указатели на блоки данных), и если директория большая — ещё и структура индекса самой директории. Все эти записи нужно прочитать или записать отдельно от содержимого файла.

Создание файла размером в 100 байт и файла размером в 100 мегабайт с точки зрения метаданных — почти одна и та же операция: выделить inode, записать запись в каталог, обновить индекс директории, при необходимости обновить битовые карты свободных блоков. Разница в стоимости самих данных (100 байт против 100 мегабайт) тонет на фоне фиксированных накладных расходов метаданных. Отсюда следствие: миллион файлов по 1 килобайту создают в сумме те же метаданные-операции, что и миллион файлов по 100 мегабайт — при том что суммарный объём отличается в 100 000 раз. Это и есть причина, почему директория на 40 гигабайт из полутора миллионов файлов ведёт себя на диске тяжелее, чем один файл на 400 гигабайт.

Метаданные: своя цена на HDD и своя — на SSD

На механических дисках (HDD) стоимость метаданных-операции — это в первую очередь позиционирование головки. Файл лежит не подряд с записью каталога, не подряд с inode соседнего файла — это разные участки поверхности. Каждая операция чтения/записи метаданных с большой вероятностью требует отдельного seek, а время позиционирования на HDD измеряется миллисекундами — на три порядка дольше, чем передача мегабайта последовательных данных. Именно поэтому на HDD миллион мелких файлов исторически считается едва ли не худшим сценарием нагрузки: диск не передаёт данные, он в основном "прыгает" между метаданными.

На SSD физического позиционирования нет, но есть свой потолок — IOPS (операций ввода-вывода в секунду), который у накопителя ограничен архитектурой контроллера и NAND-памяти. Мы разбирали отдельно, что такое IOPS на самом деле и почему заявленная в характеристиках цифра почти никогда не достижима — в контексте мелких файлов это означает, что даже без seek-задержек каждая метаданные-операция всё равно "ест" единицу IOPS-бюджета. Миллион операций создания файлов — это миллион (а на практике больше, потому что на каждый файл обычно приходится несколько операций записи метаданных) обращений к накопителю. Если ваш тариф VPS или тип диска даёт условно несколько тысяч IOPS, обработка миллиона мелких файлов растягивается на сотни секунд одной лишь метаданными-нагрузкой — независимо от того, что суммарный объём данных передаётся за секунды.

Отдельная грабля — исчерпание inode при формально свободном месте: файловая система резервирует фиксированное число inode при форматировании, и миллион мелких файлов может выесть их раньше, чем закончится место на диске. Мы разбирали это отдельно в статье про нехватку inode при свободном месте на диске — стоит свериться, если у вас именно такая директория с массой файлов.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Листинг директории: почему `ls` начинает думать

Простая команда ls в директории с несколькими файлами выполняется мгновенно. В директории с миллионом файлов она может занимать заметное время — и дело не только в выводе на экран (это легко проверить через ls -f | wc -l, которая не сортирует и не запрашивает лишние атрибуты, но всё равно требует прочитать список).

Причина в устройстве директории как структуры данных. У классических файловых систем директория — это, по сути, список пар "имя → inode". Наивная реализация (линейный список) требует последовательного просмотра при поиске конкретного имени: O(n) операций на файл в худшем случае, O(n²) на построение полного листинга с сортировкой имён. Современные файловые системы стараются уйти от этого — ext4 использует HTree-индекс (хешированное B-дерево) для директорий, XFS — B+-деревья, что даёт логарифмический, а не линейный поиск. Но даже с индексом операции с очень большими плоскими директориями (сотни тысяч — миллионы записей в одной директории без вложенности) заметно медленнее, чем те же операции в директории с разумным числом файлов: индекс тоже нужно читать с диска, он тоже растёт в размере, а некоторые операции (сортировка вывода ls -l, применение stat к каждому файлу для отображения размера и прав) всё равно требуют отдельного обращения к inode каждого файла.

Практическая проверка — сравнить время выполнения:

# Быстрый подсчёт без сортировки и метаданных
time ls -f /path/to/huge-dir | wc -l

# Медленный вариант: -l требует stat() на каждый файл
time ls -la /path/to/huge-dir | wc -l

# find тоже упирается в то же самое дерево метаданных
time find /path/to/huge-dir -maxdepth 1 -type f | wc -l

Разница между первой и второй командой на директории с сотнями тысяч файлов может быть в разы — именно из-за количества обращений к метаданным, а не из-за объёма выводимого текста. Если ls -la в такой директории занимает секунды или десятки секунд — это симптом, а не каприз конкретной команды: то же самое замедление почувствует любая программа, которая перебирает файлы в этой директории (индексатор, антивирус, резервное копирование, веб-сервер, отдающий статику).

Бэкап миллиона файлов: арифметика, которая не сходится с интуицией

Резервное копирование — сценарий, где проблема мелких файлов проявляется больнее всего, потому что бэкап обычно оценивают "в гигабайтах", а платит он в основном операциями. Скопировать 40 гигабайт одним файлом (например, образ диска или один большой архив) современный канал и диск делают быстро — это почти чистая последовательная передача данных. Скопировать те же 40 гигабайт как полтора миллиона мелких файлов — совсем другая задача: на каждый файл нужно как минимум открыть его на источнике, прочитать метаданные, создать запись на приёмнике, записать метаданные там, закрыть файл. Даже если сами данные копируются мгновенно (файл в один килобайт), накладные расходы на операцию никуда не деваются.

Отсюда типичная картина: rsync или tar, копирующие условно 40 гигабайт одним архивом, укладываются в разумное время, а rsync -a по директории с миллионом мелких файлов на том же канале и том же диске растягивается на часы — при том же объёме данных. Это не аномалия конкретного инструмента, а прямое следствие того, что стоимость операции метаданных не зависит от размера файла, а количество операций линейно растёт с числом файлов.

Дополнительно бьёт по времени бэкапа то, что многие системы резервного копирования (в том числе снятие снапшота с последующим построчным обходом) требуют полного обхода дерева файлов для построения списка перед началом самого копирования — на директории с миллионом записей уже сам этот обход (аналог find) занимает заметное время, прежде чем передана хоть одна связка байт.

# Наглядно: время на построение списка файлов может быть сопоставимо
# со временем самого копирования при большом числе мелких файлов
time find /data/many-small-files -type f | wc -l

# Копирование как есть — накладные расходы на каждый файл
time rsync -a /data/many-small-files/ /backup/many-small-files/

# Тот же объём, но одним потоком через архивацию — обычно заметно быстрее
tar -C /data -cf - many-small-files | pv | tar -C /backup -xf -

Решение: архивирование редко изменяемых данных

Если файлы после создания почти не меняются (логи старше недели, обработанные изображения, готовые отчёты, артефакты сборки) — не держите их отдельными файлами. Один архив вместо тысяч файлов превращает миллион метаданные-операций в одну: для файловой системы это уже не миллион записей в каталоге, а один inode с непрерывным (или почти непрерывным) диапазоном блоков.

# Архивация каталога с множеством мелких, редко читаемых файлов
tar -cf archive-2026-08.tar /data/logs/2026-08/
# при необходимости сжатие — но сжатие добавляет CPU-нагрузку,
# для уже сжимаемых данных (jpg, уже сжатые логи) выигрыш небольшой
tar -czf archive-2026-08.tar.gz /data/logs/2026-08/

# После проверки целостности — исходные файлы можно удалить
tar -tf archive-2026-08.tar | wc -l   # сверить количество файлов
rm -rf /data/logs/2026-08/

Важная оговорка: это решение работает для данных, которые вы читаете редко и целиком (или почти целиком) — восстановление бэкапа, разбор инцидента, ретроспективный анализ логов. Если приложению нужен произвольный доступ к отдельному файлу внутри архива на постоянной основе — распаковка каждый раз обесценивает выигрыш, и тут архивация не подходит, нужна другая структура хранения (см. следующий раздел). Для баз данных, состоящих из множества мелких файлов сегментов (некоторые NoSQL-хранилища, поисковые индексы), архивирование "на горячую" вообще может привести к неконсистентному снапшоту — такие данные бэкапить нужно штатными инструментами самой СУБД, а не наивным tar по живой директории.

Иерархия директорий и объектное хранилище: когда плоская структура — ошибка архитектуры

Если файлы нужно продолжать хранить по отдельности (кэш превью, сгенерированные аватары, объекты пользовательской загрузки), решение — не архив, а иерархия. Вместо одной директории на миллион файлов — дерево поддиректорий, где на каждом уровне лежит разумное число записей (условно сотни-тысячи, а не сотни тысяч).

Классический приём — шардирование по хешу имени файла:

# Вместо /data/uploads/<uuid>.jpg (миллион файлов в одной директории)
# раскладываем по первым символам хеша имени:
name="a1b2c3d4-file.jpg"
hash=$(echo -n "$name" | md5sum | cut -c1-4)
dir1=${hash:0:2}
dir2=${hash:2:2}
mkdir -p "/data/uploads/$dir1/$dir2"
mv "$name" "/data/uploads/$dir1/$dir2/$name"
# итог: /data/uploads/a1/b2/a1b2c3d4-file.jpg

При двух уровнях по два хекс-символа получается 256 × 256 = 65 536 конечных директорий — миллион файлов распределяется по ним в среднем по 15 штук на директорию, и каждая операция листинга или поиска работает с крошечной директорией, а не с миллионной. Именно так поступают системы, которые сами генерируют массу мелких файлов: Git раскладывает объекты по первым двум символам SHA (.git/objects/a1/b2c3...), большинство CDN-кэшей и систем превью используют тот же принцип. Это не изобретение конкретного приложения, а стандартный обходной манёвр вокруг деградации плоских директорий.

Если же природа нагрузки — именно масса мелких объектов с доступом по ключу (а не по иерархическому пути), стоит рассмотреть объектное хранилище вместо классической файловой системы. Системы объектного хранения (S3-совместимые: MinIO, Garage, SeaweedFS) изначально проектируются под большое число небольших объектов — у них другая внутренняя индексация (плоское пространство ключей с хеш- или B-дерево-подобным индексом самого хранилища, а не иерархия inode классической ФС), и метаданные объекта хранятся отдельно от больших файлов данных, куда объекты упаковываются пачками. На практике объектное хранилище выигрывает там, где классическая ФС с миллионами мелких файлов начинает деградировать по листингу и метаданным — но требует переписать логику доступа к файлам под API хранилища (PUT/GET по ключу), а не под прямые пути в файловой системе, и это может быть немаленькой архитектурной правкой для существующего приложения. Выбор конкретной файловой системы тоже имеет значение — разбор ext4 против XFS может подсказать, какая из них лучше держит именно ваш профиль нагрузки по мелким файлам, если полный переход на объектное хранилище пока не по плечу.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро понять, что у меня именно эта проблема?

Сравните df -h (место) и df -i (inode) на разделе, и посчитайте число файлов в подозрительной директории: find /path -type f | wc -l. Если файлов сотни тысяч — миллионы, а суммарный объём (du -sh /path) при этом скромный, — это она.

Можно ли просто увеличить IOPS-лимит на VPS и не переделывать структуру хранения?

Частично поможет, но не решает архитектурную проблему: рост числа файлов линейно увеличивает число операций метаданных, и рано или поздно упрётесь в новый потолок — только на большем масштабе. Изменение структуры (архив, иерархия, объектное хранилище) снимает проблему на уровне алгоритмической сложности, а не отодвигает её.

У меня NVMe с высоким IOPS — актуальна ли проблема листинга директорий?

Да, частично. Ограничение по IOPS накопителя снимается, но деградация некоторых файловых систем на очень больших плоских директориях (рост индекса, накладные расходы на его обход) остаётся отдельным фактором — она зависит от структуры файловой системы, а не только от скорости накопителя.

Что делать, если файлы уже лежат плоско и переносить их некуда?

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

Сжимать архив (tar.gz) или оставить без сжатия (tar)?

Зависит от типа данных: для уже сжатых форматов (jpg, mp4, gz-логи) сжатие почти не экономит место, но добавляет нагрузку на CPU при создании и распаковке архива. Для текстовых логов и других легко сжимаемых данных сжатие обычно оправдано — но экономию места стоит проверить на реальных данных, а не считать заранее.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →