# Искусство компромиссов: системное проектирование высоконагруженных сервисов

Источник: https://www.youtube.com/watch?v=soTfFFIQQ8U
Канал: freeCodeCamp.org
Опубликовано: 13.10.2026

---

Обычный `INSERT` в базу данных — это верная смерть для системы масштаба WhatsApp, тогда как переход на пакетную запись ускоряет пропускную способность сразу в сто раз. На уровне Senior-дизайна дьявол кроется не в умении писать код, а в способности предсказать race conditions при бронировании отеля или выбрать между SQL и NoSQL, основываясь на миллисекундах задержки. Это подробный гид по архитектурному мышлению, где каждый выбор — от WebSockets до CDC — является компромиссом между стоимостью и масштабируемостью.

## 🏗️ Профессиональный подход к System Design: Фреймворк и Оценка

[[JUMP:00:00]]

Проектирование программных систем (System Design) сегодня является критически важным навыком не только для Senior и Staff-инженеров, но и для позиций уровня Mid [01:36]. В условиях, когда ИИ берет на себя написание кода, компании все больше ценят специалистов, способных видеть «общую картину», понимать взаимодействие компонентов и аргументированно обсуждать компромиссы (trade-offs) [01:49]. Процесс оценки кандидата выходит далеко за рамки простого рисования схем: это комплексная проверка технического суждения, умения работать в условиях неопределенности и навыков коммуникации [05:16].

### 📏 Пять измерений оценки кандидата
[[JUMP:03:06]]

Большинство инженеров ошибочно полагают, что их оценивают только по качеству спроектированной схемы. На самом деле, интервьюеры смотрят на пять ключевых аспектов [03:19]:

*   **Коммуникация и коллаборация.** Умение задавать уточняющие вопросы и валидировать предположения [05:22]. Интервьюер хочет понять, насколько легко с вами работать: будете ли вы уточнять требования на старте или потратите время на разработку решения, которое никому не нужно [06:08].
*   **Структурное мышление.** Способность разбить расплывчатую задачу на четкие этапы [06:20]. Сильные кандидаты не мечутся между случайными решениями, а следуют понятному плану [06:33].
*   **Техническое суждение.** Способность объяснить, *почему* выбран конкретный инструмент [06:59]. Выбор SQL-базы или Kafka должен основываться на требованиях системы, а не на личных привычках [07:12].
*   **Практическая глубина.** Различие между теоретиком и практиком проявляется в обсуждении режимов сбоя (failure modes) и масштабирования [07:39]. Опытный инженер видит «узкие места», которые незаметны на уровне теории [07:51].
*   **Тайм-менеджмент.** Умение уложиться в 45–60 минут, не застревая на одном этапе [08:17]. Неспособность управлять временем на интервью часто воспринимается как признак плохой организации работы в реальных проектах [08:54].

### 🔄 Пятишаговый фреймворк проектирования
[[JUMP:09:18]]

Для успешного прохождения интервью (обычно длительностью 60 минут) используется повторяемая структура, помогающая сохранять фокус и демонстрировать ход мыслей [09:32]:

1.  **Уточнение требований (5–10 минут).** Самая частая ошибка — сразу приступать к рисованию [09:59]. Необходимо разделить требования на функциональные (основные сценарии использования) и нефункциональные (масштаб, задержки, доступность) [11:28]. Например, при проектировании системы типа Spotify невозможно охватить всё за час, поэтому критически важно ограничить область (scope) [11:55].
2.  **Проектирование API и модели данных (15–20 минут).** Определение основных сущностей (например, видео и пользователи для YouTube) и связей между ними [13:44]. Здесь же обсуждаются контракты API (пагинация, ошибки, версионирование) и выбор типа базы данных [14:24].
3.  **Высокоуровневая архитектура (15 минут).** Отрисовка базовых компонентов: API-серверов, балансировщиков нагрузки, баз данных и слоев кэширования [15:17]. Важно проследить путь запроса от фронтенда до базы данных [16:20].
4.  **Узкие места и надежность (5–10 минут).** Обсуждение точек отказа (Single Points of Failure), механизмов резервирования и мониторинга [17:26]. На этом этапе senior-инженер показывает, как система будет вести себя под нагрузкой или при сбоях [18:08].
5.  **Компромиссы и расширение.** Завершающий этап, где кратко перечисляются принятые жертвы (например, выбор согласованности в ущерб доступности) и возможные пути развития системы в будущем [19:02].

### 💡 Практические советы и этикет интервью
[[JUMP:20:09]]

Интервью по System Design — это не монолог и не домашнее задание, а обсуждение между двумя коллегами [23:02]. 

*   **Избегайте крайностей:** не молчите, рисуя схемы, но и не говорите без умолку [20:35]. Рекомендуется делать «чек-ин» с интервьюером после каждого этапа, спрашивая: «Имеет ли смысл этот объем требований, прежде чем я перейду к архитектуре?» [12:50].
*   **Предлагайте варианты:** даже если выбор кажется очевидным (например, SQL для платежной системы), упомяните альтернативы и объясните, почему NoSQL в данном случае подходит меньше [21:42].
*   **Будьте честны:** невозможно создать идеальный дизайн за 60 минут [19:44]. Важно показать, что вы осознаете несовершенства своей схемы и знаете, как их исправить при наличии времени [19:56].

В завершение вводной части автор упоминает, что далее эти принципы будут применены к проектированию музыкальной платформы (аналога Spotify) с нагрузкой в 500 000 пользователей и библиотекой в 30 миллионов песен [25:10].

## 🎧 Проектирование систем: от емкости до масштабирования

[[JUMP:25:24]]

На этапе детального проектирования системы стриминга музыки, такой как Spotify, фокус смещается с общих идей на конкретные расчеты и выбор технологий. Основная сложность здесь заключается в огромном объеме медиаданных, которые диктуют свои правила как для хранения, так и для передачи через сеть. 

### Расчет емкости и стоимости данных
[[JUMP:26:30]]

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

*   **Хранилище аудио:** При среднем размере песни в 3 МБ (качество 128 кбит/с) и библиотеке из 30 миллионов треков, только «сырые» данные займут около 90 ТБ [26:56]. Однако в реальности эта цифра увеличивается в 2-3 раза (до 180–270 ТБ) из-за необходимости репликации в разных регионах и хранения версий файлов, которые артисты могут перезаливать [27:10].
*   **Метаданные:** Информация о песнях (название, артист, URL) занимает около 100 байт на трек, что для всей библиотеки составляет всего 3 ГБ [27:48]. Данные профилей пользователей (500 тыс. человек) при расчете 1 КБ на профиль займут около 0,5 ГБ [28:20].
*   **Пропускная способность:** Если каждый пользователь слушает 10–15 песен в день, это создает колоссальный исходящий трафик (egress). Затраты на передачу данных из облака пользователям (egress costs) станут одной из главных статей расходов [29:00]. 

Для оптимизации передачи используются разные форматы: 64 кбит/с (AAC) для экономии мобильного трафика, 128 кбит/с как стандарт и 320 кбит/с для премиум-пользователей [26:02].

### Высокоуровневая архитектура и рабочие процессы
[[JUMP:29:20]]

Система строится вокруг разделения тяжелых медиафайлов и легких метаданных. В центре находится балансировщик нагрузки (Load Balancer), использующий алгоритмы Round Robin или Least Connections для распределения HTTP-запросов между API-серверами [29:45].

**Процесс чтения (воспроизведение песни):**

1. Приложение запрашивает песню через `GET /songs/:id` [31:35].
2. API-сервер проверяет JWT-токен пользователя [31:50].
3. Сервер запрашивает метаданные из SQL-базы и генерирует **Signed URL** (подписанную одноразовую ссылку) [32:15].
4. Приложение использует этот URL для запроса фрагментов аудио напрямую из объектного хранилища (например, AWS S3), которое стримит данные обратно пользователю [32:25].

**Процесс записи (загрузка артистом):**
Артист отправляет `POST`-запрос, API проверяет формат и размер файла [33:20]. Аудио сохраняется в Blob-хранилище, после чего из него извлекаются метаданные (длительность, битрейт), которые записываются в SQL-базу [33:53]. Если используется CDN, инициируется процесс инвалидации кеша, чтобы пользователи сразу увидели обновление [34:55].

### Дизайн API и структура базы данных
[[JUMP:35:25]]

Для обеспечения RESTful-принципов API разделяется на функциональные блоки:

*   **Поиск и обнаружение:** Эндпоинты `/search` с параметрами `type`, `limit` и `offset` для пагинации, а также `/songs/trending` для популярных треков [35:40].
*   **Управление контентом:** Доступ к песням артиста через `/artists/:id/songs` или получение деталей конкретной песни [36:26].
*   **Действия пользователей:** Создание плейлистов (`POST /playlists`), добавление треков с указанием их позиции в списке (`PUT /playlists/:id/songs`) и возможность «лайкнуть» песню через `/songs/:id/like` [37:46].

В качестве базы данных выбрана **реляционная СУБД (PostgreSQL/MySQL)**, так как структура требует множества соединений (Joins) [30:20]. Например, для связи песен и артистов, а также плейлистов и треков используются связующие таблицы (Join Tables) [42:00]. Для ускорения поиска по названиям и именам создаются индексы [43:50].

### Стратегии масштабирования до 50 миллионов пользователей
[[JUMP:44:30]]

Когда база пользователей вырастает в 100 раз (с 500 тыс. до 50 млн), а библиотека до 200 млн песен, система требует горизонтального масштабирования:

1.  **Географическое распределение:** Развертывание CDN-узлов и балансировщиков в разных регионах (Северная Америка, Европа и т.д.) для снижения задержек [45:34].
2.  **Управление кешем:** Использование политики LRU (Least Recently Used) для автоматического удаления редко прослушиваемого контента из кеша при заполнении хранилища [46:05].
3.  **Масштабирование БД:** Разделение операций чтения и записи (Read Replicas) — запись идет в мастер-базу, а чтение — в ведомые реплики (followers) [47:05]. 
4.  **Шардирование:** Разделение данных по географическому признаку (шард для США, шард для Европы) или по диапазонам ID артистов для обеспечения бесконечной горизонтальной масштабируемости [47:35].

### Философия Senior-инженера
[[JUMP:48:50]]

Различие между Middle и Senior разработчиком заключается в подходе к проектированию. Senior-инженер представляет процесс как пирамиду:

*   **Основание (Планирование):** Самая важная часть — понимание требований, расчет емкости и проектирование архитектуры [49:05].
*   **Середина (Выбор стека):** Выбор СУБД, стратегий кеширования и хранилищ [49:18].
*   **Верхушка (Кодинг):** Самая маленькая часть, требующая меньше всего «мозговой активности», которую сегодня могут выполнять ИИ-модели [49:40].

Мидл-разработчики часто переворачивают эту пирамиду, начиная сразу с кодинга без четкого плана, что ведет к архитектурным тупикам при масштабировании [50:10].

## 🏨 Проектирование системы бронирования отелей (Booking.com/Airbnb)

[[JUMP:51:14]]

Разработка системы бронирования — классическая задача на интервью для системных архитекторов [51:14]. Ошибка многих разработчиков заключается в том, что они рассматривают её как типичное CRUD-приложение с кэшированием и репликацией [51:28]. Однако главная сложность здесь заключается в обработке конкурентности: когда два пользователя одновременно пытаются забронировать последний свободный номер [51:41]. Простой подход «select, затем insert» приведет к проблеме двойного бронирования, списанию средств у обоих клиентов и потере доверия к платформе [51:55]. Senior-уровень подразумевает понимание того, как сохранять целостность данных под высокой нагрузкой и проектировать систему с учетом возможных отказов отдельных компонентов [52:08].

### Сбор требований и функционал
[[JUMP:53:29]]

Проектирование начинается с уточняющих вопросов для определения рамок (scope) [53:29]. В рамках данной системы мы принимаем следующие условия:

*   **Платежи:** Поддержка оплаты прямо на платформе (интеграция с внешними шлюзами) [53:55].
*   **Каналы бронирования:** Поддержка как веб-версии, так и мобильного приложения [54:08].
*   **Отмены и овербукинг:** Система должна поддерживать отмену бронирования [54:21]. Также учитывается практика овербукинга, когда отели позволяют бронировать чуть больше номеров, чем есть в наличии, рассчитывая на статистический процент отмен [54:33].
*   **Динамическое ценообразование:** Цены меняются в зависимости от спроса и дат (например, в период Рождества стоимость может вырасти втрое) [55:00].

К основным функциональным требованиям относятся: поиск отелей по локации, датам и количеству гостей [56:35], управление бронированиями (просмотр, отмена) [56:48] и предотвращение двойных бронирований [57:02]. Нефункциональные требования включают строгую консистентность для процесса бронирования (нельзя допустить два бронирования на один номер) [57:39], низкую задержку (поиск менее 500 мс) [58:07] и высокую доступность на уровне «трех девяток» (99.9%) [58:20].

### Оценка масштаба и нагрузки (QPS vs TPS)
[[JUMP:58:49]]

Для оценки нагрузки можно использовать два подхода: «сверху вниз» (на основе трафика посещений) или «снизу вверх» (на основе количества бронирований) [1:00:20]. Если взять за основу показатели Booking.com, система получает около 1.1 млрд бронирований в год [1:00:33]. При среднем сроке проживания в 2 ночи это дает примерно 550 млн успешных транзакций в год или 1.5 млн бронирований в день [1:02:08]. В секунду это составляет всего **17 транзакций (TPS)**, которые записываются в базу данных [1:02:21].

Однако нагрузка на чтение значительно выше. При 500 млн визитов в месяц и просмотре примерно 8 страниц за визит, мы получаем около 200 визитов в секунду [1:03:03]. Учитывая запросы на поиск и проверку доступности, система должна поддерживать около **1000 запросов в секунду (QPS)** на чтение [1:03:28]. Это делает систему «тяжелой на чтение» (read-heavy), что диктует выбор архитектуры, оптимизированной для быстрого поиска при сохранении строгой согласованности транзакций [1:03:41].

### Высокоуровневая архитектура и сервисы
[[JUMP:1:04:20]]

Система строится на базе сервис-ориентированной архитектуры с API Gateway в качестве единой точки входа [1:05:00]. Основные компоненты включают:

*   **Hotel Service:** Хранит информацию об отелях и номерах. Данные здесь статичны и хорошо кэшируются [1:05:27].
*   **Reservation Service:** Сердце системы, отвечающее за логику бронирования, инвентаризацию и решение проблем конкурентности [1:05:40].
*   **Rate Service:** Управляет динамическими ценами в зависимости от спроса [1:05:53].
*   **Payment & Notification Services:** Обработка платежей (Stripe, PayPal) и отправка подтверждений пользователям [1:06:18].

Для обеспечения консистентности на уровне данных используется SQL-база (например, PostgreSQL), обеспечивающая ACID-гарантии [1:06:30]. Однако для поиска 1000 QPS возможностей SQL недостаточно, поэтому внедряется **ElasticSearch** [1:07:25]. Он обеспечивает полнотекстовый поиск и сложную фильтрацию (по радиусу от локации, цене, рейтингу) [1:07:38]. Данные в ElasticSearch попадают через механизм событийной согласованности (eventual consistency): после успешной оплаты сообщение публикуется в очередь (Kafka или RabbitMQ), откуда оно считывается для обновления поискового индекса [1:09:23].

### Проектирование API и проблема микросервисов
[[JUMP:1:10:01]]

При выборе между монолитом и микросервисами важно найти баланс. Полные микросервисы с разделенными базами данных порождают проблемы распределенных транзакций [1:10:43]. Прагматичный подход для этой системы — **общая база данных для тесно связанных сущностей** (Hotels и Reservations) [1:11:08]. Это позволяет использовать локальные ACID-транзакции вместо сложных распределенных протоколов, сохраняя при этом разделение логики на уровне приложений [1:11:20].

API проектируется по принципам REST [1:11:47]:

1.  `GET /hotels` — поиск через ElasticSearch с фильтрами [1:13:19].
2.  `GET /hotels/{id}` — получение детальной информации напрямую из основной БД [1:13:33].
3.  `POST /hotels` — создание нового объекта админом (сначала запись в SQL, затем асинхронное обновление индекса поиска) [1:14:12].
4.  `PUT/PATCH /hotels/{id}` — обновление данных [1:14:39].
5.  Номера отелей обрабатываются как подресурсы: `/hotels/{id}/rooms` [1:15:19].

## 🏨 Управление бронированием: API и архитектура данных
[[JUMP:1:15:31]]

Процесс проектирования системы бронирования отелей требует четкого разделения зон ответственности между сервисами и тщательной проработки структуры базы данных. На этапе реализации API важно следовать принципам RESTful для обеспечения предсказуемости интерфейса [1:15:57]. В то время как управление информацией об отелях и номерах кажется тривиальным, настоящая сложность кроется в логике резервирования, предотвращении двойных бронирований и эффективном поиске [1:17:49].

### Проектирование REST API и обработка транзакций
[[JUMP:1:15:31]]

Основные эндпоинты системы делятся на управление каталогом и управление бронированиями. Для работы с отелями используются стандартные GET-запросы к `/hotels` и `/hotels/{id}`, которые возвращают список доступных объектов или детальную информацию из базы данных [1:15:31]. Аналогичная логика применяется к номерам: добавление нового типа номера происходит через POST-запрос к `/hotels/{id}/rooms` [1:15:57].

Специфика бронирования (Reservation Service) требует более сложных механизмов:

*   **История и детали:** Пользователь может получить историю своих заказов через `/reservations` или детали конкретного счета/инвойса через `/reservations/{id}` [1:17:07].
*   **Создание брони:** POST-запрос инициирует процесс резервирования, где критически важно предотвратить двойные клики пользователя (double-clicking) [1:18:03].
*   **Отмена и изменение:** Удаление брони (DELETE) требует проверки бизнес-правил — например, запрет на отмену менее чем за неделю до заезда [1:21:07]. Изменение дат реализуется через PUT-запрос, что может инициировать дополнительные финансовые транзакции [1:21:33].

Для предотвращения дублирующихся запросов от одного и того же пользователя рекомендуется генерировать уникальный идентификатор бронирования (Reservation ID) на стороне фронтенда [1:18:17]. Это «легковесное» решение позволяет бэкенду просто проверять наличие ID в базе: если транзакция с таким идентификатором уже существует, система вернет существующую бронь вместо создания новой [1:20:41].

### Модели данных и инвентаризация номеров
[[JUMP:1:12:53]]

Фундаментальный принцип системы: пользователь бронирует **тип номера** (Room Type), а не конкретную физическую комнату [1:23:05]. Конкретный номер комнаты присваивается гостю только в момент регистрации (check-in) [1:23:19]. 

Архитектура БД строится на следующих сущностях:

1.  **Hotels:** Содержит ID, название, адрес и описание. Для крупных сетей можно использовать таблицу `Hotel Groups`, объединяющую разные филиалы под одним брендом [1:23:33].
2.  **Room Types:** Определяет характеристики (количество кроватей, вместимость). Отношение с отелями — «один ко многим» [1:24:28].
3.  **Room Type Rates:** Позволяет гибко управлять ценами для одного типа номера (например, базовый тариф, вид на океан или тариф с завтраком) [1:24:40].
4.  **Room Type Inventory:** Ключевая таблица, отслеживающая наличие мест по дням для каждого типа номера в конкретном отеле [1:26:41]. Она предзаполняется на 2 года вперед [1:29:45].
5.  **Transactions:** Вынесение платежей в отдельную таблицу позволяет отслеживать несколько операций по одной брони, например, доплату при смене дат или частичный возврат при отмене [1:28:51].

### Гарантии консистентности и борьба с Race Conditions
[[JUMP:1:13:14]]

Выбор реляционной базы данных (SQL) обусловлен необходимостью соблюдения ACID-гарантий [1:13:28]. При соотношении 1000 чтений к 17 записям в секунду SQL-системы обеспечивают нужную консистентность, предотвращая ситуацию, когда два разных пользователя одновременно бронируют последний доступный номер [1:13:54].

Для решения проблем конкурентности рассматриваются два подхода:

*   **Serializable Isolation Layer:** Простой в реализации метод, который заставляет транзакции выполняться последовательно [1:13:51]. Подходит для систем с низким TPS, но создает риск дедлоков и плохо масштабируется [1:13:33].
*   **Row Locking (Select for Update):** Оптимальный метод для высокой нагрузки. Система блокирует конкретную строку инвентаря на время проверки доступности и записи брони [1:16:13]. Это позволяет другим транзакциям, работающим с другими номерами, идти параллельно, сохраняя целостность данных [1:16:41].

### Масштабирование через шардирование и поиск
[[JUMP:1:17:11]]

Когда нагрузка на БД начинает расти, применяется шардирование. В качестве **ключа шардирования (shard key)** эффективнее всего использовать `hotel_id` [1:17:53]. Это гарантирует, что все данные по одному отелю (типы номеров, инвентарь) останутся на одном узле, позволяя выполнять локальные джойны без дорогостоящих кросс-шардовых запросов [1:18:07].

Для распределения данных используются два метода:

1.  **Modulo Sharding:** `hash(hotel_id) % N`. Подходит, если количество шардов стабильно [1:18:32]. Однако добавление нового сервера потребует перераспределения почти всех данных [1:19:15].
2.  **Consistent Hashing:** Использует хэш-кольцо, где добавление нового узла затрагивает лишь малую часть данных (около 1/N) [1:19:29]. Это предпочтительный вариант для динамически растущих систем [1:20:21].

Ранее в разговоре они касались использования Elastic Search для ускорения поиска по фильтрам (локация, цена, даты), так как выполнение таких тяжелых запросов напрямую в SQL-базе было бы слишком медленным для масштабируемой системы [1:22:27].

## 🏨 Оптимизация поиска и масштабирование системы бронирования
[[JUMP:1:40:34]]

### Кэширование инвентаря и управление доступностью
[[JUMP:1:40:34]]

Для эффективной работы системы бронирования отелей необходимо кэшировать «горячие» данные об инвентаре в Redis или Memcached [1:40:34]. Структура кэша представляет собой ключ, состоящий из комбинации ID отеля, типа номера и даты, а значение содержит количество доступных номеров и общее число забронированных мест [1:40:46]. При расчетах также учитывается фиксированный процент овербукинга (например, 10%), который не хранится в кэше, но используется логикой приложения [1:41:01].

Процесс работы с кэшем выглядит следующим образом:

*   **Поиск:** При поиске отеля система сначала проверяет Redis [1:41:38]. Если данные найдены (cache hit), они мгновенно отображаются пользователю [1:41:52].
*   **Промах кэша:** Если данных нет или их TTL (время жизни, обычно около 5 минут) истекло, система обращается к основной базе данных, после чего обновляет кэш [1:42:04].
*   **Бронирование:** При совершении транзакции данные в базе обновляются, а соответствующие ключи в кэше инвалидируются [1:42:32]. Это гарантирует, что следующий поиск покажет актуальное состояние доступности номеров [1:42:57].

### Оптимизация поиска с Elastic Search
[[JUMP:1:43:09]]

Использование стандартных SQL-запросов с оператором `LIKE` для поиска отелей крайне неэффективно, так как это приводит к полному сканированию таблиц (full table scan), что убивает производительность даже при наличии индексов [1:44:53]. SQL плохо справляется с ранжированием по релевантности, опечатками в запросах пользователей и сложными географическими расчетами [1:45:36].

Elastic Search решает эти проблемы, предлагая специализированный движок для поиска и аналитики [1:43:22]:

1.  **Сложные фильтры:** Позволяет быстро находить отели по множеству критериев (цена, звездность, локация) среди миллионов документов [1:44:03].
2.  **Полнотекстовый поиск:** Токенизирует запросы (например, «luxury hotel Paris») и ранжирует результаты по релевантности [1:46:02].
3.  **Обработка опечаток:** Благодаря алгоритмам расстояния редактирования система поймет запрос, даже если пользователь пропустил букву в названии города [1:46:30].
4.  **Гео-поиск:** Оптимизирован для нахождения объектов в радиусе определенного количества километров от заданных координат [1:46:44].

Данные в Elastic Search денормализуются: вместо множества связанных таблиц SQL (отели, типы номеров, удобства) создается один JSON-документ, содержащий всю необходимую информацию, что исключает необходимость дорогостоящих JOIN-операций при поиске [1:47:24].

### Синхронизация данных и репликация
[[JUMP:1:47:48]]

Поскольку PostgreSQL остается единственным источником истины (Single Source of Truth) для транзакций, необходимо поддерживать актуальность данных в Elastic Search [1:47:48]. Для этого используется механизм Change Data Capture (CDC). Инструмент с открытым исходным кодом Debezium считывает логи упреждающей записи (Write Ahead Log) из Postgres и транслирует изменения в Kafka [1:48:32]. Затем специальные потребители (consumers) обновляют индексы Elastic Search, обеспечивая итоговую согласованность (eventual consistency) [1:48:58].

Для обеспечения масштабируемости при нагрузке в тысячи запросов в секунду применяется репликация базы данных [1:49:49]:

*   **Primary Database:** Используется для всех операций записи и критичных чтений (например, сразу после бронирования) во избежание конфликтов [1:50:41].
*   **Read Replicas:** Распределяются по разным зонам доступности. Пользователь подключается к ближайшей реплике для ускорения поиска [1:50:15].
*   **Географическое распределение:** Кэши Redis и индексы Elastic Search также реплицируются по регионам (США, Европа, Азия), чтобы данные были физически ближе к пользователям [1:52:13].

## 💬 Проектирование мессенджера (аналог WhatsApp)
[[JUMP:1:53:18]]

### Требования и оценка масштаба
[[JUMP:1:54:38]]

Проектирование систем обмена сообщениями — классическая задача для Senior-интервью, требующая понимания работы с постоянными соединениями (stateful connections) и обеспечения надежности при сбоях [1:53:18]. Основные функциональные требования включают:

*   Индивидуальные и групповые чаты (до 100 участников) [1:54:51].
*   Доставка сообщений в офлайн-режиме и отслеживание статусов (отправлено, доставлено, прочитано) [1:55:06].
*   Поддержка медиафайлов и отображение статуса присутствия (online/offline) [1:55:19].

Система должна обладать низкой задержкой (менее 500 мс) и гарантировать доставку сообщений [1:55:31]. При масштабе в 500 млн активных пользователей ежедневно генерируется около 10 млрд сообщений [1:56:35]. Это создает нагрузку в 115 000 записей в секунду в среднем и до 500 000 в пиковые часы [1:57:14]. Общий объем данных может достигать 3.6 петабайт в год, однако стратегия мессенджеров обычно подразумевает удаление сообщений с сервера сразу после их успешной доставки на устройство пользователя [1:58:19].

### Высокоуровневая архитектура компонентов
[[JUMP:1:59:11]]

Архитектура системы строится на следующих ключевых элементах:

*   **Chat Servers:** Поддерживают постоянные WebSocket-соединения с клиентами для двусторонней связи в реальном времени [1:59:50].
*   **Message Queue (Kafka/RabbitMQ):** Используется для декуплирования (отвязки) процесса приема сообщения от процесса его доставки и записи в базу [2:00:15].
*   **NoSQL Storage (Cassandra/DynamoDB):** Оптимальный выбор для хранения сообщений благодаря высокой скорости записи и простоте запросов по ID пользователя или беседы [2:00:55].
*   **Presence Service:** Отдельный сервис, который обрабатывает «сердцебиения» (heartbeats) от клиентов, обновляя статус пользователя в Redis [2:03:15].
*   **Media & CDN:** Файлы сохраняются в объектном хранилище (S3), а для быстрой загрузки по всему миру распространяются через сеть доставки контента (CDN) [2:01:58].

### Протоколы связи: почему WebSocket?
[[JUMP:2:03:41]]

Существует три основных подхода к организации связи между клиентом и сервером:

1.  **Polling (опрос):** Клиент периодически запрашивает новые данные. Это неэффективно, так как создает задержки и тратит ресурсы сервера на пустые ответы [2:04:07].
2.  **Long Polling:** Сервер удерживает запрос открытым до появления новых данных или тайм-аута. Это лучше обычного опроса, но все равно требует частого переустановления соединений [2:05:12].
3.  **WebSocket:** Создает единое долгоживущее TCP-соединение. Именно этот вариант является стандартом для мессенджеров, так как обеспечивает мгновенную передачу данных в обе стороны с минимальными накладными расходами [2:03:53].

## 📡 Инфраструктура реального времени: WebSockets и синхронизация данных

[[JUMP:2:05:39]]

В основе современных мессенджеров лежит необходимость мгновенного обмена сообщениями. Использование традиционных HTTP-запросов для этих целей неэффективно: постоянное открытие и закрытие соединений, а также полный HTTP-handshake для каждого сообщения создают огромную нагрузку на систему [2:05:39]. В условиях масштабирования чат-серверы тратят больше ресурсов на управление жизненным циклом соединений, чем на саму доставку данных [2:05:51].

Решением является использование **WebSockets** — постоянного двунаправленного соединения между клиентом и сервером [2:06:17]. Процесс начинается с инициации HTTP-соединения, которое затем «апгрейдится» до WebSocket [2:06:29]. Это позволяет серверу мгновенно «пушить» сообщения пользователю, как только они становятся доступны, а клиенту — отправлять ответы без создания новых соединений [2:06:56]. 

Однако у WebSockets есть свои компромиссы:

* **Балансировка нагрузки:** Требуются специальные балансировщики, поддерживающие 4-й уровень (Layer 4) вместо стандартного Layer 7 [2:07:21].
* **Память:** Каждое открытое соединение потребляет оперативную память сервера для хранения состояния [2:07:35].
* **Service Discovery:** Поскольку чат-серверы могут падать или масштабироваться, системе необходим реестр сервисов (например, Consul или AWS Cloud Map) [2:08:13]. Балансировщик запрашивает список «здоровых» узлов у реестра каждые 10 секунд, чтобы направлять трафик только на активные серверы [2:08:40].

### 🗄️ Проектирование схемы данных и API

[[JUMP:2:09:31]]

Для системы уровня WhatsApp требуется комбинация различных типов баз данных [2:09:31]. Информация о пользователях (ID, номер телефона, имя, время последнего входа) хранится в реляционных БД, таких как **PostgreSQL** или **MySQL** [2:09:45]. 

Для самих сообщений, учитывая их огромный объем, идеально подходит **Cassandra** — распределенная NoSQL база данных [2:10:12]. Таблица сообщений включает:

* Sender ID и Receiver ID (связи с таблицей пользователей) [2:10:26].
* Content (текст или ссылки на медиафайлы) [2:10:38].
* Статус (отправлено, доставлено, прочитано) и временные метки [2:10:51].

Для управления группами используются таблицы `groups` и `group_members` в SQL-базе, обеспечивая связь «один ко многим» [2:11:06]. Важным элементом является **User Connections Registry** на базе **Redis** [2:11:45]. Этот кэш хранит соответствие ID пользователя и ID сервера, к которому он подключен, обновляясь каждые 300 секунд через «heartbeat» [2:11:57].

Взаимодействие начинается с аутентификации: клиент отправляет токен и данные устройства, получая в ответ ID сессии и счетчик непрочитанных сообщений [2:12:38]. После установления WebSocket-соединения сервер может не только доставлять новые сообщения, но и подтверждать получение (acknowledgement) или сообщать о прочтении [2:13:05].

### 🔄 Синхронизация между устройствами и обработка медиа

[[JUMP:2:15:31]]

Современный пользователь может одновременно использовать телефон, планшет и десктоп [2:15:31]. Когда User A отправляет сообщение, сервер чата должен определить все активные устройства User B через Redis [2:16:27]. Если User B подключен к разным серверам (например, серверу B и серверу E), сообщение дублируется на оба узла [2:16:41]. Синхронизация статусов (например, «прочитано») также транслируется на все устройства обоих участников [2:18:06].

При конфликтах данных между устройствами используется техника **LWW (Last Write Wins)** с номерами версий [2:18:57]. Для экономии ресурсов применяется инкрементальная синхронизация: новые устройства запрашивают историю только за последние 30 дней, а старые данные подгружаются лениво из «холодного» хранилища [2:19:22].

Передача медиафайлов (фото, видео) реализуется через **REST API**, а не через WebSockets, чтобы не перегружать чат-серверы тяжелым трафиком [2:14:24]. Процесс выглядит так:

1. Клиент запрашивает у сервера **Pre-signed URL** для загрузки [2:30:03].
2. Сервер генерирует временную ссылку на объектное хранилище (например, **AWS S3**) [2:30:16].
3. Клиент загружает файл напрямую в S3, минуя чат-сервер [2:30:29].

### 👥 Групповые чаты и оффлайн-доставка

[[JUMP:2:22:31]]

Если получатель сообщения находится оффлайн, сервер добавляет сообщение в индивидуальный инбокс в Redis и БД [2:23:10]. Одновременно с этим инициируется push-уведомление через системы Apple (APNs) или Google (FCM) [2:23:24]. При возвращении пользователя в сеть сервер проверяет наличие ожидающих сообщений в инбоксе и доставляет их [2:24:30].

В групповых чатах (на 50–100 человек) нагрузка возрастает [2:25:10]. При отправке сообщения сервер запрашивает список участников у **Group Service** [2:25:50]. Затем система проверяет статус каждого участника через Redis:

* **Для онлайн-пользователей:** Сообщение публикуется через модель **Redis Pub/Sub** и доставляется параллельно на соответствующие серверы [2:26:32].
* **Для оффлайн-пользователей:** Записываются данные в инбокс и отправляются push-уведомления [2:27:52].

Несмотря на множественную доставку, в базу данных (Cassandra) выполняется всего одна операция записи сообщения, что оптимизирует использование ресурсов [2:28:32].

## 📦 Оптимизация передачи контента и масштабирование записи

[[JUMP:2:30:44]]

При проектировании систем уровня WhatsApp передача тяжелых медиафайлов (видео, изображения, голосовые заметки) требует принципиально иного подхода, чем пересылка текстовых сообщений [2:33:11]. Если чат-сервер, обслуживающий 100 000 соединений, попытается одновременно передавать файлы даже по несколько мегабайт, он мгновенно выйдет из строя под нагрузкой на полосу пропускания [2:32:59].

### Обработка медиафайлов через Pre-signed URLs и CDN
[[JUMP:2:30:44]]

Для разгрузки системы используется механизм **Pre-signed URLs** (пре-подписанных ссылок) и объектное хранилище (например, S3). Процесс выглядит следующим образом:

*   **Загрузка:** Пользователь А отправляет файл в S3, получает подтверждение и URL, после чего передает этот URL как часть текстового сообщения через основной чат-сервер [2:30:44]. 
*   **Получение:** Когда Пользователь Б хочет просмотреть медиа, его приложение запрашивает у чат-сервера временную ссылку на скачивание [2:31:25]. Сервер генерирует Pre-signed URL с ограниченным временем жизни (TTL), соответствующим политике удержания сообщений (например, 30 дней) [2:33:38].
*   **Доставка:** Наиболее масштабируемый подход — использование **CDN** [2:32:05]. Файлы реплицируются на узлы, географически близкие к пользователям. Несмотря на более сложную логику запросов, это дает колоссальные преимущества в производительности и стоимости при огромном масштабе [2:32:47].

### Декуплинг и пакетная запись через Kafka
[[JUMP:2:34:05]]

Чтобы масштабировать обработку сообщений до миллионов транзакций, необходимо отвязать (decouple) прием сообщений от их записи в базу данных [2:34:05]. В центре этой архитектуры стоит **Kafka** или аналогичная очередь сообщений [2:34:32].

1.  **Партиционирование:** Каждому инстансу сервиса хранения сообщений назначаются свои партиции (например, инстанс №1 обрабатывает партиции 0–9, инстанс №2 — 10–19) [2:34:58]. 
2.  **Batch Writing (Пакетная запись):** Вместо того чтобы выполнять 1000 отдельных вставок в БД, каждая из которых занимает ~5 мс (итого 5 секунд), система накапливает пакет в памяти (например, 1000 сообщений за 5 секунд) и выполняет одну атомарную запись [2:38:15, 2:40:29]. Это сокращает время операции до 50 мс, обеспечивая **100-кратный прирост пропускной способности** [2:40:41].
3.  **Обогащение данных:** Перед записью каждый инстанс валидирует схему, проверяет лимиты метаданных, вычисляет хэш сообщения для дедупликации и извлекает упоминания (@username) или ссылки для генерации превью [2:37:22, 2:37:48].
4.  **Обработка ошибок:** Если запись не удалась, используется логика повторов (retries). Если и это не помогает, сообщение отправляется в **Dead Letter Queue (DLQ)** — отдельную очередь для проблемных данных, которые будут обработаны позже вручную или специальным механизмом, чтобы не блокировать основной поток [2:35:51, 2:39:35].

### Сквозное шифрование (End-to-End Encryption)
[[JUMP:2:41:49]]

Безопасность в WhatsApp строится на базе **Signal Protocol**, который гарантирует, что даже серверы компании не могут прочитать содержимое переписки [2:42:02]. 

*   **Ключи:** При регистрации пользователь генерирует пару ключей: приватный (никогда не покидает устройство) и публичный (загружается на Key Server) [2:42:41]. Также генерируется набор «предварительных ключей» (prekeys) для будущих сессий [2:43:06].
*   **Обмен:** Когда Пользователь А начинает чат с Пользователем Б, он скачивает публичный ключ и один из prekeys адресата с сервера [2:43:34]. На их основе устанавливается сессия и выполняется обмен ключами (key exchange) [2:43:47].
*   **Защита от взлома:** Каждое сообщение в сессии шифруется с использованием счетчика сообщений [2:44:28]. Это означает, что даже если сессионный ключ будет скомпрометирован, злоумышленнику будет крайне сложно расшифровать отдельные сообщения из-за постоянно меняющихся параметров шифрования [2:44:42].

### Борьба с абьюзом и Rate Limiting
[[JUMP:2:45:08]]

Для предотвращения DDOS-атак и скрапинга данных необходимо внедрить строгие лимиты на действия пользователей [2:45:08]. 

Система проверяет каждый тип активности через распределенное хранилище (например, **Redis** для обеспечения атомарности между серверами) [2:51:30]:

*   **Сообщения:** Ограничение, например, 100 сообщений в минуту [2:46:02].
*   **Группы:** Лимит на создание (например, не более 5 групп в час) и на количество инвайтов [2:47:59, 2:51:02].
*   **Соединения:** Проверка количества попыток входа с разных IP-адресов [2:50:22].

При превышении лимитов применяется **прогрессивное наказание**: сначала предупреждение («Too many requests»), затем блокировка на 1 час, затем на 24 часа, и, наконец, деактивация аккаунта для ручной проверки модератором [2:46:40, 2:47:06]. Для проверки, является ли пользователь человеком, может быть предложено решение капчи [2:46:27].

### Шардирование базы данных
[[JUMP:2:52:10]]

Когда данных становится слишком много для одного сервера, применяется шардирование (разделение данных между инстансами БД) [2:52:22]. Существует два основных подхода:

1.  **По User ID:** Все сообщения пользователя А хранятся в одном шарде. Чтение происходит мгновенно, но активные пользователи создают «горячие точки» (hotspots), делая шарды несбалансированными [2:52:35, 2:53:03].
2.  **По Message ID:** Обеспечивает равномерное распределение данных, но для получения истории переписки одного пользователя придется опрашивать все шарды системы, что крайне медленно и дорого [2:53:30, 2:53:42].

Оптимальное решение для мессенджера — **шардирование по User ID с дублированием записей** [2:54:21]. Сообщение записывается дважды: в шард отправителя и в шард получателя [2:54:36]. Это удваивает объем записи, но делает чтение истории «ослепительно быстрым», так как каждый пользователь обращается только к своему шарду или его реплике [2:54:50, 2:55:31].

## 🏁 Масштабирование, отказоустойчивость и стратегия проектирования

[[JUMP:2:55:44]]

На финальных этапах проектирования крупной системы, такой как WhatsApp, фокус смещается с базовой логики на обеспечение глобальной доступности и выживаемости сервиса при сбоях. Важно понимать, что идеальных систем не существует, и задача инженера — не избежать ошибок, а минимизировать их последствия через продуманные компромиссы (trade-offs).

### Оптимизация хранилища: шардирование и репликация
[[JUMP:2:55:44]]

При работе с огромными объемами данных необходимо заранее определить, что является приоритетом: скорость записи или скорость чтения [2:55:44]. Если этот вопрос не был прояснен на этапе сбора требований, его стоит задать именно сейчас, прежде чем внедрять сложные механизмы оптимизации [2:55:57].

Для масштабирования записи обычно применяется **шардирование** — разделение базы данных на более мелкие фрагменты (шарды) [2:56:23]. Например, данные могут распределяться по `user_id`: пользователи с ID от 1 до 1000 попадают в первый шард, который становится основным узлом (primary node) для их записей [2:57:01]. 

Для масштабирования чтения используется **репликация** [2:56:36]. Данные из основного узла асинхронно синхронизируются с несколькими репликами (обычно тремя), которые обеспечивают итоговую согласованность (eventual consistency) [2:57:15]. Все операции чтения, такие как просмотр списка чатов или непрочитанных сообщений, направляются исключительно на реплики, не нагружая основной узел [2:57:40].

### Очистка данных и фоновые задачи
[[JUMP:2:58:00]]

В системах такого масштаба невозможно бесконечно хранить все данные в оперативной памяти или быстрых хранилищах. Необходима инфраструктура для очистки «мусора»: просроченных медиафайлов, старых недоставленных сообщений или устаревших соединений [2:58:08]. 

Для этого внедряется планировщик задач (например, Airflow или стандартные cron-задания), который запускает воркеры (worker pool) на ежедневной или еженедельной основе [2:58:22]. Процесс выглядит следующим образом:

*   Задачи помещаются в отдельную очередь (не путать с основной очередью сообщений) [2:58:48].
*   Воркеры извлекают задачи и выполняют удаление данных [2:59:01].
*   Результаты работы отправляются в систему мониторинга для отслеживания эффективности [2:59:28].

Хотя такие задания усложняют эксплуатацию системы (operational complexity), альтернатива — накопление «мусора» до тех пор, пока система не деградирует полностью [2:59:54].

### Стратегии восстановления при отказе кэша и Kafka
[[JUMP:3:00:19]]

Даже самые надежные компоненты, такие как Redis или Kafka, могут выйти из строя. Проектирование «пути отступления» — критическая часть работы Senior-инженера.

**Отказ Redis:** Поскольку Redis используется для быстрого хранения входящих сообщений, его падение может парализовать доставку. Решением является механизм **fallback to database** [3:01:37]. Если Redis недоступен, сервер чата должен уметь считывать и записывать сообщения напрямую в основную базу данных, пусть это и будет медленнее [3:01:49]. После восстановления Redis запускается фоновое задание для синхронизации пропущенных данных из БД обратно в кэш [3:02:14].

**Отказ Kafka:** Если кластер Kafka становится недоступен, серверы чатов не могут публиковать сообщения, что грозит потерей данных [3:07:59]. Для минимизации потерь используется локальный буфер в памяти сервера чата [3:08:12]. 

1.  При ошибке записи в Kafka сообщение помещается в локальный буфер (например, на 100 000 записей) [3:08:54].
2.  Система ждет заданное время (например, 2 секунды), необходимое для перезапуска Kafka, и пробует снова [3:09:45].
3.  Если сервер чата тоже рискует упасть, данные записываются в локальный лог-файл на диске (append-only log) [3:10:25]. После восстановления сервера данные «проигрываются» из этого файла и отправляются в Kafka [3:10:39].

### Глобальное распределение и Feature Flags
[[JUMP:3:04:28]]

Для обслуживания пользователей по всему миру (например, в Токио или Лондоне) система должна быть распределена по регионам (US-East, US-West, Singapore и т.д.) [3:04:56]. Каждый регион содержит полный стек: балансировщики нагрузки, серверы чатов, кэш и шарды базы данных [3:05:09]. Пользователь подключается к ближайшему IP-адресу через DNS-lookup для минимизации задержек [3:05:23].

Для безопасного внедрения обновлений используются **Feature Flags** (флаги функций) [3:11:45]. Инструменты вроде LaunchDarkly позволяют:

*   Разделять окружения (включать фичу на Staging, но оставлять выключенной на Production) [3:12:24].
*   Проводить постепенное развертывание (gradual rollout), открывая доступ к функции сначала 20% пользователей [3:12:37].
*   Обеспечивать обратную совместимость, отсекая новые поля данных для старых версий клиентских приложений [3:13:29].

### Резюме: как подходить к System Design интервью
[[JUMP:3:13:44]]

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

1.  **Начинайте с простого:** уточните требования, сделайте предварительные расчеты («back-of-the-envelope calculations») и постройте высокоуровневую схему для небольшого числа пользователей [3:13:44].
2.  **Эволюционируйте:** постепенно выявляйте «узкие места» и предлагайте решения по масштабированию [3:14:10].
3.  **Обсуждайте компромиссы:** объясняйте, почему вы выбрали WebSockets вместо HTTP-поллинга, признавая, что они потребляют больше памяти, но дают реальное время [3:14:50].
4.  **Отказоустойчивость напоследок:** приступайте к обсуждению сценариев сбоев только после того, как основные компоненты системы определены [3:15:29].

Главная цель — показать процесс мышления. Интервьюер хочет видеть не «идеальную» схему, а умение рассуждать вслух, воспринимать подсказки и выбирать инструменты на основе конкретных задач, а не личных предпочтений [3:15:54].