Serverless и два кейса системного дизайна
Этот урок закрывает распределённую серию практикой. Сначала разберём serverless: что это, где он выигрывает и какие лимиты прячутся за маркетингом. Потом достанем шпаргалку нефункциональных требований, способ начать любую задачу системного дизайна с чисел, а не с квадратиков. И прогоним через неё два полных кейса: гарантию доставки email и систему рейтинга под нагрузкой. Оба кейса регулярно встречаются и на собеседованиях, и в проде.
Serverless: сервер есть, заботы нет
FaaS, ядро serverless: ты пишешь функцию, платформа запускает её по событию, масштабирует от нуля до тысяч копий и считает деньги за миллисекунды работы. Сервер никуда не делся, просто он не твой.
Модель исполнения
Функция просыпается от триггера: HTTP-запрос через gateway, сообщение в очереди, событие хранилища (файл загружен в S3), таймер по расписанию. Один вызов обрабатывает одно событие; параллельная нагрузка, это не потоки внутри твоего процесса, а платформа, поднимающая нужное число копий окружения. Из этого следует непривычное: у функции нет «между запросами». Нет фонового воркера, нет прогретого кеша в памяти процесса, который живёт неделями, нет демона, слушающего сокет.
Под капотом каждая копия живёт в microVM Firecracker: изоляция уровня виртуалки по цене, близкой к контейнеру. Платформа не убивает окружение сразу после ответа, а держит его тёплым несколько минут и переиспользует для следующих вызовов.
Cold start и прогрев
Отсюда главная особенность задержек. Первый вызов собирает всё с нуля: запустить microVM, загрузить рантайм, загрузить твой код, выполнить инициализацию модулей. Это cold start, и для Node с тяжёлым бандлом он легко стоит 500 миллисекунд и больше. Второй вызов попадает в уже тёплое окружение и стартует за единицы миллисекунд.
Что с этим делают. Во-первых, худеют: меньше зависимостей, бандл через esbuild, ленивые импорты тяжёлого. Во-вторых, выбирают рантайм с быстрым стартом. В-третьих, платят: provisioned concurrency держит N окружений тёплыми постоянно, превращая cold start из случайности в статью бюджета. Заметь иронию: это уже наполовину обычный сервер, просто управляемый платформой.
Переиспользование окружения даёт и ловушку: глобальные переменные модуля переживают вызов. Это используют сознательно (соединение с базой открывают вне хендлера, чтобы тёплые вызовы его переиспользовали), но это же стреляет: данные одного пользователя, случайно сохранённые в глобале, всплывают в чужом запросе.
Лимиты
У всего есть потолок, и он ближе, чем кажется: максимальное время работы вызова (минуты, не часы), память, размер запроса и ответа, размер бандла. Самый коварный лимит косвенный: соединения с базой. Тысяча параллельных вызовов, это тысяча процессов, каждый со своим коннектом, и Postgres с его сотней соединений по умолчанию ложится не от нагрузки, а от подключений. Лечится внешним пулом (PgBouncer, RDS Proxy) или serverless-драйверами, которые ходят в базу по HTTP.
Где выигрывает и где проигрывает
Выигрывает там, где нагрузка неровная или редкая: спайки (тот самый старт продаж), задачи по расписанию, webhooks, glue-код между сервисами, обработка загруженных файлов. Масштабирование от нуля и оплата за миллисекунды здесь честно экономят деньги и нервы.
Проигрывает на противоположном профиле. Стабильно высокий RPS: постоянно работающая Lambda дороже постоянно работающей VM в разы. Долгоживущие соединения: WebSocket и функция, умирающая через 15 минут, несовместимы по природе. Жёсткий p99: пока есть cold start, хвост задержек не твой. И вечный фоновый риск: vendor lock. Рецепт против последнего ты уже знаешь из урока про стрелки внутрь: хендлер делаем тонким адаптером, ядро не знает слова Lambda, и переезд на сервер сводится к замене одного слоя.
Шпаргалка НФТ: дизайн начинается с чисел
Нефункциональные требования описывают, как система должна работать, а не что она делает. Пропустишь этот шаг, и архитектура рисуется под вымышленную нагрузку. Шпаргалка из десяти пунктов:
- Масштаб и нагрузка: пользователи в день, действия, пики, размеры файлов.
- Соотношение чтений и записей.
- Задержка: целевой p95 на ключевые операции.
- Консистентность и доступность: выбор C/A по операциям, как в уроке про CAP.
- SLA доступности.
- Объём данных.
- Безопасность: аутентификация, авторизация, аудит, персональные данные.
- Надёжность: что можно терять, что нельзя.
- Специфичные требования: идемпотентность, защита от злоупотреблений.
- Наблюдаемость: метрики, логи, трейсы.
Считаем афишу
Прикинем числа для афиши мероприятий с продажей билетов, она уже появлялась в уроке про CAP.
Чтения. 1M DAU, каждый делает в среднем 20 запросов на чтение в день (список мероприятий, карточки, места, цены):
1M пользователей x 20 запросов = 20M запросов в сутки
В сутках 86 400 секунд, для прикидок округляем до 100 000
20 000 000 / 100 000 = 200 RPS на чтение в среднем
Пиковый коэффициент x10: 2 000 RPS на чтение в пике
Удобный трюк: считать в степенях десятки. 2 x 10^7 запросов на 10^5 секунд, это 2 x 10^2, те же 200 RPS, но без калькулятора.
Записи. Покупает 1 из 100 пользователей в день:
1 000 000 / 100 = 10 000 покупок в сутки
10 000 / 100 000 секунд = 0.1 RPS в среднем
Старт продаж популярного концерта: 20 000 попыток за 10 минут
20 000 / 600 = 33 RPS на покупку в пике
Соотношение чтений к записям получилось около 2000 к 1. Это сразу решения: реплики на чтение, кеш, denormalized view. А одна покупка, это не один запрос: создание заказа и брони, колбэк платёжного провайдера, выпуск билетов.
Дальше по шпаргалке: p95 на карточку мероприятия до 250 мс; доступность 99.9%; бронь живёт 10 минут и её можно потерять, оплаченный заказ терять нельзя; повторная обработка платёжного колбэка не должна создавать дубли; защита от массовой скупки.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.