Масштабирование: оси, узкие места и эволюция системы
Разговор о масштабировании в большинстве команд начинается не с измерения, а с интуиции: «нас стало больше, надо распиливать монолит», «давайте добавим Kafka», «пора шардировать базу». Через полгода выясняется, что 90 % задержки давал один запрос без индекса, а распиленная система стала медленнее исходной, потому что вызов внутри процесса заменили сетевым.
Эта глава — не про механику. Механику разбирают вглубь соседние треки: кэши и приёмы масштабирования — глава архитектурных паттернов, репликация и шардирование — глава о базах данных, партиционирование в распределённых системах — соответствующая глава, измерение и профилирование — весь трек performance. Здесь — три вещи, которых обычно не хватает именно прикладному разработчику: карта осей роста, дисциплина поиска узкого места и понимание, в каком состоянии находится ваша система и что для неё сейчас разумно.
Что значит «масштабируемая система»
Масштабируемость — не синоним быстроты. Быстрая система на одном пользователе может рассыпаться на тысяче; медленная, но правильно устроенная — держать миллион. Рабочее определение:
Масштабируемая система — та, что сохраняет приемлемую задержку при росте нагрузки за приемлемые деньги.
В определении три величины, и все три обязаны быть измеримы.
Нагрузка. Не «много пользователей», а конкретный параметр: запросов в секунду, событий в секунду, объём данных, число одновременных соединений, размер самого крупного клиента. У разных систем ведущий параметр разный: у ленты соцсети — отношение чтений к записям и размер фан-аута, у аналитики — объём сканируемых данных, у мессенджера — число открытых соединений.
Задержка. Только в перцентилях. Средняя задержка — самая вредная метрика в отрасли: при средней 40 мс каждый сотый запрос может занимать 3 секунды, и это будет ровно тот запрос, который делает ваш самый крупный клиент, у которого больше всего данных. Работают с p50, p95, p99 и p99.9, и отдельно смотрят максимум. В системе, где страница собирается из 20 внутренних вызовов, p99 каждого вызова превращается в p80 страницы — эффект хвостовых задержек. Как это измерять — глава об измерении; как формулировать цель в виде SLO — глава о SLI и SLO.
Стоимость. Система, которая держит нагрузку при линейном росте счёта, масштабируема. Система, у которой удвоение нагрузки утраивает счёт, — нет, у неё просто ещё не кончились деньги. Стоимость — такое же архитектурное ограничение, как задержка.
Что вы на самом деле покупаете
Про масштабирование часто говорят обобщённо — «оно улучшает всё». Полезнее разделить выгоды и назвать цену каждой, потому что бесплатных среди них нет.
| Выгода | В чём выражается | Чем за неё платят |
|---|---|---|
| Стоимость владения | нагрузка обслуживается железом подходящего размера, а не одной переразмеренной машиной «с запасом на пик» | сложность инфраструктуры и людей, которые её понимают |
| Производительность | задержка держится в границах SLO при росте нагрузки; часть работы выполняется параллельно | накладные расходы на координацию и сеть |
| Распределение нагрузки | отказ одного узла не убивает сервис; пики размазываются по экземплярам и очередям | необходимость stateless, идемпотентности и балансировки |
| Переиспользование | вынесенная функция — аутентификация, поиск, нотификации — обслуживает несколько продуктов | публичный контракт, который придётся эволюционировать совместимо |
Ключевое: это не автоматические следствия «масштабирования вообще». Каждая выгода привязана к конкретной оси и к конкретному изменению кода. Клонирование даёт распределение нагрузки, но не снижает стоимость, если узкое место — база. Декомпозиция даёт переиспользование, но ухудшает задержку, потому что вызов в процессе превращается в сетевой.
Идеал «удвоили железо — удвоили пропускную способность» недостижим: любая координация между экземплярами (общий лок, общая база, консенсус) отъедает часть выигрыша, а на некоторой точке добавление узлов начинает ухудшать результат. Формализует это универсальный закон масштабируемости Нила Гантера (Universal Scalability Law); практический вывод из него один — у каждой системы есть точка, после которой добавлять узлы бессмысленно, и найти её можно только нагрузочным тестом (глава о нагрузочном тестировании).
Вертикальное масштабирование: первый и часто правильный ответ
Scale up — «дайте машину побольше». В профессиональной среде это принято слегка стыдиться, и зря.
Аргументы за. Оно не требует изменений в коде: не нужен stateless, не нужны распределённые блокировки, не нужно думать про согласованность. Оно мгновенно: смена типа инстанса — минуты. Оно предсказуемо по эффекту. И современное железо огромно: инстансы с сотнями vCPU и терабайтами памяти доступны по кредитной карте, а NVMe даёт миллионы IOPS. Подавляющее большинство продуктов в мире целиком помещается в одну такую машину и никогда из неё не вырастет.
Аргументы против, и оба серьёзные.
- Цена растёт нелинейно. До некоторого размера цена почти пропорциональна ресурсам, дальше — премия за топовые конфигурации, и вдвое более мощная машина стоит втрое. В какой-то момент десять средних машин дешевле одной большой при той же суммарной мощности.
- Один отказ = весь сервис. Одна машина — это одна точка отказа, одно окно обслуживания, один перезапуск ядра. Даже если производительности хватает, требования к доступности могут заставить перейти к нескольким экземплярам.
Практический вывод: вертикальное масштабирование — правильный первый шаг и неправильный последний. Оно покупает время, чтобы разобраться в системе, но не решает вопрос доступности. Про выбор железа и типов инстансов — глава про VPS, VDS и bare metal.
Куб масштабирования: три оси
Модель предложили Мартин Аббот и Майкл Фишер в книге «The Art of Scalability». Её ценность в том, что она разделяет три независимых способа расти: систему можно двигать по любой оси отдельно или по нескольким сразу.
Ось X — горизонтальное клонирование. N одинаковых экземпляров одной и той же сборки за балансировщиком нагрузки. Каждый экземпляр умеет обработать любой запрос. Это самый дешёвый и самый первый шаг после вертикального масштабирования. Что требует от системы: stateless. Экземпляр не должен помнить ничего между запросами: сессия — во внешнем хранилище, загруженные файлы — в объектном хранилище, кеш в памяти — только как оптимизация, потеря которой ничего не ломает. Балансировка и её режимы — глава о прокси и балансировке. Чего не решает: нагрузку на базу. Двадцать клонов приложения бьют в одну базу двадцатью пулами соединений — и именно там всё и упирается.
Ось Y — функциональная декомпозиция. Разные части системы отвечают за разные функции: сервис заказов, сервис каталога, сервис платежей. Каждый можно масштабировать, релизить и эксплуатировать отдельно, у каждого свой профиль нагрузки и свой стек. Что требует от системы: границы домена. Разделять надо по швам предметной области, а не по техническим слоям — иначе получится набор сервисов, которые нельзя изменить по отдельности. Швы ищут через ограниченные контексты, а следствия выбора разбирает глава о микросервисах. Чего не решает: размер данных. Если у вас 40 ТБ заказов, вынос заказов в отдельный сервис не делает таблицу меньше.
Ось Z — разделение данных. Тот же самый код, но каждый экземпляр обслуживает свою часть данных, определяемую ключом: клиенты A–I на первом шарде, J–R на втором; или клиент по tenant_id; или регион по геолокации.
Что требует от системы: ключ разделения и слой маршрутизации, знающий, куда идти. Ключ выбирают один раз и меняют потом очень дорого. Механика — репликация и шардирование и партиционирование.
Чего не решает: запросы, которым нужны данные всех шардов сразу. Аналитика поверх шардированной базы — отдельная большая работа.
Предупреждение: распространённая путаница в определениях
По интернету и по старым конспектам гуляют перепутанные определения осей. Чаще всего встречаются два искажения, и оба стоит распознавать:
- «Ось Y — это вертикальное масштабирование». Неверно. Ось Y — функциональная декомпозиция, разделение по функциям. Путаница возникает из-за того, что на плоском рисунке ось Y направлена вверх, и слово «вертикальный» кажется подходящим.
- «Ось X — это разбиение приложения на части (например, MVC)». Неверно ровно наоборот: ось X — это отсутствие разбиения, копирование целого. Разделение приложения на части — это ось Y, а на MVC-слои — вообще не масштабирование, а организация кода.
И главное: вертикальное масштабирование (scale up) — увеличение мощности одной машины — не является ни одной из осей куба. Куб описывает способы размножения, а scale up — способ укрупнения. Их часто ставят в один ряд («вертикальное против горизонтального»), но это ортогональные вещи: можно scale up каждого из двадцати клонов по оси X.
Правильные определения проверяются по первоисточнику — книге Abbott & Fisher и материалам AKF Partners, где куб и был сформулирован.
Читать эту картину надо слева направо: почти всегда есть дешёвый шаг, который ещё не сделан, и его надо сделать раньше дорогого.
Сначала найди узкое место, потом масштабируй
Система не «медленная вообще». В каждый момент времени её ограничивает один ресурс — тот, что насыщен. Всё, что вы делаете с остальными ресурсами, не даёт ничего.
Это формализует закон Амдала: если доля работы p ускоряется в n раз, а доля 1 - p не ускоряется вовсе, общее ускорение равно 1 / ((1 - p) + p / n). Практическое следствие жёсткое: если оптимизируемая часть занимает 20 % времени, то даже сделав её мгновенной, вы получите ускорение всего в 1,25 раза. Работать надо там, где время реально проводится, — а не там, где интереснее.
Второй полезный закон — закон Литтла: L = λ × W, где L — среднее число запросов, находящихся в системе одновременно, λ — скорость их поступления, W — среднее время обработки одного. Формула тривиальна и невероятно полезна на практике: она позволяет прикинуть нужный размер пула соединений или числа воркеров прямо на салфетке. Если приходит 500 запросов в секунду и каждый держит соединение с базой 40 мс, одновременно занято 500 × 0,04 = 20 соединений. Пул в 200 соединений вам не нужен — он только создаст базе проблемы. И обратное: если время обработки вырастет до 400 мс, потребуется 200 соединений, пул исчерпается, и запросы начнут ждать — так медленный запрос превращается в отказ всего сервиса.
Дисциплина поиска — метод USE Брендана Грегга (описание метода): для каждого ресурса проверяем три вещи — Utilization (насколько занят), Saturation (есть ли очередь) и Errors (есть ли ошибки). Ресурсы: CPU, память, диск, сеть, а в прикладной системе ещё и пулы соединений, блокировки в базе, очереди задач, лимиты внешних API.
эндпоинт, перцентиль, с какого времени"] B --> C{"Есть измерения:
метрики, трейсы, профиль?"} C -->|нет| C1["Сначала наблюдаемость.
Без неё дальше идти нельзя"] C1 --> C C -->|да| D["Трейс запроса:
где проводится время"] D --> E{"Где основная доля?"} E -->|"Ожидание БД"| F["USE по БД: план запроса,
блокировки, размер рабочего набора"] E -->|"CPU приложения"| G["Профилировщик:
сериализация, регулярки, крипта"] E -->|"Ожидание в пуле"| H["Закон Литтла:
сравнить размер пула и λ×W"] E -->|"Внешний вызов"| I["Таймауты, ретраи,
кэш ответа, батчинг"] E -->|"Сеть и передача"| J["Размер ответа, сжатие,
N+1 по сети, CDN"] F --> K{"Самое дешёвое
исправление найдено?"} G --> K H --> K I --> K J --> K K -->|да| L["Чиним, измеряем снова:
узкое место переехало"] K -->|нет| M["Только теперь — масштабирование:
выбрать ось X, Y или Z"] L --> B M --> B
Ключевое свойство схемы — цикл. Узкое место всегда переезжает. Починили запрос — упёрлись в CPU сериализации. Починили сериализацию — упёрлись в сеть. Это нормально и означает, что вы движетесь. Ненормально — оптимизировать, не проверив, что именно вы оптимизируете. Порядок работы подробно разобран в главе о рабочем процессе оптимизации.
Узкое место почти всегда — база данных
Приложение легко клонируется, база — нет. Поэтому в подавляющем большинстве прикладных систем упирается именно она. Полезно держать в голове лестницу решений, отсортированную по возрастанию цены и риска. Двигаться по ней надо снизу вверх, не перепрыгивая ступеней.
| Ступень | Что делаем | Цена | Типичный выигрыш | Чем платим |
|---|---|---|---|---|
| 1 | Индекс под реальный запрос | минуты | часто в десятки–сотни раз | замедление записи, место |
| 2 | Переписать запрос: убрать N+1, лишние JOIN, SELECT * |
часы | в разы | время разработчика |
| 3 | Кэш перед тяжёлым чтением | дни | снимает большую часть чтений | инвалидация, устаревшие данные |
| 4 | Реплики на чтение | дни | масштабирует чтения почти линейно | лаг репликации, чтение своих записей |
| 5 | Шардирование по ключу | месяцы | снимает потолок и по записи | сложность, кросс-шардовые запросы |
Первые две ступени в реальных системах закрывают большинство проблем производительности, и почти всегда они ещё не сделаны. Прежде чем обсуждать шардирование, стоит открыть план запроса (индексы и планы) — обычно там всё и написано. Подробный разбор ступеней — в главах производительность базы данных и кэширование; что именно живёт в слое данных вашей системы — в главе Слой данных.
Про ступень 4 стоит сказать отдельно, потому что она чаще всего ломает приложения неожиданным образом: реплика отстаёт. Пользователь сохранил профиль, его перекинуло на чтение с реплики, где записи ещё нет, — и он видит старые данные и думает, что ничего не сохранилось. Лечится это не «увеличением скорости репликации», а явным правилом «читай свои записи» — после записи некоторое время читаем с мастера. Модели согласованности разбирает глава распределённых систем.
Так выглядит путь запроса в системе, где применены все три оси:
ось X: один из 20 клонов participant CACHE as Кэш participant RT as Роутер шардов
ось Z participant S3 as Шард 3, мастер participant R3 as Шард 3, реплика participant PAY as Сервис платежей
ось Y U->>LB: GET /orders/98213 LB->>APP: любой здоровый экземпляр APP->>CACHE: order:98213 CACHE-->>APP: промах APP->>RT: ключ tenant_id=4471 RT-->>APP: шард 3 APP->>R3: SELECT — чтение уходит на реплику R3-->>APP: строка заказа APP->>PAY: GET /payments?order=98213 PAY-->>APP: статус оплаты APP->>CACHE: положить с TTL 60 c APP-->>U: 200 OK Note over APP,S3: запись пошла бы на мастер S3,
и следующие 5 секунд этот клиент
читает тоже с мастера — «читай свои записи»
Что ломается при масштабировании
Переход от одного экземпляра к нескольким ломает предположения, которые в однопроцессной системе были бесплатной истиной.
- Состояние в памяти. In-memory кэш, счётчик, локальный rate limiter, «список активных пользователей» в словаре — всё это при двадцати экземплярах превращается в двадцать разных, расходящихся истин. Правило: локальное состояние допустимо только как оптимизация, потеря которой не меняет поведения.
- Sticky sessions. Привязка пользователя к экземпляру по cookie кажется дешёвым способом сохранить сессию в памяти. Она ломает выкат (перезапуск экземпляра выкидывает всех его пользователей), ломает равномерность нагрузки и незаметно консервирует зависимость от локального состояния. Сессию выносят в Redis или в подписанный токен.
- Идемпотентность. Как только появляются ретраи (а с балансировщиком и таймаутами они появляются сразу), любая операция может выполниться дважды. Списание денег, отправка письма, создание заказа обязаны иметь ключ идемпотентности. Подробно — идемпотентность и гарантии доставки.
- «Горячий» шард. Разделение по ключу равномерно только если ключ равномерен. Шардирование интернет-магазина по
tenant_idработает, пока один клиент не оказывается в сто раз крупнее остальных, — и весь его трафик приходит на один шард. Диагностируется это только по метрикам на шард, а не по средним. - Распределённые транзакции. Разнеся данные по сервисам, вы потеряли
BEGIN ... COMMITповерх них. Согласованность приходится собирать вручную — через сагу с компенсациями, и это существенно дороже, чем кажется на этапе рисования схемы: саги и распределённые транзакции. - Отказы становятся частичными. Раньше система работала или не работала. Теперь она работает наполовину: три сервиса живы, один отвечает медленно, и общая деградация выглядит хуже честного отказа. Отсюда таймауты, автоматы защиты, ограничение нагрузки и осознанная деградация — паттерны устойчивости и деградация.
Эволюция системы: семь состояний
Никто не проектирует систему «на микросервисах» с нуля осмысленно — системы приходят в текущее состояние по истории. Вопрос «стоит ли нам разделяться» имеет разный ответ в зависимости от того, где вы находитесь. Ниже — семь типичных состояний.
Обратите внимание на два перехода. Из «новорождённого монолита» есть путь прямо в «монструозный» — он проходится сам собой, без единого решения, если никто не занимается границами. А из «распределённого монолита» единственный разумный путь ведёт назад, к модульному монолиту.
1. Стартап-MVP
Как выглядит: один процесс, одна база, один разработчик или трое. Цель — понять, нужен ли продукт кому-нибудь. Половина таких систем не доживает до второго квартала, и это нормальный исход.
Настоящая проблема: не нагрузка, а скорость проверки гипотез и стоимость ошибки. Самый дефицитный ресурс — время.
Что делать: самая скучная технология, монолит, управляемая база, минимум инфраструктуры. Всё, что можно не писать, — не писать.
Стоит ли разделяться: нет, категорически. Микросервисы стоят месяцев на инфраструктуру, наблюдаемость и деплой — ровно тот ресурс, которого у стартапа нет. И ключевая выгода разделения — параллельная работа многих команд — на команде из трёх человек попросту не существует. Ту же позицию Мартин Фаулер формулирует как MonolithFirst и Microservice Premium: за разделение платят авансом, а получают потом.
2. Новорождённый монолит
Как выглядит: продукт нашёл пользователей, код растёт быстро, архитектура не выверена. Неизвестно, какие внешние сервисы понадобятся, куда пойдёт продукт и как будет выглядеть модель данных через полгода.
Настоящая проблема: границы предметной области ещё не проявились. Любая линия, проведённая сейчас, скорее всего пройдёт не там.
Что делать: наводить порядок внутри процесса — выделять модули, договариваться, что они вызывают друг друга только через явные интерфейсы, не тащить логику в контроллеры. Заводить CI, тесты и наблюдаемость: они пригодятся при любом сценарии.
Стоит ли разделяться: нет. Разделение фиксирует границы в бетоне, а вы ещё не знаете, где они. Исключение — команда, для которой распределённая разработка является привычной средой, а не новым опытом. Таких команд заметно меньше, чем считающих себя таковыми.
3. Зрелый стабильный монолит
Как выглядит: система давно на рынке, функциональность выверена, проблемы решаются рефакторингом, развитие плавное и в едином концепте, конкуренции между функциональными блоками нет.
Настоящая проблема: её может и не быть. Это одно из немногих здоровых состояний, и «мы всё ещё монолит» — не диагноз.
Что делать: поддерживать модульность, следить за временем сборки и прогона тестов, не давать вырасти циклическим зависимостям.
Стоит ли разделяться: возможно, но только под конкретную цель. Разумные причины: нужна независимая эволюция и релиз отдельных блоков, у блоков резко разный профиль нагрузки, нужно переиспользовать компонент из нескольких каналов, часть системы требует другого технологического стека. Готовность к цене обязательна: усиление команды, время на аккуратное выделение слабосвязанных блоков, инфраструктура и обучение. Разделение без цели — это чистые расходы.
4. Многоуровневый монолит в стиле DDD (модульный монолит)
Как выглядит: функциональность сгруппирована в изолированные или слабосвязанные модули, доменная логика отделена от инфраструктуры, у модулей есть явные контракты, у каждого — своя схема или как минимум свой набор таблиц, к которым не ходят другие.
Настоящая проблема: обычно это уже не проблема архитектуры, а вопрос организации: сколько команд работает над одним репозиторием и мешают ли они друг другу.
Что делать: держать границы модулей проверяемыми автоматически — тестом на зависимости, ArchUnit, линтером импортов. Граница, которую не проверяет CI, размывается за квартал.
Стоит ли разделяться: это лучшая стартовая точка для разделения, и одновременно состояние, в котором можно комфортно остаться навсегда. Модуль с чистым контрактом выносится за сетевую границу почти механически. Пока такой нужды нет — не выносите: вызов в процессе быстрее сетевого на три порядка и не может дать таймаут. Подробно — монолит и модульный монолит.
5. Распределённый монолит
Как выглядит: монолит формально распилили, но сервисы связаны сильнее, чем были модули. Есть явные и неявные зависимости, общая база «на всех», синхронные цепочки вызовов на пять звеньев. Бизнес просит изменение в одном модуле — переделывать приходится три сервиса. Развернуть сервисы можно только вместе и в правильном порядке, а протестировать один без остальных нельзя.
Настоящая проблема: худшее из двух миров. Связанность монолита плюс сетевая ненадёжность, операционная сложность и цена распределённой отладки. Хуже исходного монолита по всем параметрам.
Что делать: остановить дальнейшее дробление — оно ухудшает ситуацию. Разобраться, где проходят настоящие швы, а не те, по которым резали. Практический ход, который многие считают поражением, а он таковым не является: слить обратно сервисы, релизящиеся всегда вместе, в один — и резать заново, уже по границам домена. Первым делом убрать общую базу: пока два сервиса пишут в одну таблицу, это один сервис в двух процессах.
Стоит ли разделяться дальше: нет, пока не собрана пирамидка заново.
6. SOA-решение с сервисной шиной
Как выглядит: корпоративная система, построенная вокруг ESB. Шина занимается маршрутизацией, преобразованием форматов, оркестрацией и содержит значительную часть бизнес-логики. Интеграции с тяжёлыми внешними системами, неоднородные технологии, требования к согласованию данных.
Настоящая проблема: SOA — не предок микросервисов, а другая модель, и это ключевая мысль. Различие принципиальное: в SOA логика преобразования и оркестрации живёт в шине (smart pipes), в микросервисной модели канал обязан быть максимально надёжным и «тупым» — его цель передать сообщение, а вся логика остаётся в сервисах (smart endpoints, dumb pipes). Поэтому «независимые» модули SOA-решения на деле сильно связаны через шину, и заменой её на брокер сообщений дело не обходится.
Что делать: если система решает свою задачу — эксплуатировать её и не трогать. Выбор SOA обычно был сделан правильно: он диктовался масштабом интеграций, разнородностью систем и требованиями к согласованию данных.
Стоит ли переходить: как правило, нет. Разумные основания появляются только вместе: основная часть решения устарела, необходимость интегрироваться с тяжеловесными системами отпала, стоит вопрос избавления от легаси, высокий уровень абстракции и согласования больше не нужен, а скорость независимой разработки отдельных модулей или автономность команд стали критичны. Если совпало не всё — переход обойдётся дороже выгоды.
7. Монструозный легаси-монолит
Как выглядит: монолит, который развивался так долго, что превратился в структуру, не поддающуюся изменению. Спагетти-код, заплатки поверх заплаток, отсутствие тестов, уволившиеся авторы, невозможность оценить последствия правки.
Настоящая проблема: стоимость любого изменения непредсказуема, а часто и просто высока. Бизнес перестаёт получать изменения в разумные сроки.
Что делать: соблазн «снести всё и построить заново» здесь силён и иногда обоснован — это единственное состояние, где полная переработка бывает рациональной. Но переписывание с нуля проваливается чаще, чем удаётся: система содержит годы накопленного знания о крайних случаях, которого нет ни в одной документации. Безопасный путь — удушающее замещение (Strangler Fig): поставить фасад перед легаси, по одному переводить сценарии на новую реализацию, гасить старые куски по мере освобождения. Работа с легаси-кодом — соответствующая глава принципов.
Стоит ли разделяться: да, но постепенно и с фасадом, а не «большим взрывом».
Критерии перехода к разделению — и обратные
Прямые критерии. Каждый из них — про свойство системы или организации, а не про технологию.
- Независимый релизный цикл. Части системы нужно выкатывать с разной частотой, и общий релиз стал узким местом. Это самый сильный критерий.
- Разный профиль нагрузки. Один компонент требует двадцати экземпляров в пик, другой — одного круглосуточно. Держать их вместе означает платить за двадцать копий всего.
- Разный технологический стек по необходимости. Именно по необходимости: обработка изображений на другом рантайме, ML-инференс на GPU-узлах. «Хотим попробовать новый язык» — не критерий.
- Организационная граница и закон Конвея. Система неизбежно повторяет структуру коммуникаций организации (формулировка Конвея). Если над одним модулем работают две команды в разных часовых поясах — вы платите за это координацией. Если границы сервисов совпадают с границами команд — снимаете. Про устройство команд под это — топологии команд.
- Переиспользуемые сервисы. Аутентификация, поиск, нотификации, аудит — вызываются из нескольких продуктов и по разным каналам. Такие вещи выносят даже из вполне здорового монолита.
- Разные требования к доступности. Приём платежей должен работать 99,99 % времени, генерация отчётов может полежать час. Объединённые в одном процессе, они получают худшие требования обоих и худшую цену.
- Требование менять части системы на лету. Круглосуточный доступ, при котором нельзя останавливать всё ради изменения одного блока.
Обратные критерии — когда разделяться не надо, даже если хочется:
- Нет CI/CD, автоматического деплоя и отката. Двадцать сервисов, которые выкатываются вручную, — это двадцатикратный ручной труд. См. главу CI/CD.
- Нет наблюдаемости: сквозной трассировки, централизованных логов, метрик на сервис. Без неё отладка распределённой системы превращается в гадание — наблюдаемость и дежурство.
- Границы домена неизвестны. Резать не по чему.
- Команда одна. Главную выгоду — независимость команд — получать некому.
- Настоящая проблема в другом: медленный запрос, отсутствующий индекс, синхронная отправка письма в обработчике HTTP. Разделение их не чинит, а размазывает.
- Нет практики версионирования контрактов. Сетевая граница — это публичный API, который придётся эволюционировать совместимо: совместимость и эволюция контрактов.
И общий вывод, который стоит помнить отдельно от всех списков: решение диктует бизнес, а не мода. Практически в каждом критерии выше движущая сила — не желание команды разработки, а требование к продукту: скорость вывода изменений, доступность, стоимость, автономность команд. Если разделение не решает задачу бизнеса, это трата денег и времени. Если ни команда, ни бизнес не понимают текущей и стратегической картины продукта — это трата денег и времени тем более. Как оформлять такие решения, чтобы через год было понятно «почему», — архитектурные решения и ADR и глава Архитектурные шаблоны на практике.
Стоимость масштабирования
Облачный счёт — это архитектурное ограничение, просто отложенное на месяц. Схема, которая «держит нагрузку», но стоит вдвое больше выручки, нежизнеспособна, и узнать об этом лучше на этапе проектирования.
Два практических момента.
Считайте стоимость на единицу полезной работы, а не в абсолютных числах: доллары на тысячу запросов, на активного пользователя, на гигабайт обработанных данных. Рост абсолютного счёта при росте бизнеса нормален; рост удельной стоимости — сигнал, что архитектура работает против вас. Отдельно смотрите на статьи, которые не видны в счёте за вычисления: межзонный трафик, исходящий трафик, запросы к объектному хранилищу, логи. В зрелых системах они регулярно оказываются крупнее счёта за серверы — разбор в главе про облачную стоимость.
Автомасштабирование — не бесплатный ответ, а механизм со своими отказами. Три ловушки:
- Масштабирование по CPU для сервиса, ждущего ввода-вывода. Приложение проводит время в ожидании базы, CPU при этом низкий, автоскейлер не реагирует — и очередь растёт при «здоровых» метриках. Для таких сервисов правильный сигнал — длина очереди или время ожидания в ней, а не загрузка процессора.
- Автоскейл, добивающий базу. Нагрузка выросла, автоскейлер поднял тридцать экземпляров, каждый открыл пул на двадцать соединений, база получила шестьсот соединений вместо ста и легла. Дальше все экземпляры получают таймауты, healthcheck начинает валить поды, оркестратор их пересоздаёт — и система входит в петлю, из которой не выходит сама. Защита: пулер соединений перед базой, верхний предел числа экземпляров, ограничение скорости добавления.
- Запаздывание. От решения масштабироваться до готового принимать трафик экземпляра проходят минуты: запуск, прогрев, JIT, наполнение кэшей. Если всплеск длится две минуты, автоскейл к нему не успеет — нужен запас или буферизация через очередь. Планирование ёмкости под это — глава SRE о capacity.
Типичные ошибки
- Масштабировать без измерения. Добавить узлы, потому что «медленно», не зная, где проводится время. Половина мощности уходит в пустоту, а узкое место остаётся на месте.
- Кэш как лекарство от плохого запроса. Кэш поверх запроса, который делает seq scan по десяти миллионам строк, прячет проблему до первого промаха. При холодном старте или инвалидации весь трафик разом приходит на нижележащую базу — и она ложится ровно в тот момент, когда меньше всего хотелось.
- Шардирование по неудачному ключу. Ключ выбирается один раз и меняется потом ценой миграции всех данных. Шардирование по дате даёт «горячий» текущий шард; по автоинкрементному id — тот же эффект; по крупному клиенту — перекос, который вылезет через год.
- Автоскейл, добивающий базу. См. выше: масштабировать надо всю цепочку, а не самое лёгкое её звено.
- «Распилим монолит» без наблюдаемости и без CI/CD. Разделение умножает операционную нагрузку. Если выкат одного сервиса ручной, выкат двадцати — катастрофа. Сначала инструменты, потом разделение.
- Оптимизировать то, что удобно, а не то, что тормозит. Закон Амдала: ускорение части, дающей 5 % времени, даёт максимум 5 %.
- Средняя задержка как метрика. Пока смотрите на среднее, самые крупные клиенты страдают молча.
- Разделение как решение проблемы качества кода. Плохо структурированный код после выноса за сетевую границу становится плохо структурированным распределённым кодом. С таймаутами.
Мини-итог
- Масштабируемость — это сохранение приемлемой задержки при росте нагрузки за приемлемые деньги. Все три величины измеряются, задержка — только в перцентилях.
- Вертикальное масштабирование — правильный первый шаг: оно ничего не требует от кода и покупает время. Его потолок — цена и единственная точка отказа.
- Куб масштабирования: X — клонирование (требует stateless), Y — функциональная декомпозиция (требует границ домена), Z — разделение данных (требует ключа). Вертикальное масштабирование не является осью куба; распространённая путаница «Y = вертикальное» — ошибка.
- Сначала измерить, потом масштабировать. Метод USE, закон Амдала, закон Литтла. Узкое место всегда одно и всегда переезжает после починки.
- База — узкое место по умолчанию. Лестница: индекс → запрос → кэш → реплики → шардирование, снизу вверх без перепрыгиваний.
- При росте ломаются: состояние в памяти, sticky sessions, неидемпотентные операции, равномерность шардов, транзакционность.
- Состояние системы определяет ответ: стартапу микросервисы противопоказаны; модульный монолит — и лучшая точка старта, и достойное место, где можно остаться; распределённый монолит — худшее из двух миров, из него идут назад; SOA — не предок микросервисов, а другая модель.
- Решение о разделении диктует бизнес: релизный цикл, доступность, стоимость, автономность команд. Без CI/CD и наблюдаемости разделение противопоказано.
Источники
- Martin L. Abbott, Michael T. Fisher. «The Art of Scalability» — сайт книги, описание куба у AKF Partners
- Brendan Gregg, The USE Method
- Neil Gunther, Universal Scalability Law
- Martin Fowler, MonolithFirst и Microservice Premium
- Martin Fowler, Strangler Fig Application
- Sam Newman. «Monolith to Microservices» — о книге
- Melvin Conway, How Do Committees Invent?
Что дальше
Система выросла и держит нагрузку. Дальше начинается самая долгая часть её жизни: дежурства и инциденты, наблюдаемость, технический долг, миграции и постепенное вытеснение того, что вы сами написали три года назад.