Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Горизонтальное масштабирование PHP: всё что вы хотели знать, но боялись спросить

(0 reviews)

"Просто добавь серверов" — говорят менеджеры. "Это не так просто" — говорим мы. Сегодня расскажу почему не так просто, и как всё-таки сделать так, чтобы было просто.


Проблема №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 таких архитектурных обсуждений всегда много — там есть практические нюансы, которых нет в документации.

Масштабируйтесь грамотно. 📊🖥️


0 Comments

Recommended Comments

There are no comments to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.