Горизонтальное масштабирование PHP: всё что вы хотели знать, но боялись спросить
"Просто добавь серверов" — говорят менеджеры. "Это не так просто" — говорим мы. Сегодня расскажу почему не так просто, и как всё-таки сделать так, чтобы было просто.
Проблема №1: Состояние сессий
Запускаете второй сервер, и пользователи жалуются: "я только что вошёл, а меня снова просит логин". Потому что сессия хранится в файловой системе первого сервера.
Правильное решение: Redis-сессии:
// app/Config/App.php
public string $sessionDriver = 'CodeIgniter\Session\Handlers\RedisHandler';
public string $sessionSavePath = 'tcp://redis-cluster:6379?auth=password&database=1';
public int $sessionExpiration = 7200;
public bool $sessionMatchIP = false; // Важно! Иначе CDN сломает сессии
С Sentinel для HA:
public string $sessionSavePath = 'tcp://redis-sentinel:26379?auth=password&database=1&sentinel_master=mymaster';
Проблема №2: Загрузка файлов
Пользователь загружает аватар на сервер #1. Следующий запрос идёт на сервер #2 — он ничего не знает об этом файле.
Решение: S3-совместимое хранилище:
<?php
namespace App\Services;
use Aws\S3\S3Client;
class FileStorageService
{
private S3Client $s3;
public function __construct()
{
$config = config('FileStorage');
$this->s3 = new S3Client([
'version' => 'latest',
'region' => $config->region,
'endpoint' => $config->endpoint,
'credentials' => ['key' => $config->key, 'secret' => $config->secret],
'use_path_style_endpoint' => $config->usePathStyle,
]);
}
public function upload(string $localPath, string $remotePath, string $acl = 'private'): string
{
$this->s3->putObject([
'Bucket' => $this->bucket,
'Key' => $remotePath,
'SourceFile' => $localPath,
'ACL' => $acl,
'ContentType' => mime_content_type($localPath),
]);
return $this->getUrl($remotePath);
}
public function getSignedUrl(string $remotePath, int $expiry = 3600): string
{
$cmd = $this->s3->getCommand('GetObject', ['Bucket' => $this->bucket, 'Key' => $remotePath]);
return (string) $this->s3->createPresignedRequest($cmd, "+{$expiry} seconds")->getUri();
}
}
Проблема №3: Cron jobs
10 серверов, у каждого crontab — пользователи получают email 10 раз. Решение: distributed locking:
<?php
namespace App\Libraries;
class DistributedLock
{
private string $lockValue;
public function __construct(
private readonly string $key,
private readonly int $ttl = 300
) {
$this->lockValue = gethostname() . '_' . getmypid() . '_' . uniqid();
}
public function acquire(): bool
{
// SET key value NX EX ttl — атомарная операция
$result = cache()->getRedis()->set(
$this->key,
$this->lockValue,
['NX', 'EX' => $this->ttl]
);
return $result === true;
}
public function release(): bool
{
// Lua для атомарного освобождения только СВОЕГО лока
$script = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return (bool) cache()->getRedis()->eval($script, [$this->key, $this->lockValue], 1);
}
}
Базовый класс для singleton cron-команд:
abstract class SingletonCommand extends BaseCommand
{
protected int $lockTtl = 300;
final public function run(array $params): void
{
$lock = new DistributedLock('cron_lock_' . static::class, $this->lockTtl);
if (!$lock->acquire()) {
$this->write("Another instance is already running. Skipping.");
return;
}
try {
$this->execute($params);
} finally {
$lock->release();
}
}
abstract protected function execute(array $params): void;
}
Чеклист горизонтального масштабирования
Перед тем как добавить второй сервер:
✅ Сессии в Redis/Memcached, не в файловой системе
✅ Файлы в S3/MinIO, не на диск
✅ Кэш в Redis, не в файловой системе
✅ Логи в centralized storage (ELK/Loki)
✅ Cron с distributed locking
✅ Конфиги из environment variables
✅ Нет hardcoded hostname в коде
✅ Нет состояния в оперативной памяти между запросами
✅ Graceful shutdown (SIGTERM handler)
✅ Health checks настроены
Если всё это есть — добавление сервера занимает минуты. Если нет — недели головной боли.
На ithub.uno таких архитектурных обсуждений всегда много — там есть практические нюансы, которых нет в документации.
Масштабируйтесь грамотно. 📊🖥️
Recommended Comments