Статьи

От API до отказоустойчивости: разбираемся с System Design на Python

System Design часто воспринимают как набор знакомых технологий: Redis, Kafka, Docker, Kubernetes, микросервисы, балансировщики. Но если просто добавить их на архитектурную схему, система от этого лучше не станет.

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

Разберем, как подойти к проектированию системы на Python от первых требований до масштабирования и отказоустойчивости.

Сначала требования, потом архитектура

Одна из самых частых ошибок — сразу начинать рисовать схему из сервисов, баз данных и очередей, не ответив на главный вопрос: что именно мы проектируем?
Представим, что нужно сделать сервис сокращения ссылок. Пользователь отправляет длинный 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 — далеко не единственный показатель.

Для разных компонентов важны разные параметры:

  • API — requests per second и latency;
  • БД — количество чтений/записей, размер данных, сложность запросов;
  • Кэш — количество запросов и объем рабочего набора;
  • Очередь — скорость поступления и обработки сообщений;
  • Хранилище — количество и размер объектов.

Поэтому вопрос «сколько серверов нам понадобится?» стоит задавать только после оценки нагрузки и поиска потенциальных bottleneck’ов.

High-Level 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, а дальше уже отдельно решать вопросы:

  • Сколько экземпляров приложения запускать;
  • Как распределять запросы;
  • Где хранить состояние;
  • Какие операции выполнять синхронно;
  • Какие выносить в background workers.

Фреймворк — это только один компонент архитектуры.

Даже очень быстрый API не поможет, если каждый запрос ждет медленную базу данных или внешний сервис.

Масштабирование: сначала найди bottleneck

Самая очевидная идея при росте нагрузки — добавить серверов.
Если 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 забирает задачу из очереди и выполняет тяжелую обработку отдельно.
Этот же подход подходит для:

  • Отправки email;
  • Генерации отчетов;
  • Обработки изображений и видео;
  • Импорта больших файлов;
  • Синхронизации с внешними сервисами.

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

Async в Python: где он действительно помогает

asyncio используется для конкурентного выполнения задач, которые большую часть времени проводят в ожидании I/O: сетевых запросов, ответов от внешних API, операций с БД и других подобных операций.
Например, если API должно получить данные сразу из нескольких независимых сервисов, асинхронный подход позволяет не ждать каждый ответ последовательно: пока одна операция ожидает данные от сети, event loop может заниматься другой.

Но здесь есть принципиальное ограничение: для CPU-bound задач асинхронность сама по себе ничего не ускоряет. Если код занят тяжелым вычислением, event loop блокируется, и остальные корутины не смогут нормально выполняться.
В таком случае работу обычно выносят из event loop: например, в отдельные процессы или фоновые workers. При этом важно различать две задачи: конкурентность и ускорение вычислений — не одно и то же. asyncio помогает эффективнее использовать время ожидания I/O, но не добавляет процессору вычислительной мощности.

Поэтому выбор между синхронным и асинхронным подходом определяется прежде всего характером нагрузки, а не тем, что «async быстрее».

Кэширование: самый быстрый запрос — тот, которого нет

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

API → Cache → если данных нет → Database → Cache

Но у кэша есть цена — сложность актуализации данных.
Если цена товара изменилась в БД, а старое значение осталось в Redis, пользователь некоторое время может получить устаревшую информацию.
Поэтому нужно заранее определить:

  • Сколько времени данные могут быть неактуальными;
  • Когда кэш инвалидируется;
  • Какой TTL использовать;
  • Что делать при недоступности кэша.

Кэш должен ускорять систему, а не становиться единственным источником истины.

База данных: ищем проблему не только в Python

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

Например, endpoint получает список заказов пользователя, а SQL-запрос делает тяжелые JOIN, сортировки и вычисления по миллионам строк. В такой ситуации оптимизация Python почти ничего не даст.
Нужно посмотреть на:

SQL → индексы → план выполнения → объем данных → структуру таблиц

Когда одной БД становится недостаточно, появляются более сложные решения: read replicas, partitioning, шардирование и другие способы распределения нагрузки.
Но здесь важно не попасть в другую ловушку — не масштабировать базу раньше времени.
Если обычная PostgreSQL с хорошими индексами и нормально написанными запросами справляется с нагрузкой, сложная распределенная схема может только увеличить количество проблем.

Отказоустойчивость: проектируем не только happy path

Система должна работать не только тогда, когда всё идеально.
Что произойдет, если:

  • База временно недоступна;
  • Внешний API отвечает пять секунд;
  • Один экземпляр приложения упал;
  • Очередь начала быстро заполняться;
  • Redis перестал отвечать?

Для внешних вызовов нужны timeout’ы. Иногда применяются retries, но бесконечно повторять запрос нельзя: при проблеме downstream-сервиса это способно только увеличить нагрузку.

В распределенных системах также применяются:

  • Rate limiting — ограничивает поток запросов.
  • Circuit breaker — временно прекращает вызовы проблемного сервиса.
  • Backpressure — не позволяет системе принимать больше работы, чем она способна обработать.

Например, если внешний сервис стабильно выдерживает 500 RPS, нет смысла отправлять ему 5 000 запросов в секунду в надежде, что ситуация somehow исправится.

Наблюдаемость — часть архитектуры

Систему недостаточно построить. Нужно понимать, что с ней происходит в production.
Минимальный набор обычно включает метрики, логи и tracing.
Нас интересуют не только ошибки, но и:

  • latency;
  • RPS;
  • CPU и memory;
  • Количество соединений с БД;
  • cache hit/miss;
  • Длина очередей;
  • Количество retry;
  • Ошибки внешних сервисов.

Особенно полезны percentiles вроде p95 и p99. Средняя latency может выглядеть прекрасно, пока небольшой процент пользователей ждет ответа несколько секунд.
А distributed tracing помогает понять, на каком именно участке цепочки возникла задержка:

API → Service A → Service B → Database

Вместо предположения «Python работает медленно» можно увидеть, например, что 80% времени запрос провел в одном внешнем API.

Безопасность тоже проектируется заранее

Производительность — не единственная характеристика хорошей системы.
На уровне архитектуры нужно учитывать:

  • Аутентификацию и авторизацию;
  • rate limiting;
  • Валидацию входных данных;
  • Управление секретами;
  • Шифрование соединений;
  • Права доступа между сервисами;
  • Защиту персональных данных;
  • Безопасное логирование.

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

А теперь собираем всё вместе

Представим, что мы проектируем сервис, который принимает пользовательские документы и формирует по ним отчет.
Наивная реализация может выглядеть так:

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.


Хотите узнать больше? Изучите другие статьи из раздела:
2026-08-26 15:00 Python