Кратко

Основные понятия

Ключевые концепции, необходимые для интервью по System Design

Основные понятия - это фундаментальные принципы и концепции, которые лежат в основе каждого интервью по проектированию систем. В отличие от конкретных технологий (Redis, Kafka) или узкоспециализированных паттернов, это строительные блоки, которые встречаются почти в каждой задаче по System Design.

Думайте об основных понятиях как о словаре и грамматике System Design. Прежде чем обсуждать, как масштабировать Telegram или спроектировать Uber, нужно понимать, что такое кэширование, когда шардировать базу данных и как на самом деле работают сети. Интервьюеры предполагают, что вы это знаете, и будут проверять ваше понимание, когда вы будете предлагать использовать эти подходы.

Общая структура

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

Основы сетей

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

Самое важное решение - выбор протокола общения. Для большинства систем по умолчанию берут HTTP поверх TCP. Это стандартный протокол, он работает везде и закрывает 90% сценариев. Интервьюер будет ожидать этого выбора, если у вас нет конкретной причины сделать иначе.

Сетевые уровни

WebSockets и Server‑Sent Events (SSE) появляются, когда нужны обновления в реальном времени. Ключевое отличие: SSE - это только отправка данных от сервера к клиенту (например, счет в реальном времени или уведомления), а WebSockets дают двустороннюю связь, где обе стороны отправляют сообщения (например, чат или совместное редактирование). SSE проще в реализации и лучше работает на стандартной HTTP-инфраструктуре, но WebSockets необходимы, когда клиентам нужно часто отправлять данные на сервер. Подробнее о подходах можно почитать в описании паттерна Обновления в реальном времени.

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

gRPC стоит упоминать для внутреннего взаимодействия между сервисами, когда критична производительность. Он использует бинарную сериализацию и HTTP/2, что делает его существенно быстрее JSON поверх HTTP. Но для публичных API его почти не используют из‑за ограниченной поддержки в браузерах. Типичный паттерн: REST для внешних API и gRPC для внутренней коммуникации.

Балансировка нагрузки - еще одна любимая тема интервьюеров. Балансировщики L7 работают на уровне приложения и умеют маршрутизировать запросы по содержимому HTTP‑запроса: например, API‑вызовы отправлять в один сервис, а запросы к веб‑страницам - в другой. Балансировщики L4 работают на уровне TCP, они быстрее, но примитивнее: просто распределяют соединения, не заглядывая в их содержимое. Для WebSockets обычно нужен L4‑балансировщик, потому что вам необходимо держать постоянное TCP-соединение.

Частая ошибка - предлагать WebSockets там, где достаточно HTTP с long polling или SSE. WebSockets сильно усложняют масштабирование stateful‑соединений. Не выбирайте их только потому, что в задаче звучит фраза "в реальном времени" - они необходимы, когда действительно требуется двусторонняя коммуникация.

География и задержки важнее, чем кажется большинству кандидатов. Запрос из Владивостока в Калининград имеет минимальную задержку около 100 мс просто из‑за скорости света в оптоволокне, еще до обработки запроса. Если системе нужна низкая задержка по всему миру, придется делать региональные развертывания с репликацией или партиционированием данных по геолокации. Именно поэтому существуют CDN - чтобы отдавать статический контент с edge-серверов, расположенных ближе к пользователям.

С деталями можно ознакомиться в статье Основы сетей.

Проектирование API

Почти в каждом интервью по System Design вам придется набросать API, через которые клиенты взаимодействуют с системой. Хорошая новость в том, что большинству интервьюеров не нужен идеальный дизайн API. Они хотят увидеть, что вы можете предложить разумные эндпоинты и перейти к более сложным архитектурным проблемам. Но небрежное проектирование API может сигнализировать о нехватке опыта, так что основы все же знать стоит.

В 90% интервью вы по умолчанию выбираете REST. Он отображает ресурсы на эндпоинты и использует HTTP‑методы для работы с ними. Например, GET /users/{id} для получения информации о пользователе, POST /events/{id}/bookings для создания бронирования. REST понятен, широко распространен, и интервьюер будет ожидать именно его, если вы не предложите другое решение.

Частая ошибка - тратить слишком много времени на проектирование API. Вы должны уметь набросать 4–5 ключевых эндпоинтов за пару минут и двигаться дальше. Если вы все еще проектируете API через 10 минут после начала интервью - вы ушли слишком глубоко.

Есть несколько концепций, которые стоит упоминать по мере необходимости. Если вы возвращаете большие наборы данных, понадобится пагинация. Пагинация основанная на курсоре лучше для реального времени, где новые элементы добавляются постоянно, но пагинация, основанная на смещении (offset) подходит в большинстве случаев. Для аутентификации используйте JWT‑токены для пользовательских сессий и API‑ключи для межсервисного взаимодействия. И если систему могут атаковать боты или злоумышленники, упомяните ограничение запросов(rate limiting). Но не уходите глубоко в эти темы, если интервьюер сам не спрашивает.

Подробнее можно ознакомиться в статье Проектирование API.

Моделирование данных

Моделирование данных звучит просто, но имеет огромные последствия для системы. Решения о том, какие данные хранить и как их структурировать, напрямую влияют на производительность, масштабируемость, поддерживаемость и сложность разработки.

Моделирование данных

Первый большой выбор - реляционная база данных или NoSQL. Реляционные базы данных, такие как Postgres отлично подходят для структурированных данных с явными связями и требованием сильной согласованности: пользователи -> заказы -> продукты. Вы можете писать сложные запросы с помощью SQL, использовать транзакции для обеспечения согласованности данных и применять ограничения внешнего ключа. Базы данных NoSQL, такие как DynamoDB или MongoDB, прекрасно подходят, когда вам нужны гибкие схемы (ваша структура данных часто меняется) или вам нужно горизонтально масштабировать множество серверов без сложных JOIN-ов.

В реляционных БД обычно обсуждают нормализацию и денормализацию. Нормализация - это разбиение данных на таблицы, чтобы избежать дублирования. Есть таблицы users, orders и products. Каждый заказ ссылается на user_id и product_id вместо копирования полных данных о пользователе и продукте в каждую запись заказа. Это сохраняет согласованность (обновили имя продукта - оно обновилось везде), но требует JOIN‑ов для получения полной информации, а JOIN‑ы становятся дорогостоящими на больших объемах данных.

Денормализация - это обратный процесс: вы дублируете данные, чтобы ускорить чтение и избежать JOIN‑ов. Вместо JOIN‑а с таблицей users вы сохраняете username в каждом заказе. Теперь заказ можно отобразить без дополнительных запросов. Минус такого подхода это обновления. Если пользователь поменял имя, нужно обновить и запись в users, и все записи этого пользователя в orders. Но для систем с интенсивным чтением, где данные редко меняются, это часто оправданный компромисс.

На интервью безопасный выбор по умолчанию - начать с нормализованной реляционной модели и денормализовать только данные только под конкретные "горячие" запросы, если видите проблемы с производительностью чтения. Не предлагайте денормализацию заранее без четкой причины. Интервьюеры хотят увидеть, что вы понимаете компромиссы, а не применяете техники вслепую.

NoSQL базы заставляют мыслить иначе. Например, в DynamoDB вы проектируете ключ партиционирования (partition key) и ключ сортировки(sort key), исходя из паттернов доступа. Если вы строите соцсеть и самый частый запрос - "получить все посты пользователя X", вы делаете user_id ключом партиционирования. Тогда этот запрос становится быстрым, потому что затрагивает только один раздел. Но запрос "все посты с хэштегом Y" теперь требует сканирования всей таблицы, потому что вы не учитывали такой паттерн доступа. Поэтому нужно заранее понимать свои запросы и проектировать модель данных основываясь на них.

С деталями можно ознакомиться в статье Моделирование данных.

Индексация БД

Индексы нужны, чтобы ускорять запросы. Без индекса поиск пользователя по email означает сканирование каждой строки в таблице users. Если у вас 10 миллионов пользователей, это 10 миллионов проверок. С индексом по колонке email база может находить нужную строку за миллисекунды.

Самый распространенный индекс - B‑Tree. Он хранит данные отсортированными в виде дерева и поддерживает поиск по точному совпадению (пользователь с email X) и поиск по диапазону (заказы между датой A и датой B). Большинство реляционных БД создает B‑Tree индексы по умолчанию. Hash‑индексы быстрее для точных совпадений, но не подходят под запросы по диапазонам, поэтому используются реже. Кроме того, существуют специализированные индексы, такие как полнотекстовые (Full‑Text) для поиска по тексту (найти посты с определенными словами) и геопространственные для запросов по локации (рестораны в радиусе 5 км).

Индексы в базе данных

На собеседованиях подумайте о паттернах своих запросов и предложите индексы на столбцы, которые часто используются. Если вы ищете пользователей по email для аутентификации, проиндексируйте столбец email. Если вы получаете заказы пользователя, проиндексируйте столбец user_id в таблице заказов. Для составных запросов, таких как "найти события в Москве 19 января", вам может потребоваться составной индекс как по городу, так и по дате.

Для специализированных запросов, которые не поддерживаются основной БД, понадобятся внешние системы. Elasticsearch - стандарт для полнотекстового поиска. Для геопространственных запросов в Postgres часто используют расширение PostGIS. Обычно такие внешние индексы синхронизируются с основной БД через механизм Change Data Capture, что приводит к конечной согласованности (eventual consistency), но позволяет делать запросы, с которыми основная БД не справляется.

С полным разбором можно ознакомиться в статье Индексация БД.

Кэширование

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

Разница в производительности огромная. Попадание в кэш Redis занимает около 1 мс против 20–50 мс для типичного запроса в БД. Когда вы обслуживаете миллионы запросов, ускорение в 20–50 раз критично. Вы также снижаете нагрузку на базу, даете ей выдерживать больше записей и не масштабируете ее слишком рано.

Внешний кэш

Паттерн, который используется в 90% случаев, ленивая загрузка (cache-aside) с Redis. На чтении вы сначала проверяете кэш. Если данные есть - возвращаете их. Если нет - читаете базу данных, кладете результат в кэш с TTL и возвращаете ответ. Это легко реализуется и работает для большинства систем с интенсивным чтением.

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

Также нужно думать и об отказах кэша. Если Redis станет недоступным или откажет, все запросы внезапно пойдут в базу данных. Выдержит ли она такой всплеск нагрузки? Это называется cache stampede и может уронить всю систему. Подходы для смягчения этой проблемы включают использование небольшого внутреннего (in-process) кэша в качестве резервного варианта, использование паттерна предохранитель (circuit breaker) для предотвращения перегрузки базы данных или деградация производительности до восстановления Redis.

Частая ошибка - кэшировать все подряд. Кэшируйте только данные, которые часто читаются и редко меняются. Если вы кэшируете данные, которые меняются в каждом запросе, вы просто добавляете задержку и сложность без пользы для системы. Сначала профилируйте систему, затем кэшируйте "горячие" запросы.

Также стоит упомянуть CDN‑кэширование. Оно предназначено для статических ресурсов вроде картинок, видео и JavaScript‑файлов, которые отдаются с edge‑локаций, расположенных близко к пользователю для уменьшения задержки из-за пересылки данных на большие расстояния.

In‑process кэш подходит для небольших значений, которые редко меняются, например feature flags или конфигурации. Но для основных данных приложения по умолчанию выбирайте внешний кэш с Redis.

Подробности - в статье Кэширование, а также в описании паттерна Масштабирование чтения.

Шардирование

Шардирование появляется, когда система перерастает одну базу и нужно разделить данные на несколько независимых серверов. Это происходит из‑за ограничений по объему хранимых данных (один экземпляр Postgres упирается в несколько TB данных), по скорости записи (десятки тысяч записей в секунду) или по чтению, с которым не справляются даже реплики.

Шардирование

Самое важное решение - ключ шардирования. Он определяет, как распределяются данные, и влияет на все остальные решения при проектировании. Для приложения вроде Twitter шардирование по user_id означает, что все посты, лайки и комментарии пользователя лежат в одном шарде. Запросы, ограниченные одним пользователем быстрые, потому что обращаются к одному шарду. Но глобальные запросы, такие как "трендовые посты по всем пользователям" становятся дорогими, потому что нужно опрашивать все шарды и агрегировать результат.

Большинство систем используют шардирование основанное на хешировании, когда ключ хэшируется и через операцию деление по модулю выбирается шард. Это дает равномерное распределение и позволяет избежать "горячих точек" (hot spots). Шардирование основанное на диапазонах значений работает, если ваши паттерны доступа естественно разделяются (например, мульти-арендный SaaS, где каждая компания читает только свои данные), но легко получить "горячие точки", если один диапазон нагружен сильнее. Шардирование, основанное на каталоге использует таблицу‑справочник для определения нужного шарда. Это гибкий подход, но добавляет зависимость и задержку к каждому запросу, поэтому на интервью почти никогда не стоит этого предлагать.

Самая большая ошибка с шардированием - делать его слишком рано. Правильно сконфигурированная база с репликами выдерживает гораздо большую нагрузку, чем считают многие кандидаты. Прежде чем предлагать шардирование, сделайте оценку емкости по всем операциям. Если у вас 10k записей в секунду и 500 ГБ данных - шардирование еще не нужно. Поднимайте эту тему, когда числа реально это оправдывают, а не как стратегию по умолчанию.

Шардирование также создает новые проблемы. Транзакции между несколькими шардами почти невозможны, и вам придется проектировать границы так, чтобы их избежать. Если перевод в банковском приложении требует обновить счета на разных шардах, вам понадобятся распределенные транзакции или саги, которые сложны и медленны. "Горячие точки" возникают, когда один шард получает диспропорционально высокий трафик (например, шард Тейлор Свифт перегружен, а остальные простаивают). При этом пересоздание шардов болезненно: нельзя просто добавить новый шард, не перелив огромные объемы данных.

На интервью заводите речь про шардирование только после того, как объяснили, почему одной базы недостаточно. Затем четко назовите ключ шардирования и опишите компромисс (быстро для X запросов, медленно для Y). Этого обычно достаточно.

С полным разбором можно ознакомиться в статье Шардирование и в описании паттерна Масштабирование записи.

Согласованное хеширование

Согласованное хеширование (consistent hashing) решает конкретную проблему, которая возникает в распределенных кэшах и шардированных базах данных. При простом распределении hash(key) % N добавление или удаление сервера меняет N. В результате почти каждый ключ переезжает на другой сервер, а значит, нужно перемещать большую часть данных. С миллионами записей это катастрофа. Согласованное хеширование устраняет проблему: и сервера, и ключи размещаются на виртуальном кольце. Вы хешируете ключ и размещаете его на кольце, а данные принадлежат ближайшему серверу по часовой стрелке. Когда вы добавляете сервер, перемещаются только ключи между ним и предыдущим сервером. Когда вы удаляете сервер, его ключи переходят следующему. Все остальное остается на месте.

Согласованное хеширование

Улучшение значительное. При простом хешировании по модулю добавление одного сервера в 10‑серверный кластер требует переместить примерно 90% данных. При согласованном хешировании - только около 10% (ключи в затронутом диапазоне). Это позволяет реализовать эластичное масштабирование без массивных миграций данных.

Эта техника используется во многих местах. Распределенные кэши, такие как Memcached и Redis Cluster используют ее для распределения ключей между узлами. Распределенные базы данных, такие как Cassandra и DynamoDB, применяют ее для шардирования. Некоторые балансировщики нагрузки используют ее, чтобы стабильно назначать запросы, когда сервера добавляются или убираются. CDNs используют ее, чтобы маршрутизировать запросы к edge‑серверам.

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

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

Подробнее - в статье Согласованное хеширование.

Теорема CAP

Теорема CAP появляется, когда вы проектируете распределенные системы и выбираете как ведут себя данные при отказах. Она говорит, что вы можете одновременно обеспечить только два свойства из трех: Согласованность (consistency) (все узлы видят одни и те же данные), Доступность (availability) (каждый запрос на рабочий узел получает ответ) и Устойчивость к разделению (partition tolerance) (система работает даже при разрыве сети). Поскольку разрывы сети неизбежны, выбор фактически происходит между согласованностью и доступностью.

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

Теорема CAP

Для большинства систем доступность - правильный выбор по умолчанию. Пользователи могут терпеть слегка устаревшие данные (лента Twitter на 2 секунды старее), но они не терпят недоступность приложения. Ленты соцсетей, рекомендательные системы и аналитические дашборды отлично работают с конечной согласованностью (eventual consistency).

Сильная согласованность важна, когда устаревшие данные приводят к реальным бизнес‑проблемам. Инвентарные системы должны точно считать остатки, иначе вы продадите больше товаров, чем есть на складе. Банковские системы должны корректно считать баланс, иначе возможны мошеннические операции. Системы бронирования, такие как Ticketmaster, должны не допускать двойной продажи одного и того же места. Это те случаи, где чтение устаревших данных даже на несколько секунд может стоить денег или стать причиной неприятного пользовательского опыта.

Не обязательно выбирать одну модель для всей системы. Часто разные части одной системы имеют разные требования к согласованности. В e‑commerce описания продуктов и отзывы могут быть рассчитаны на конечную согласованность, (ничего страшного, если отзыв появится через 5 секунд), а подсчет остатков и обработка заказов должны быть строго согласованными.

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

С деталями можно ознакомиться в статье Теорема CAP.

Важные числа

Как обсуждалось в Структуре интервью, мы рекомендуем не делать оценки "на салфетке" в начале интервью. Важно делать такие оценки только тогда, когда они влияют на решение. Нужно ли шардировать базу? Потянет ли один Redis узел нагрузку? На такие вопросы нельзя ответить без численных оценок.

Сложность в том, чтобы понимать, какие числа использовать. Современное железо значительно мощнее, чем думают многие кандидаты. Хорошо настроенный сервер базы данных обслуживает десятки тысяч запросов в секунду. Один Redis узел выдерживает сотни тысяч операций в секунду. Если вы опираетесь на цифры 2010‑х годов, вы начнете предлагать шардирование и кэширование раньше, чем нужно.

Начните с задержек доступа к данным, потому что они влияют почти на каждое решение. Доступ к оперативной памяти занимает наносекунды. Чтение SSD - микросекунды. Сетевые вызовы внутри дата центра занимают 1–10 мс. Межконтинентальные вызовы - десятки и сотни миллисекунд.

Если интервьюер спрашивает "сколько серверов нам нужно", сделайте расчет: "ожидаем 50k запросов в секунду, один сервер выдержит примерно 5k, значит нужно около 10 серверов плюс резерв". Интервьюер хочет увидеть ход ваших мыслей, а не услышать список заученных фактов.

Объемы хранения важны для принятия решений о шардировании. Один экземпляр Postgres комфортно выдерживает несколько TB данных. Шардирование не нужно, пока вы не достигли десятков или сотен TB данных. Если кто‑то предлагает шардирование при 500 GB, он добавляет сложность без реальной необходимости.

КомпонентКлючевые метрикиТриггеры масштабирования
Кэширование- ~1 мс задержки
- 100k+ операций/сек
- Ограничение по памяти (до 1 ТБ)
- Hit rate < 80%
- Задержка > 1 мс
- Использование памяти > 80%
Базы данных- До 50k транзакций/сек
- Задержка чтения < 5 мс (с кэшем)
- Емкость хранения 32 TB+
- Пропускная способность записи > 10k транзакций/сек
- Задержка чтения > 5 мс без кэша
- Нужна геораспределенность
Серверы приложений- 100k+ одновременных соединений
- 8–64 ядер @ 2–4 ГГц
- 64–512 ГБ RAM стандартно, до 2 ТБ
- CPU > 70% загрузки
- Задержка ответа > SLA
- Соединения ~100k/узел
- Память > 80%
Очереди сообщений- До 1 млн сообщений/сек на брокер
- Сквозная задержка < 5 мс
- До 50 ТБ хранения
- Пропускная способность ~800k сообщений/сек
- Число разделов ~200k на кластер
- Растущее отставание потребителей

С полным разбором можно ознакомиться в статье Важные числа.

Войдите чтобы отмечать прогресс