Изучите проектирование платёжных систем: горизонтальное масштабирование, гонки, идемпотентность. Практические паттерны для собеседований и реальных задач.
Представьте: день зарплаты, 2 часа дня. Миллионы людей одновременно оплачивают аренду, переводят деньги родным, гасят кредитки и покупают в интернете. Вопрос с системного интервью: если миллионы запросов приходят практически одновременно, неужели каждый из них попадает на один центральный сервер? Что мешает всей платёжной системе зависнуть? На первый взгляд кажется, что это проблема масштабирования. Но на самом деле это комбинация: горизонтального масштабирования, конкурентности, распределённых систем, согласованности баз данных, повторных попыток, идемпотентности, обратного давления, изоляции сбоев и узких мест в downstream-сервисах. Именно поэтому проектирование платёжных систем — такая интересная задача.
Часто новички представляют платёжную систему так: миллионы пользователей -> один UPI-сервер -> банк. Если бы это было правдой, мы бы столкнулись с серьёзной проблемой: одна машина физически не может безопасно обработать весь платёжный трафик страны. Вместо этого нужно думать о распределённой системе: пользователи -> API/шлюз -> несколько серверов (S1, S2, S3) -> платёжные сервисы -> банки. Реальная реализация сложнее, но это правильная ментальная модель. Ключевая идея: система распределена между множеством машин и организаций.
Допустим, вы платите арендодателю 25 000 рублей. В тот же момент миллионы других людей делают похожие операции. Нормальный трафик — 100K запросов в секунду, в день зарплаты — более 1M. Как справиться с дополнительной нагрузкой?
Можно купить огромную машину: 256 ядер CPU, 2 ТБ RAM. Это вертикальное масштабирование. Но есть пределы: CPU, память, сеть, количество соединений. И главная проблема: если сервер умирает, вся система падает. Для платёжной системы это неприемлемо.
Вместо одной огромной машины добавляем много обычных. Перед ними ставим балансировщик нагрузки. Если один сервер обрабатывает 20 000 запросов в секунду, для 1 миллиона нужно примерно 50 серверов. Теперь, если один сервер выходит из строя, трафик перенаправляется на здоровые. Это первый паттерн: горизонтальное масштабирование — когда объём запросов превышает возможности одной машины, распределяйте запросы между множеством машин.
Представьте, что два платежа приходят одновременно: Harsh -> арендодателю 25 000 и Harsh -> Amazon 20 000. Они попадают на разные серверы. У Harsh на счету 30 000. Оба сервера читают баланс одновременно, видят 30 000, затем списывают: один получает 5 000, другой 10 000. В итоге система позволила потратить 45 000 при балансе 30 000. Это гонка. И здесь проектирование платёжных систем становится интересным.
Платёж — это не просто UPDATE balance. Концептуально нужно: проверить баланс, верифицировать платёж, списать со счёта отправителя, зачислить получателю, записать транзакцию. Критический переход состояния должен быть безопасным при конкурентном доступе. Нужна гарантия, что две конкурирующие операции не смогут некорректно изменить одни и те же финансовые данные. В зависимости от архитектуры это могут быть транзакции БД, блокировки, оптимистичная блокировка, сериализация, владение партициями, тщательно спроектированные конечные автоматы. Главный урок для интервью: не просто «используйте блокировки БД», а «определите общее изменяемое состояние и защитите критический переход».
Допустим, мы успешно масштабировали серверы приложений: 500 серверов, но одна база данных. Все серверы борются за один ресурс. Приложение масштабируется, база — нет. Это классическое узкое место распределённых систем: самый быстрый компонент не имеет значения, если более медленная общая зависимость ограничивает всю систему.
Вместо того чтобы прогонять всё через одну БД, мы можем разделить данные и нагрузку. Концептуально: платёжные запросы -> партиции A и B -> БД-A и БД-B. Стратегия разделения может быть по ID аккаунта, банку, ID клиента, домену транзакции, географическому региону. Это паттерн №2: шардирование (партиционирование) — когда один ресурс не справляется, разделите нагрузку на независимые партиции.
Наши серверы приложений и базы данных здоровы, но downstream-банк перегружен. Наша система может обрабатывать миллионы входящих запросов, но зависимый сервис — лишь меньший объём. Отсюда принцип: пропускная способность распределённой системы ограничена её критическими узкими местами и зависимостями. Нельзя решить проблему downstream, просто добавив серверы приложений.
Если компонент безопасно обрабатывает 100K операций в секунду, а мы получаем 300K, и мы слепо всё пересылаем, компонент падает. Вместо этого для асинхронной работы можно ввести буфер: очередь, которая поглощает временные всплески. Это паттерн №3: обратное давление — когда производители генерируют работу быстрее, чем потребители могут её обработать, замедлите производителей или буферизуйте работу. Здесь пригодятся Kafka или другие очереди. Но заметьте: мы не начали с «давайте использовать Kafka», а с «производитель быстрее потребителя — нужно буферизация/обратное давление», и только потом выбрали технологию.
Не обязательно. Это ловушка на интервью: не стоит говорить «мы просто положим все платежи в Kafka». У финансовых транзакций есть требования к корректности и задержкам. Есть разница между критическим переходом состояния платежа и побочными эффектами. Например, платёж: обновить финансовое состояние, отправить уведомление, обновить аналитику, сгенерировать чек, обновить рекомендательную систему. Переход финансового состояния может требовать строгой корректности, а вот отправка уведомления «25 000 оплачено» не обязательно должна блокировать основную транзакцию. Побочные эффекты часто можно делать асинхронно.
Теперь самое интересное. Вы нажимаете «Оплатить 25 000». Запрос доходит до платёжной системы, деньги списываются, но ответ не доходит до вашего телефона из-за таймаута сети. Приложение показывает «Ошибка». Вы думаете: «Ладно, попробую ещё раз». И снова жмёте. Система получает два одинаковых платежа. Если она обработает оба как новые, с вашего счёта спишется 50 000. Это недопустимо.
Нужно, чтобы система понимала: «этот повтор — тот же самый платёж». Поэтому клиент/запрос должен нести уникальный идентификатор: payment_id = ABC123. При первом запросе система выполняет платёж и сохраняет результат. При повторном запросе с тем же ID система видит, что такой платёж уже обработан, и возвращает сохранённый результат, не списывая деньги повторно. Это идемпотентность. Без неё повторные попытки приводят к двойному списанию.
Чтобы закрепить материал, разберите простой пример на Python: реализуйте идемпотентный обработчик платежей с использованием словаря для хранения результатов. Создайте функцию process_payment(payment_id, amount, balance), которая проверяет, есть ли payment_id в словаре; если есть — возвращает сохранённый результат; если нет — выполняет списание и сохраняет результат. Протестируйте с двумя одинаковыми запросами. Также попрактикуйтесь в проектировании: нарисуйте схему с балансировщиком, несколькими серверами и шардированной БД. Объясните, как вы защитите состояние от гонок и как обеспечите идемпотентность. Эти паттерны пригодятся не только на собеседованиях, но и в реальных проектах.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →