Получить доступ

Масштабирование

Что именно упирается, вертикальный и горизонтальный рост, stateless и балансировка, шардинг и consistent hashing, реплики на чтение и лаг, автоскейл и поиск настоящего узкого места.

Вертикально или горизонтально

Разговор о масштабировании начинается не с выбора между вертикальным и горизонтальным ростом, а с вопроса, какой ресурс заканчивается первым: процессор, память, диск, сеть, пул соединений или блокировки в базе. Пока ответа нет, любое решение — угадывание, и на собеседовании это видно сразу.

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

Горизонтальное масштабирование — добавлять машины и распределять нагрузку. Оно масштабируется дальше и переживает отказ узла, но покупается ценой распределённости: балансировка, отказ от локального состояния, частичные отказы, согласованность и координация. Всё, что в одной машине было бесплатно, здесь становится отдельной задачей.

На практике их сочетают, и разумный порядок обычно такой: сначала устранить очевидную неэффективность, потом вертикально усилить машину, потом делать сервис stateless и расти вширь, и только затем шардировать данные. Приложения без состояния растут горизонтально почти бесплатно, а состояние — дорого; поэтому основная архитектурная работа состоит в том, чтобы вынести состояние из приложения.

Полезно помнить, что горизонтальный рост не бесплатен даже для stateless-сервиса. Число соединений к базе умножается на число инстансов, и в какой-то момент добавление машин начинает ухудшать ситуацию: база тратит ресурсы на обслуживание соединений, а не на запросы. Именно поэтому между приложением и базой ставят пулер соединений, а не просто увеличивают количество инстансов.

Вертикальное масштабирование — одна более мощная машина (больше CPU/RAM); горизонтальное — балансировщик распределяет нагрузку на пул одинаковых инстансов.
Вертикально — расти вверх (мощнее машина), горизонтально — вширь (больше машин за балансировщиком).

Stateless и балансировка

Stateless-сервис не хранит между запросами ничего, что нельзя восстановить: сессии, счётчики, загруженные файлы, промежуточный прогресс выносятся в базу, распределённый кэш, объектное хранилище или брокер. Смысл не в чистоте идеи, а в свойстве: любой инстанс может обслужить любой запрос, любой инстанс можно погасить и добавить без потерь. Именно это делает возможными выкатку, автоскейл и переживание отказа узла.

Привязка клиента к инстансу (sticky sessions по куке или по адресу) позволяет жить с состоянием в памяти, но это компромисс, а не решение: нагрузка распределяется неравномерно, при выкатке или падении инстанса сессия теряется, автоскейл работает хуже. Как оптимизация локального кэша привязка полезна, как фундамент архитектуры — нет.

Балансировщики работают на двух уровнях. На четвёртом (TCP) они дёшевы, быстры и ничего не знают о содержимом запроса. На седьмом (HTTP) они понимают пути и заголовки, умеют маршрутизировать по версии, взвешивать трафик для канареечной выкатки, ограничивать частоту запросов и повторять неудачные — платой становятся задержка и стоимость. Алгоритм выбирают по характеру нагрузки: round-robin для одинаковых инстансов и коротких запросов, least-connections при разнородной длительности, хеш по ключу — когда важна локальность кэша.

Здоровье инстансов проверяют двумя разными пробами, и путать их — частая ошибка. Проба живости отвечает на вопрос, нужно ли перезапустить процесс; проба готовности — можно ли слать на него трафик прямо сейчас. Инстанс, прогревающий пулы и кэши, жив, но не готов, и балансировщик не должен на него нагружать. Отдельно обязателен корректный останов: инстанс сначала перестаёт проходить пробу готовности, дожидается завершения текущих запросов и только потом умирает — иначе каждая выкатка теряет часть трафика.

Сам балансировщик не должен становиться единой точкой отказа: его резервируют несколькими узлами и распределяют трафик через DNS или anycast. И у него есть свои пределы — число одновременных соединений и пропускная способность, — про которые вспоминают редко, обычно уже во время инцидента.

Шардинг и consistent hashing

Когда упирается уже не приложение, а база на запись, данные разрезают на шарды. Схем разбиения три. По диапазону ключа — удобны диапазонные запросы, но легко получить горячий шард, если ключ монотонный: все свежие записи идут в один диапазон. По хешу — распределение равномерное, но диапазонные запросы разъезжаются по всем шардам. Через справочник соответствия — гибко и позволяет переносить отдельных клиентов, но сам справочник нужно хранить, кэшировать и защищать от превращения в узкое место.

Ключ шардирования — главное решение, и менять его потом очень дорого. Хороший ключ одновременно даёт равномерное распределение и собирает вместе данные, которые читаются вместе. Если типичный запрос вынужден обходить все шарды и собирать результат, задержка определяется самым медленным шардом, а с ростом их числа растёт и вероятность, что какой-то из них тормозит.

Наивное распределение по остатку от деления хеша на число узлов плохо переносит изменение этого числа: при добавлении одного узла переезжает почти всё. Consistent hashing размещает узлы и ключи на кольце, и при добавлении или удалении узла переезжает только доля ключей, обратно пропорциональная числу узлов. Чтобы распределение было ровным, каждый физический узел представляют десятками виртуальных точек на кольце — иначе разброс нагрузки между узлами получается заметным.

Цену шардинга нужно называть честно: исчезают межшардовые джойны и транзакции, глобальная уникальность и автоинкремент требуют отдельного решения (идентификаторы вроде UUID или snowflake), а перешардирование превращается в операционный проект с двойной записью и переносом данных под нагрузкой. Поэтому шардинг — последнее средство, к которому приходят после кэша, реплик на чтение, архивации холодных данных и вертикального роста базы.

Отдельный сюжет — перекос. Даже при равномерном хешировании несколько крупных клиентов могут давать основную часть нагрузки и класть её на один шард. Лечится это составным ключом (клиент плюс что-то более гранулярное), выделением тяжёлых клиентов на отдельные шарды или подшардированием их данных. Симптом простой: утилизация одного шарда сильно выше остальных, и добавление шардов ничего не меняет.

Реплики на чтение и репликационный лаг

Нагрузку, где чтений на порядок больше записей, масштабируют репликами: запись идёт на лидера, чтение расходится по репликам. Это простой и дешёвый шаг, который часто откладывает шардинг на годы. Но реплики не масштабируют запись — каждая реплика применяет тот же поток изменений, что и лидер, поэтому в системе с преобладанием записи они только добавляют работы. Предложить реплики в ответ на проблему с записью — заметная ошибка на собеседовании.

Репликационный лаг — не аномалия, а нормальный режим асинхронной репликации: лидер подтверждает запись, не дожидаясь применения на репликах. Лаг растёт при массовых обновлениях, долгих транзакциях и перестроении индексов, и измерять его нужно во времени, а не в байтах отставания — только время говорит, насколько устарели данные, которые видит пользователь.

Следствие лага — пользователь может не увидеть собственную запись сразу после сохранения. Гарантию чтения своих записей строят несколькими способами: отправлять чтения этого пользователя на лидера в течение нескольких секунд после его записи, передавать позицию журнала вместе с запросом и ждать, пока реплика её догонит, или закладывать оптимистичное обновление в интерфейсе. Выбор зависит от того, насколько дорого лишний трафик на лидера.

Синхронная или полусинхронная репликация убирает лаг для подтверждённых записей, но перекладывает цену на задержку записи и на доступность: если реплика недоступна, запись останавливается. Это ровно тот компромисс между задержкой и согласованностью, который описывает PACELC, и его стоит называть вслух.

Практическая деталь, о которой часто забывают: маршрутизация чтений на реплики должна быть осознанной, а не автоматической для всего подряд. Отчёты, аналитика и экспорт — идеальные кандидаты; проверка баланса перед списанием — нет. Разделяйте чтения по требованию к свежести, а не по удобству кода.

Автоскейл и его ловушки

Автоскейл добавляет и снимает инстансы по сигналу нагрузки. Выбор сигнала важнее самого механизма: загрузка процессора годится для вычислительных сервисов, а для сервисов, ждущих ввод-вывод, она вводит в заблуждение — там честнее скейлиться по числу запросов, по задержке или по глубине очереди. Для потребителей брокера правильный сигнал почти всегда один — отставание в разделах.

Без гистерезиса и паузы после изменения скейлер начинает дёргать количество инстансов туда-сюда на каждом всплеске, а каждый цикл стоит запусков и прогревов. Порог на увеличение и порог на уменьшение делают разными, а уменьшение — более медленным и осторожным: ошибиться в сторону лишнего инстанса дешевле, чем снять нужный.

Холодный старт — главная причина, по которой автоскейл не спасает от резкого всплеска. Свежий инстанс должен прогреть пулы соединений, кэши и код (для JVM — дождаться компиляции горячих участков), и до этого момента отвечает медленнее, а иногда и хуже, чем никак. Проба готовности обязана это отражать, а под предсказуемые пики лучше работает расписание или прогноз, а не реакция по факту.

Самая опасная ловушка в том, что автоскейл верхнего слоя способен добить нижний. Больше инстансов — больше соединений к базе, больше запросов к внешнему API с квотой, больше конкуренции за общий ресурс. Поэтому у автоскейла обязателен потолок, а у нижних слоёв — своя защита: ограничение пула на инстанс, лимиты частоты запросов, очередь. Автоскейл масштабирует только то, что действительно масштабируется горизонтально.

Где настоящее узкое место

Самая частая ошибка масштабирования — увеличивать то, что не является ограничением. Добавление инстансов приложения не поможет, если каждая операция проходит через один глобальный счётчик, общую блокировку или единственного лидера базы: последовательная часть работы остаётся последовательной, сколько машин ни поставьте.

Это формулируется и количественно. Если часть работы не распараллеливается, она задаёт потолок ускорения; а если узлы вдобавок координируются между собой, то после определённого числа узлов производительность начинает падать — стоимость согласования растёт быстрее выигрыша. Практический вывод один: сначала сокращают последовательную часть и координацию, потом добавляют мощность.

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

Стоит помнить и о том, что деградация под нагрузкой нелинейна. По мере приближения утилизации к ста процентам время ожидания в очереди растёт стремительно, поэтому системы держат с запасом, а не на пределе: сервис, работающий на девяноста процентах, отвечает на небольшой всплеск непропорционально большой задержкой.

И самый недооценённый вариант ответа: часто масштабировать не нужно вовсе. Убрать запрос в цикле, добавить недостающий индекс, включить пакетную обработку, закэшировать тяжёлый расчёт — такие изменения дают выигрыш в разы или на порядки и не добавляют ни одной новой машины и ни одной новой проблемы.

Как отвечать на собеседовании

Не начинайте с решения. Начните с вопроса, что именно упирается и в каких числах: сколько запросов в секунду, каково соотношение чтения и записи, где проведено время запроса. Ответ «добавим инстансов» без этого разбора звучит как заученный, а разбор сразу выделяет опытного инженера.

Держите в голове порядок шагов и проговаривайте его: устранить очевидную неэффективность, вырасти вертикально, сделать сервис stateless и вырасти горизонтально, добавить реплики на чтение, и только потом шардировать. Такой порядок показывает, что вы понимаете стоимость каждого шага, а не просто знаете слова.

Каждый выбранный шаг называйте вместе с его ценой: реплики дают лаг и не помогают записи, шардинг убирает джойны и транзакции и требует выбора ключа, автоскейл упирается в холодный старт и способен добить базу, привязка сессий ломает выкатку. Именно перечисление цены отличает ответ уровня senior.

Заканчивайте проверкой: по каким метрикам будет видно, что стало лучше. Задержка на девяносто девятом перцентиле, насыщение пулов, лаг реплик, распределение нагрузки по шардам. Решение без способа убедиться в его пользе — это гипотеза, а не инженерия.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем