PHP: Детерминированная раскладка файлов

Когда в проекте накапливаются сотни тысяч файлов (кеш изображений, временные данные, миниатюры), простое хранение в одну папку превращается в проблему:

  • Файловая система плохо работает с толстыми каталогами. Даже на 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 + горячем кешировании ОС эффект заметен меньше, но всё равно есть.
1.1 г. Просмотров: 343
Материал помог? Оценка помогает находить устаревшие и спорные решения.

Комментарии

0

Нашли ошибку или хотите дополнить материал?

Комментариев пока нет.Можно первым дополнить материал или сообщить об ошибке.
Telegram