Когда в проекте накапливаются сотни тысяч файлов (кеш изображений, временные данные, миниатюры), простое хранение в одну папку превращается в проблему:
- Файловая система плохо работает с толстыми каталогами. Даже на ext4 с dir_index операции записи/чтения заметно замедляются.
- Массовое удаление или резервное копирование из одного каталога на сотни тысяч файлов тоже тормозит.
- Индексы каталогов раздуваются, inode-кеш ОС страдает.
Решение — раскладывать файлы по подпапкам по хэшу ключа. Алгоритм детерминированный: один и тот же ключ всегда ведёт в одну и ту же подпапку.
function getPathByKey(string $key, int $depth = 2, int $width = 2): string
{
$hash = md5($key); // 32 символа
$parts = [];
for ($i = 0; $i < $depth; $i++)
{
$parts[] = substr($hash, $i * $width, $width);
}
return implode('/', $parts);
}
// Использование
$key = 'example_image_123.png';
$subPath = getPathByKey($key, 3, 2);
echo $subPath;
// → "a9/47/3f"
// Структура хранения:
/cache/
a9/
47/
3f/
example_image_123.png
Как это помогает:
- Директории остаются тонкими: вместо тысяч файлов в одной папке — десятки.
- Операции stat, unlink, readdir выполняются быстрее.
- Резервное копирование и rsync работают предсказуемо.
Какую глубину выбрать:
- 2 уровня по 2 hex-символа → 65 536 корзин. При 1 млн файлов это ~15 файлов в каталоге.
- 3 уровня — для десятков миллионов файлов.
Когда прирост особенно заметен:
- Массовая генерация и удаление кеша.
- Холодный старт (без кеша в памяти ОС).
- Сценарии с большим числом параллельных обращений.
Итог: если проект хранит много файлов, раскладка по подпапкам через хэш — простой и надёжный способ ускорить работу и сделать файловую систему предсказуемой.
Тестируем запись и чтение 20 000 файлов по 64кб:
declare(strict_types=1);
function getPathByKey(string $key, int $depth = 2, int $width = 2): string {
$h = md5($key);
$parts = [];
for ($i = 0; $i < $depth; $i++) $parts[] = substr($h, $i * $width, $width);
return implode('/', $parts);
}
function writeMany(string $base, array $keys, bool $sharded, int $bytes): float {
$buf = random_bytes($bytes);
$start = microtime(true);
foreach ($keys as $k) {
$rel = $sharded ? getPathByKey($k, 2, 2) . '/' . $k : $k;
$path = $base . '/' . $rel;
$dir = dirname($path);
if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
throw new RuntimeException('mkdir failed: ' . $dir);
}
file_put_contents($path, $buf, LOCK_EX);
}
return microtime(true) - $start;
}
function readMany(string $base, array $keys, bool $sharded): float {
$start = microtime(true);
$total = 0;
foreach ($keys as $k) {
$rel = $sharded ? getPathByKey($k, 2, 2) . '/' . $k : $k;
$path = $base . '/' . $rel;
$total += strlen(@file_get_contents($path)) ?: 0;
}
return microtime(true) - $start;
}
function unlinkMany(string $base, array $keys, bool $sharded): float {
$start = microtime(true);
foreach ($keys as $k) {
$rel = $sharded ? getPathByKey($k, 2, 2) . '/' . $k : $k;
$path = $base . '/' . $rel;
@unlink($path);
}
return microtime(true) - $start;
}
$baseFlat = __DIR__ . '/flat';
$baseShard = __DIR__ . '/shard';
@mkdir($baseFlat, 0775, true);
@mkdir($baseShard, 0775, true);
$N = 200000; // 200k файлов
$BYTES = 65536; // 64 КБ
$keys = array_map(fn($i) => 'img_' . $i . '.bin', range(1, $N));
$twFlat = writeMany($baseFlat, $keys, false, $BYTES);
$twShard = writeMany($baseShard, $keys, true, $BYTES);
$trFlat = readMany($baseFlat, $keys, false);
$trShard = readMany($baseShard, $keys, true);
$tuFlat = unlinkMany($baseFlat, $keys, false);
$tuShard = unlinkMany($baseShard, $keys, true);
echo "write flat: {$twFlat}";
echo "write shard: {$twShard}";
echo "read flat: {$trFlat}";
echo "read shard: {$trShard}";
echo "unlink flat: {$tuFlat}";
echo "unlink shard:{$tuShard}";
- На небольших объёмах разница может быть минимальна.
- Но при сотнях тысяч файлов толстый каталог начинает резко проигрывать.
- На SSD + горячем кешировании ОС эффект заметен меньше, но всё равно есть.
Комментарии
0Нашли ошибку или хотите дополнить материал?