System Design часто воспринимают как набор знакомых технологий: Redis, Kafka, Docker, Kubernetes, микросервисы, балансировщики. Но если просто добавить их на архитектурную схему, система от этого лучше не станет.
System Design — это, прежде всего, про принятие инженерных решений. Нужно понять, что должна делать система, какую нагрузку она будет выдерживать, где могут возникнуть узкие места и что произойдет, если отдельные компоненты начнут работать медленно или вообще перестанут отвечать.
Разберем, как подойти к проектированию системы на Python от первых требований до масштабирования и отказоустойчивости.
System Design — это, прежде всего, про принятие инженерных решений. Нужно понять, что должна делать система, какую нагрузку она будет выдерживать, где могут возникнуть узкие места и что произойдет, если отдельные компоненты начнут работать медленно или вообще перестанут отвечать.
Разберем, как подойти к проектированию системы на Python от первых требований до масштабирования и отказоустойчивости.
Сначала требования, потом архитектура
Одна из самых частых ошибок — сразу начинать рисовать схему из сервисов, баз данных и очередей, не ответив на главный вопрос: что именно мы проектируем?
Представим, что нужно сделать сервис сокращения ссылок. Пользователь отправляет длинный URL и получает короткий. На этом описание заканчивается, но для System Design этого недостаточно.
Нужно понять:
Именно эти параметры определяют архитектуру.
Например, если сервис получает 100 запросов на создание ссылок в секунду и 10 000 переходов, перед нами явно read-heavy система. Значит, архитектуру стоит оптимизировать прежде всего под чтение: подумать о кэшировании, эффективных индексах и масштабировании read-нагрузки.
Главный принцип: сначала определяем требования и ограничения, а уже потом выбираем технологии.
Функциональные и нефункциональные требования
Функциональные требования отвечают на вопрос «что система делает?»:
Нефункциональные — «как система должна это делать?»:
Без второй группы требований невозможно нормально спроектировать систему. Сервис на 100 пользователей и сервис на 100 миллионов пользователей могут решать одну и ту же бизнес-задачу, но архитектура у них будет совершенно разной.
Представим, что нужно сделать сервис сокращения ссылок. Пользователь отправляет длинный URL и получает короткий. На этом описание заканчивается, но для System Design этого недостаточно.
Нужно понять:
- Сколько пользователей будет у сервиса;
- Сколько ссылок создается в секунду;
- Сколько раз в секунду открываются уже созданные ссылки;
- Какой допустимый latency;
- Сколько времени должны храниться ссылки;
- Нужна ли статистика переходов;
- Насколько критична доступность сервиса.
Именно эти параметры определяют архитектуру.
Например, если сервис получает 100 запросов на создание ссылок в секунду и 10 000 переходов, перед нами явно read-heavy система. Значит, архитектуру стоит оптимизировать прежде всего под чтение: подумать о кэшировании, эффективных индексах и масштабировании read-нагрузки.
Главный принцип: сначала определяем требования и ограничения, а уже потом выбираем технологии.
Функциональные и нефункциональные требования
Функциональные требования отвечают на вопрос «что система делает?»:
- Пользователь создает ссылку;
- Пользователь открывает короткую ссылку;
- Пользователь получает статистику.
Нефункциональные — «как система должна это делать?»:
- Выдерживать 10 000 RPS;
- Отвечать быстрее 200 мс;
- Иметь доступность 99,9%;
- Переживать отказ одного экземпляра приложения;
- Хранить определенный объем данных.
Без второй группы требований невозможно нормально спроектировать систему. Сервис на 100 пользователей и сервис на 100 миллионов пользователей могут решать одну и ту же бизнес-задачу, но архитектура у них будет совершенно разной.
Оцениваем нагрузку
Точные цифры на этапе проектирования есть далеко не всегда. Поэтому используют приблизительные расчеты. Главное, определить порядок величин.
Допустим, у приложения 1 миллион активных пользователей в месяц, каждый делает в среднем 10 запросов в день. Это около 10 миллионов запросов в сутки, или примерно 116 RPS в среднем.
Но ориентироваться только на среднее значение опасно. Если в пиковые часы нагрузка увеличивается в десять раз, системе уже потребуется выдерживать порядка 1 000–1 200 RPS.
При этом RPS — далеко не единственный показатель.
Для разных компонентов важны разные параметры:
Поэтому вопрос «сколько серверов нам понадобится?» стоит задавать только после оценки нагрузки и поиска потенциальных bottleneck’ов.
Допустим, у приложения 1 миллион активных пользователей в месяц, каждый делает в среднем 10 запросов в день. Это около 10 миллионов запросов в сутки, или примерно 116 RPS в среднем.
Но ориентироваться только на среднее значение опасно. Если в пиковые часы нагрузка увеличивается в десять раз, системе уже потребуется выдерживать порядка 1 000–1 200 RPS.
При этом RPS — далеко не единственный показатель.
Для разных компонентов важны разные параметры:
- API — requests per second и latency;
- БД — количество чтений/записей, размер данных, сложность запросов;
- Кэш — количество запросов и объем рабочего набора;
- Очередь — скорость поступления и обработки сообщений;
- Хранилище — количество и размер объектов.
Поэтому вопрос «сколько серверов нам понадобится?» стоит задавать только после оценки нагрузки и поиска потенциальных bottleneck’ов.
High-Level Design: определяем границы системы
Когда требования понятны, можно переходить к высокоуровневой архитектуре.
Типичный Python web-сервис может выглядеть примерно так:
Client → Load Balancer → Python API → Cache / Database / External Services
Но важнее не сама схема, а ответственность каждого компонента и обоснованность архитектурных решений.
Например, в интернет-магазине можно выделить каталог товаров, пользователей, заказы и платежи. И здесь возникает важный вопрос: нужно ли превращать каждый из них в отдельный микросервис?
Не обязательно.
Для многих проектов разумнее начать с модульного монолита: приложение остается единым с точки зрения деплоя, но внутри него четко разделены доменные модули. Это позволяет не усложнять систему раньше времени и при необходимости позже выделить отдельные компоненты.
Есть и другая распространенная ошибка: разработчики сами создают highload, а потом сами же героически с ним борются. Например, добавляют лишние сетевые взаимодействия, дробят простой монолит на десятки сервисов, создают тяжелые цепочки запросов, а затем оптимизируют их, добавляют кэши, очереди, реплики и прочую инфраструктуру, чтобы вернуть систему примерно к тому состоянию, в котором она могла работать изначально.
Поэтому хороший System Design — это не только способность масштабировать систему, но и умение не создавать лишнюю нагрузку своими архитектурными решениями.
Модульность важнее количества микросервисов.
Если код заказов напрямую зависит от внутренней реализации платежей, перенос этих компонентов в два отдельных сервиса проблему не решит. Связанность просто переедет из кода в сетевые вызовы.
Типичный Python web-сервис может выглядеть примерно так:
Client → Load Balancer → Python API → Cache / Database / External Services
Но важнее не сама схема, а ответственность каждого компонента и обоснованность архитектурных решений.
Например, в интернет-магазине можно выделить каталог товаров, пользователей, заказы и платежи. И здесь возникает важный вопрос: нужно ли превращать каждый из них в отдельный микросервис?
Не обязательно.
Для многих проектов разумнее начать с модульного монолита: приложение остается единым с точки зрения деплоя, но внутри него четко разделены доменные модули. Это позволяет не усложнять систему раньше времени и при необходимости позже выделить отдельные компоненты.
Есть и другая распространенная ошибка: разработчики сами создают highload, а потом сами же героически с ним борются. Например, добавляют лишние сетевые взаимодействия, дробят простой монолит на десятки сервисов, создают тяжелые цепочки запросов, а затем оптимизируют их, добавляют кэши, очереди, реплики и прочую инфраструктуру, чтобы вернуть систему примерно к тому состоянию, в котором она могла работать изначально.
Поэтому хороший System Design — это не только способность масштабировать систему, но и умение не создавать лишнюю нагрузку своими архитектурными решениями.
Модульность важнее количества микросервисов.
Если код заказов напрямую зависит от внутренней реализации платежей, перенос этих компонентов в два отдельных сервиса проблему не решит. Связанность просто переедет из кода в сетевые вызовы.
Где здесь Python?
Python отлично подходит для API, интеграций, backend-сервисов и большого количества I/O-bound задач. Но при проектировании важно понимать ограничения конкретного стека.
Например, для web-приложения можно использовать Django или FastAPI, а дальше уже отдельно решать вопросы:
Фреймворк — это только один компонент архитектуры.
Даже очень быстрый API не поможет, если каждый запрос ждет медленную базу данных или внешний сервис.
Например, для web-приложения можно использовать Django или FastAPI, а дальше уже отдельно решать вопросы:
- Сколько экземпляров приложения запускать;
- Как распределять запросы;
- Где хранить состояние;
- Какие операции выполнять синхронно;
- Какие выносить в background workers.
Фреймворк — это только один компонент архитектуры.
Даже очень быстрый API не поможет, если каждый запрос ждет медленную базу данных или внешний сервис.
Масштабирование: сначала найди bottleneck
Самая очевидная идея при росте нагрузки — добавить серверов.
Если Python-приложение действительно упирается в CPU или количество доступных worker’ов, горизонтальное масштабирование может помочь:
1 instance → 2 → 4 → 8
Но представим другую ситуацию. У нас уже десять экземпляров API, а все они обращаются к одной базе, которая работает на пределе. Добавление еще десяти Python-инстансов не ускорит систему. Наоборот, база получит еще больше запросов.
Поэтому при масштабировании нужно смотреть на всю цепочку:
Client → Load Balancer → API → Cache → Database → External Services
И задавать вопрос: где именно находится ограничение?
Вертикальное и горизонтальное масштабирование
Для stateless-сервисов горизонтальное масштабирование обычно удобнее. Если состояние не хранится внутри конкретного экземпляра, новый сервер можно просто добавить за балансировщик.
Отсюда появляется важный архитектурный принцип:
Состояние лучше выносить из экземпляров приложения.
Сессии можно хранить во внешнем хранилище, файлы — в object storage, постоянные данные — в БД, часто используемые значения — в кэше.
Если Python-приложение действительно упирается в CPU или количество доступных worker’ов, горизонтальное масштабирование может помочь:
1 instance → 2 → 4 → 8
Но представим другую ситуацию. У нас уже десять экземпляров API, а все они обращаются к одной базе, которая работает на пределе. Добавление еще десяти Python-инстансов не ускорит систему. Наоборот, база получит еще больше запросов.
Поэтому при масштабировании нужно смотреть на всю цепочку:
Client → Load Balancer → API → Cache → Database → External Services
И задавать вопрос: где именно находится ограничение?
Вертикальное и горизонтальное масштабирование
- Вертикальное масштабирование — увеличиваем ресурсы одной машины: CPU, RAM, диск.
- Горизонтальное — добавляем новые экземпляры приложения.
Для stateless-сервисов горизонтальное масштабирование обычно удобнее. Если состояние не хранится внутри конкретного экземпляра, новый сервер можно просто добавить за балансировщик.
Отсюда появляется важный архитектурный принцип:
Состояние лучше выносить из экземпляров приложения.
Сессии можно хранить во внешнем хранилище, файлы — в object storage, постоянные данные — в БД, часто используемые значения — в кэше.
Повышаем отзывчивость: не все операции должны выполняться в HTTP-запросе
Представим сервис обработки изображений. Пользователь загружает файл, а обработка занимает несколько секунд.
Если выполнять все непосредственно внутри HTTP-запроса, пользователь будет ждать несколько секунд, а web-приложение будет держать ресурсы занятыми.
Гораздо лучше разделить процесс:
API → Queue → Worker → Storage
API принимает запрос и быстро возвращает пользователю статус задачи. Worker забирает задачу из очереди и выполняет тяжелую обработку отдельно.
Этот же подход подходит для:
Но очередь добавляет свои проблемы: нужно учитывать повторную доставку сообщений, порядок обработки, ошибки и повторный запуск задач.
Поэтому здесь особенно важна идемпотентность.
Если одна и та же задача будет обработана дважды, результат не должен привести к неконтролируемому побочному эффекту.
Если выполнять все непосредственно внутри HTTP-запроса, пользователь будет ждать несколько секунд, а web-приложение будет держать ресурсы занятыми.
Гораздо лучше разделить процесс:
API → Queue → Worker → Storage
API принимает запрос и быстро возвращает пользователю статус задачи. Worker забирает задачу из очереди и выполняет тяжелую обработку отдельно.
Этот же подход подходит для:
- Отправки email;
- Генерации отчетов;
- Обработки изображений и видео;
- Импорта больших файлов;
- Синхронизации с внешними сервисами.
Но очередь добавляет свои проблемы: нужно учитывать повторную доставку сообщений, порядок обработки, ошибки и повторный запуск задач.
Поэтому здесь особенно важна идемпотентность.
Если одна и та же задача будет обработана дважды, результат не должен привести к неконтролируемому побочному эффекту.
Async в Python: где он действительно помогает
asyncio используется для конкурентного выполнения задач, которые большую часть времени проводят в ожидании I/O: сетевых запросов, ответов от внешних API, операций с БД и других подобных операций.
Например, если API должно получить данные сразу из нескольких независимых сервисов, асинхронный подход позволяет не ждать каждый ответ последовательно: пока одна операция ожидает данные от сети, event loop может заниматься другой.
Но здесь есть принципиальное ограничение: для CPU-bound задач асинхронность сама по себе ничего не ускоряет. Если код занят тяжелым вычислением, event loop блокируется, и остальные корутины не смогут нормально выполняться.
В таком случае работу обычно выносят из event loop: например, в отдельные процессы или фоновые workers. При этом важно различать две задачи: конкурентность и ускорение вычислений — не одно и то же. asyncio помогает эффективнее использовать время ожидания I/O, но не добавляет процессору вычислительной мощности.
Поэтому выбор между синхронным и асинхронным подходом определяется прежде всего характером нагрузки, а не тем, что «async быстрее».
Например, если API должно получить данные сразу из нескольких независимых сервисов, асинхронный подход позволяет не ждать каждый ответ последовательно: пока одна операция ожидает данные от сети, event loop может заниматься другой.
Но здесь есть принципиальное ограничение: для CPU-bound задач асинхронность сама по себе ничего не ускоряет. Если код занят тяжелым вычислением, event loop блокируется, и остальные корутины не смогут нормально выполняться.
В таком случае работу обычно выносят из event loop: например, в отдельные процессы или фоновые workers. При этом важно различать две задачи: конкурентность и ускорение вычислений — не одно и то же. asyncio помогает эффективнее использовать время ожидания I/O, но не добавляет процессору вычислительной мощности.
Поэтому выбор между синхронным и асинхронным подходом определяется прежде всего характером нагрузки, а не тем, что «async быстрее».
Кэширование: самый быстрый запрос — тот, которого нет
Если одни и те же данные читаются тысячи раз, обращаться к основной базе для каждого запроса нерационально.
Например, карточку популярного товара могут запрашивать тысячи пользователей, хотя данные о товаре меняются несколько раз в день.
Здесь помогает кэш.
Упрощенный сценарий выглядит так:
API → Cache → если данных нет → Database → Cache
Но у кэша есть цена — сложность актуализации данных.
Если цена товара изменилась в БД, а старое значение осталось в Redis, пользователь некоторое время может получить устаревшую информацию.
Поэтому нужно заранее определить:
Кэш должен ускорять систему, а не становиться единственным источником истины.
Например, карточку популярного товара могут запрашивать тысячи пользователей, хотя данные о товаре меняются несколько раз в день.
Здесь помогает кэш.
Упрощенный сценарий выглядит так:
API → Cache → если данных нет → Database → Cache
Но у кэша есть цена — сложность актуализации данных.
Если цена товара изменилась в БД, а старое значение осталось в Redis, пользователь некоторое время может получить устаревшую информацию.
Поэтому нужно заранее определить:
- Сколько времени данные могут быть неактуальными;
- Когда кэш инвалидируется;
- Какой TTL использовать;
- Что делать при недоступности кэша.
Кэш должен ускорять систему, а не становиться единственным источником истины.
База данных: ищем проблему не только в Python
Очень часто разработчик видит медленный API и начинает оптимизировать Python-код. Но проблема может находиться гораздо глубже.
Например, endpoint получает список заказов пользователя, а SQL-запрос делает тяжелые JOIN, сортировки и вычисления по миллионам строк. В такой ситуации оптимизация Python почти ничего не даст.
Нужно посмотреть на:
SQL → индексы → план выполнения → объем данных → структуру таблиц
Когда одной БД становится недостаточно, появляются более сложные решения: read replicas, partitioning, шардирование и другие способы распределения нагрузки.
Но здесь важно не попасть в другую ловушку — не масштабировать базу раньше времени.
Если обычная PostgreSQL с хорошими индексами и нормально написанными запросами справляется с нагрузкой, сложная распределенная схема может только увеличить количество проблем.
Например, endpoint получает список заказов пользователя, а SQL-запрос делает тяжелые JOIN, сортировки и вычисления по миллионам строк. В такой ситуации оптимизация Python почти ничего не даст.
Нужно посмотреть на:
SQL → индексы → план выполнения → объем данных → структуру таблиц
Когда одной БД становится недостаточно, появляются более сложные решения: read replicas, partitioning, шардирование и другие способы распределения нагрузки.
Но здесь важно не попасть в другую ловушку — не масштабировать базу раньше времени.
Если обычная PostgreSQL с хорошими индексами и нормально написанными запросами справляется с нагрузкой, сложная распределенная схема может только увеличить количество проблем.
Отказоустойчивость: проектируем не только happy path
Система должна работать не только тогда, когда всё идеально.
Что произойдет, если:
Для внешних вызовов нужны timeout’ы. Иногда применяются retries, но бесконечно повторять запрос нельзя: при проблеме downstream-сервиса это способно только увеличить нагрузку.
В распределенных системах также применяются:
Например, если внешний сервис стабильно выдерживает 500 RPS, нет смысла отправлять ему 5 000 запросов в секунду в надежде, что ситуация somehow исправится.
Что произойдет, если:
- База временно недоступна;
- Внешний API отвечает пять секунд;
- Один экземпляр приложения упал;
- Очередь начала быстро заполняться;
- Redis перестал отвечать?
Для внешних вызовов нужны timeout’ы. Иногда применяются retries, но бесконечно повторять запрос нельзя: при проблеме downstream-сервиса это способно только увеличить нагрузку.
В распределенных системах также применяются:
- Rate limiting — ограничивает поток запросов.
- Circuit breaker — временно прекращает вызовы проблемного сервиса.
- Backpressure — не позволяет системе принимать больше работы, чем она способна обработать.
Например, если внешний сервис стабильно выдерживает 500 RPS, нет смысла отправлять ему 5 000 запросов в секунду в надежде, что ситуация somehow исправится.
Наблюдаемость — часть архитектуры
Систему недостаточно построить. Нужно понимать, что с ней происходит в production.
Минимальный набор обычно включает метрики, логи и tracing.
Нас интересуют не только ошибки, но и:
Особенно полезны percentiles вроде p95 и p99. Средняя latency может выглядеть прекрасно, пока небольшой процент пользователей ждет ответа несколько секунд.
А distributed tracing помогает понять, на каком именно участке цепочки возникла задержка:
API → Service A → Service B → Database
Вместо предположения «Python работает медленно» можно увидеть, например, что 80% времени запрос провел в одном внешнем API.
Минимальный набор обычно включает метрики, логи и tracing.
Нас интересуют не только ошибки, но и:
- latency;
- RPS;
- CPU и memory;
- Количество соединений с БД;
- cache hit/miss;
- Длина очередей;
- Количество retry;
- Ошибки внешних сервисов.
Особенно полезны percentiles вроде p95 и p99. Средняя latency может выглядеть прекрасно, пока небольшой процент пользователей ждет ответа несколько секунд.
А distributed tracing помогает понять, на каком именно участке цепочки возникла задержка:
API → Service A → Service B → Database
Вместо предположения «Python работает медленно» можно увидеть, например, что 80% времени запрос провел в одном внешнем API.
Безопасность тоже проектируется заранее
Производительность — не единственная характеристика хорошей системы.
На уровне архитектуры нужно учитывать:
Например, токены и пароли не должны попадать в логи только потому, что разработчику так удобнее отлаживать запросы.
Безопасность — не отдельный этап после разработки. Она является частью System Design.
На уровне архитектуры нужно учитывать:
- Аутентификацию и авторизацию;
- rate limiting;
- Валидацию входных данных;
- Управление секретами;
- Шифрование соединений;
- Права доступа между сервисами;
- Защиту персональных данных;
- Безопасное логирование.
Например, токены и пароли не должны попадать в логи только потому, что разработчику так удобнее отлаживать запросы.
Безопасность — не отдельный этап после разработки. Она является частью System Design.
А теперь собираем всё вместе
Представим, что мы проектируем сервис, который принимает пользовательские документы и формирует по ним отчет.
Наивная реализация может выглядеть так:
Client → Python API → Database
Но если генерация отчета занимает 20 секунд, пользователи начинают ждать, а API быстро упирается в ограничение по worker’ам.
После анализа требований архитектура может превратиться в:
Client → Load Balancer → Python API → Queue → Workers
При этом:
Каждый компонент появился потому, что решает конкретную проблему.
Наивная реализация может выглядеть так:
Client → Python API → Database
Но если генерация отчета занимает 20 секунд, пользователи начинают ждать, а API быстро упирается в ограничение по worker’ам.
После анализа требований архитектура может превратиться в:
Client → Load Balancer → Python API → Queue → Workers
При этом:
- PostgreSQL хранит пользователей, документы и статусы задач;
- Redis используется для кэширования часто запрашиваемых данных;
- object storage хранит сами файлы;
- workers занимаются генерацией отчетов;
- API остается stateless и масштабируется горизонтально;
- Мониторинг отслеживает latency, ошибки и длину очереди.
Каждый компонент появился потому, что решает конкретную проблему.
System Design — это не про то, сколько технологий вы можете назвать на собеседовании.
Хороший дизайн начинается с требований и оценки нагрузки. Затем определяются границы компонентов, выбирается способ хранения данных и взаимодействия между ними. После этого рассматриваются масштабирование, кэширование, асинхронная обработка, отказоустойчивость, наблюдаемость и безопасность.
Для Python-разработчика особенно важно понимать, где заканчивается ответственность приложения и начинается архитектура всей системы.
FastAPI или Django не сделают систему масштабируемой сами по себе. asyncio не решит CPU-bound задачи. Redis не заменит базу данных. Kafka не нужна просто потому, что она есть в популярных архитектурных схемах.
Каждое техническое решение должно отвечать на конкретный вопрос:
Какую проблему мы решаем и что получаем взамен?
Именно с этого начинается настоящий System Design.
Хотите узнать больше? Изучите другие статьи из раздела:
Хороший дизайн начинается с требований и оценки нагрузки. Затем определяются границы компонентов, выбирается способ хранения данных и взаимодействия между ними. После этого рассматриваются масштабирование, кэширование, асинхронная обработка, отказоустойчивость, наблюдаемость и безопасность.
Для Python-разработчика особенно важно понимать, где заканчивается ответственность приложения и начинается архитектура всей системы.
FastAPI или Django не сделают систему масштабируемой сами по себе. asyncio не решит CPU-bound задачи. Redis не заменит базу данных. Kafka не нужна просто потому, что она есть в популярных архитектурных схемах.
Каждое техническое решение должно отвечать на конкретный вопрос:
Какую проблему мы решаем и что получаем взамен?
Именно с этого начинается настоящий System Design.
Хотите узнать больше? Изучите другие статьи из раздела: