Код-ревью Senior Кластер, воркеры, масштабирование обновлено

Код-ревью: сервис перед масштабированием в 4 инстанса

Вопрос

Сервис работал на одном инстансе, теперь его запускают в 4 репликах за балансировщиком. Найди всё, что сломается или будет работать неправильно.

const sessions = new Map();
const rateLimits = {};
let orderCounter = 1000;

app.post('/login', async (req, res) => {
  const user = await auth(req.body);
  const sid = crypto.randomUUID();
  sessions.set(sid, user);
  res.cookie('sid', sid).json({ ok: true });
});

app.post('/orders', async (req, res) => {
  const ip = req.ip;
  rateLimits[ip] = (rateLimits[ip] || 0) + 1;
  if (rateLimits[ip] > 100) return res.status(429).end();
  const id = orderCounter++;
  await db.orders.insert({ id, user: sessions.get(req.cookies.sid) });
  res.json({ id });
});

app.post('/avatar', upload.single('file'), (req, res) => {
  fs.renameSync(req.file.path, './uploads/' + req.file.originalname);
  res.json({ url: '/uploads/' + req.file.originalname });
});

setInterval(sendDailyReport, 24 * 60 * 60 * 1000);

Коротко

Главный принцип горизонтального масштабирования: реплики не хранят состояние.

Что должен найти ревьюер

  1. Ошибкастрока 1
    Сессии в памяти процесса: запрос, попавший на другую реплику, не найдёт сессию. Нужен общий стор (Redis) или stateless-токен.
  2. Ошибкастрока 2
    Лимит считается в каждой реплике отдельно: фактический лимит 4 × 100, плюс счётчик никогда не сбрасывается и растёт в памяти бесконечно.
  3. Ошибкастрока 3
    Счётчик id в памяти: четыре реплики выдадут одинаковые номера заказов, после рестарта нумерация начнётся заново. Нужна последовательность БД или UUID.
  4. Ошибкастрока 13
    req.ip за балансировщиком это адрес балансировщика, если не настроен trust proxy: все клиенты попадут в один лимит.
  5. Ошибкастрока 17
    sessions.get может вернуть undefined: заказ создаётся без пользователя вместо ответа 401.
  6. Ошибкастрока 22
    Файл сохраняется на локальный диск реплики: с других реплик он недоступен и пропадёт при пересоздании контейнера. Нужно объектное хранилище (S3).
  7. Безопасностьстрока 22
    originalname от клиента в пути: path traversal и перезапись чужих файлов. Имя генерировать на сервере.
  8. Производительностьстрока 22
    renameSync блокирует event loop в обработчике запроса.
  9. Ошибкастрока 26
    Фоновая задача в каждой реплике: отчёт уйдёт 4 раза. Нужен отдельный воркер/cron с одной копией или распределённая блокировка; к тому же setInterval сбивается при каждом рестарте.

Разбор

Главный принцип горизонтального масштабирования: реплики не хранят состояние. Всё, что должно быть общим, уезжает во внешние системы: сессии и лимиты в Redis, счётчики в БД (sequence, identity) или генерируемые id (UUIDv7), файлы в S3-совместимое хранилище, расписание в отдельный процесс (cron-джоба в Kubernetes, BullMQ repeatable jobs) с гарантией одной копии.

Отдельно стоит сказать про trust proxy: за балансировщиком реальный адрес клиента приходит в X-Forwarded-For, и Express начинает ему доверять только при явной настройке (и только для известных прокси, иначе клиент подделает заголовок).

Хорошая проверка перед масштабированием: «что будет, если запрос попадёт на любую реплику, а любую реплику убьют в любой момент?»

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме