4. System design. Обратная связь. Если бы мы знали, что это такое, но мы не знаем, что это такое

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

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

В такие моменты система должна как-то реагировать. Непросто падать с ошибкой 503 и отправлять всех в jopa. А делать что-то умное. Что-то, что позволит ей выжить, пусть и в урезанном виде.

На помощь нам придёт “обратная связь”. Если бы мы знали, что это такое, но мы не знаем, что это такое. Шутка, конечно. На самом деле знаем, но не помним. Штука очень полезная, про неё забывают на этапе проектирования и потом напихивают в разные места, затыкая дыры. Не надо так.

Что же такое обратная связь?

Обратная связь это когда система реагирует на изменения в окружающей среде и корректирует своё поведение. Звучит просто. И на практике просто, если знать, что делать. Если не знать - сложно. Такая простая штука может значительно влиять на жизнеспособность системы. Без неё система работает ровно до первого неожиданного события, а потом падает и всё: стресс, убытки, всё лежит, все грустят.

Представь термостат. Он измеряет температуру и включает или выключает обогрев, чтобы поддерживать заданную температуру. Это классический пример обратной связи: система реагирует на изменения и корректирует своё поведение. Без этого механизма температура либо постоянно росла бы, либо падала, пока всё не сломалось. В программных системах то же самое. Только вместо температуры у нас нагрузка, задержки, ошибки, доступность ресурсов. И система должна уметь реагировать на эти изменения.

Без обратной связи система становится хрупкой. Она работает ровно до тех пор, пока всё идёт по плану. Как только появляется что-то неожиданное пик нагрузки, сбой компонента, изменение требований система ложится. Потому что у неё нет механизма, который бы сказал: “Оу-оу, полегче, я такое не умею, я такое не могу и вообще мне тяжко”. По-хорошему нужно систему научить на такое реагировать. Для этого нужно как минимум понимать потоки данных в системе.

Есть несколько типов обратной связи:

Отрицательная обратная связь это когда система компенсирует отклонения и возвращает состояние к норме. Это стабилизация. Rate limit, который замедляет запросы при перегрузке. Circuit breaker, который отключает неработающий сервис, чтобы не тратить ресурсы впустую. Кеш, который отдаёт устаревшие данные, когда свежие недоступны. Всё это примеры отрицательной обратной связи: система реагирует на проблему и пытается вернуть всё в рабочее состояние. Это то, что тебе нужно в 99% случаев.

Положительная обратная связь это когда система усиливает отклонения. Звучит опасненько. Каскадные падения сервисов, когда один упал, нагрузка перераспределилась, другие не выдержали и тоже упали. Runaway-процессы, которые жрут всё больше ресурсов, пока система не умрёт. Но иногда положительная обратная связь нужна: например, когда система должна быстро масштабироваться при росте нагрузки. Главное контролировать этот процесс.

Backpressure это сигнал “стоп, хватит”. Когда вход растёт быстрее, чем система может переварить, она должна как-то сообщить источнику: “Полегче, я не успеваю”. Это может быть явный отказ в обработке, замедление ответов или очередь, прости господи, которая переполняется и начинает отклонять новые задачи.

Управляемая деградация это когда система жертвует частью функциональности, но остаётся работоспособной. Вместо того чтобы упасть целиком, система отключает несущественные фичи, упрощает обработку, отдаёт менее свежие данные. Ядро системы продолжает работать и предоставлять основной функционал. Это не идеальное решение, но лучше, чем полный отказ. Пользователи могут не увидеть свежие рекомендации или точную статистику, но смогут прочитать ленту и создать пост, например.

Все эти механизмы работают вместе, дополняя друг друга, создавая систему, которая может адаптироваться к изменениям. Backpressure защищает от перегрузки, управляемая деградация позволяет продолжать работать при проблемах, отрицательная обратная связь стабилизирует систему, положительная (когда нужна) позволяет быстро масштабироваться.

Где обратная связь нужна в архитектуре?

Обратная связь должна быть на всех уровнях системы. Не только в одном месте, но и везде, где система взаимодействует с внешним миром или внутренними компонентами. Классическая ошибка: добавили rate limit на входе и радуемся, что всё готово. А потом система падает из-за проблем с чтением или фоновыми задачами. “Как так? Я же защитил вход!”, ага, да, но не учёл весь поток данных.

На уровне контура записи обратная связь защищает систему от перегрузки. Rate limit ограничивает количество запросов в единицу времени. Throttling замедляет обработку, когда система перегружена. Валидация отклоняет некорректные данные до того, как они попадут в систему и начнут создавать проблемы. Без этого контур записи может стать точкой отказа: слишком много данных, система не справляется, все грустят. Классическая история: кто-то решил загрузить миллион записей через API, система пытается всё обработать, база задыхается, всё падает, DDoS. А могло бы быть просто: 429 “Слишком много запросов, попробуйте позже”.

На уровне контура чтения обратная связь обеспечивает доступность данных даже при проблемах. Кеширование позволяет отдавать данные быстро, даже если источник медленный. Fallback на устаревшие данные даёт возможность показать что-то пользователю, когда свежие данные недоступны. Read replicas распределяют нагрузку чтения, не перегружая основную базу. Без этого каждое чтение становится зависимостью от работоспособности всех компонентов, и любая проблема превращается в полный отказ. Например, система, где каждое чтение идёт в основную базу: когда база тормозит, всё падает периодически, и юзер видит йух. А могло бы быть: читаем из реплики, если реплика недоступна из кеша, если кеш пустой из основной базы, но с таймаутом. И система продолжает работать. Да, кода раза в два больше, зато надёжненько.

На уровне контура обработки обратная связь управляет приоритетами и ресурсами. Приоритизация задач позволяет обрабатывать важное в первую очередь, откладывая менее критичное. Отмена долгих операций освобождает ресурсы, когда система перегружена. Изоляция фоновых задач не даёт им мешать пользовательским запросам. Без контроля тяжёлые операции могут заблокировать всю систему, и пользователи перестанут получать ответы. Классика: фоновый джоб по пересчёту статистики запустился в пик нагрузки, сожрал CPU и память, юзеры получают отказ в обслуживании, система прилегла. А могло бы быть так: фоновые задачи останавливаются при высокой нагрузке, возобновляются, когда нагрузка спадает.

На уровне инфраструктуры обратная связь обеспечивает масштабирование и отказоустойчивость. Автоскейлинг добавляет ресурсы при росте нагрузки и убирает при снижении. Circuit breakers отключают неработающие зависимости, чтобы они не тянули систему вниз. Health checks обнаруживают проблемы до того, как они станут критичными. Это позволяет системе адаптироваться, реагировать на сбои и нагрузку. Например, система курильщика, где при падении одного сервиса все остальные продолжают пытаться его вызвать, накапливают таймауты, система деградирует. А в системе здорового человека: circuit breaker глушит упавший сервис, система продолжает работать без этого сервиса, периодически проверяет, не восстановился ли он.

Важно понимать: обратная связь на одном уровне не решает проблему полностью. Нужна система обратных связей, которая работает на всех уровнях одновременно. Иначе получится ситуация, когда ты защитил вход, но система падает из-за проблем с чтением. Или наоборот. Rate limit на входе бесполезен, если чтение из базы блокирует всю систему. Кеширование не поможет, если фоновые задачи жрут все ресурсы. Circuit breaker не спасёт, если проблема внутри, а не во внешних зависимостях.

Поэтому проектировать обратную связь нужно системно. Не “добавим rate limit и всё”, а “посмотрим, где система может сломаться, как идут потоки данных, выявим уязвимые места и добавим обратную связь там, где нужно”. Это требует понимания потоков данных, о которых мы говорили в прошлый раз. Ты можешь добавить обратную связь везде, но это будет оверинжиниринг. Или можешь добавить только в одном месте, но этого будет недостаточно. Нужно найти баланс.

Пример

Давай посмотрим, как это работает на практике. Представь бота, который торгует на нескольких криптобиржах одновременно: Binance, OKX, Bybit. Нормальное состояние: WebSocket-потоки идут стабильно, ордера исполняются, задержки в пределах нормы. Но вот происходит что-то: новость про регуляцию, Трамп что-то сказал, или Хейс чихнул, или все вдруг решили поторговать. Классическая ситуация: всё было хорошо и вдруг всё плохо. Наверное, ты даже где-то такое видел?

Без обратной связи система пытается обработать всё. Каждое обновление из WS обрабатывается, каждый тик анализируется, каждый ордер отправляется на биржу. Потоки начинают душить систему, очередь сообщений растёт, задержки увеличиваются с миллисекунд до секунд. Система начинает принимать плохие решения, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще ложится. Какой-то сервис положил, допустим, базу или распределённый кеш, откуда читает контур принятия решений. В тг к тебе уже стучатся: “Бот лёг? Там движение! Ордеров нет!” А ты в душе не представляешь, почему оно там лежит и что вообще происходит.

Теперь добавим обратную связь на разных уровнях.

На контуре записи (WS-потоки) ставим backpressure. Когда система не успевает обрабатывать обновления из WebSocket, она сигнализирует: “Падажди, я не успеваю”. Это может быть явный отказ принимать новые сообщения или замедление обработки. Но важно не переборщить: слишком агрессивный backpressure заставит тебя пропустить важные обновления цены, слишком мягкий не поможет при реальном всплеске. Нужно найти золотую середину. Стоит начать с консервативных значений и вручную, по метрикам, подгонять при необходимости.

На контуре чтения (обработка котировок) включаем приоритизацию. Критичные пары BTC/USDT, ETH/USDT обрабатываются в первую очередь, менее ликвидные потом. Если система перегружена, она может временно отключить обработку некоторых пар, сосредоточившись на самых важных. Это лучше, чем пытаться обработать всё и упасть.

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

Для внешних зависимостей бирж, API-провайдеров, ораклов организуем circuit breakers. Если биржа не отвечает или отвечает слишком медленно, circuit breaker “разрывает цепь” и перестаёт отправлять ордера на эту биржу на некоторое время. Система продолжает торговать на других биржах, но не падает из-за проблем с одной биржей. Пример ошибки: бот отправляет ордер на Binance, тот не отвечает (rate limit или просто проблемы с сетью), бот ждёт таймаут, накапливает ордера, падает. А могло бы быть: circuit breaker разрывает цепь, бот продолжает работать на других биржах, периодически проверяет, не восстановился ли Binance.

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

Попробую на цифрах показать. Допустим, обычная нагрузка: 10 тысяч обновлений в секунду от всех бирж. Система справляется, все обновления обрабатываются за 10–50 миллисекунд. Внезапно приходит 50 тысяч обновлений в секунду: дамп, все начинают торговать одновременно, биржи заливают обновлениями. Без обратной связи очередь сообщений растёт, задержки увеличиваются до секунд, система начинает терять деньги, потому что цены меняются быстрее, чем система успевает на них реагировать. А потом система вообще падает.

С обратной связью картина другая. Backpressure на WS-потоках пропускает только 20 тысяч обновлений в секунду, остальные отбрасываются. Из этих 20 тысяч система обрабатывает только критичные пары: BTC/USDT, ETH/USDT; остальные временно игнорируются. Circuit breaker отслеживает задержки на биржах: если задержка превышает допустимый порог (допустим, 500 миллисекунд), он перестаёт отправлять ордера на эту биржу. Менее критичные стратегии останавливаются, освобождая CPU и память для самых важных.

Результат: система продолжает работать. Часть пар не обрабатывается, часть стратегий отключена, но ядро системы стабильно. Система может заработать меньше, но не потеряет всё из-за полного отказа. Когда волатильность спадает, всё возвращается в норму: circuit breaker возвращает в работу биржи, стратегии возобновляются, обработка всех пар восстанавливается.

Важно понимать: это неидеальное решение. Идеального не бывает. Но это управляемая деградация вместо полного отказа. С точки зрения пользователя падение прибыли значительно лучше, чем полный слив.

Приведу несколько типичных ошибок.

Типичная ошибка, игнорирование обратной связи вообще. “У нас же монга с кафкой, они всё выдержат”. Нет, не выдержат. Рано или поздно нагрузка превысит возможности, и система упадёт. Обратная связь не опциональная фича для highload-систем. Это необходимость для любой системы, которая хочет выжить.

Другая ошибка, неправильная настройка. Rate limit на 1000 запросов в секунду, когда система реально может обработать только 100. Или circuit breaker, который срабатывает после одной ошибки и блокирует сервис на час. Или кеш с TTL в секунду, который не даёт никакого эффекта. Обратная связь должна быть настроена под реальные возможности системы и требования бизнеса. Иначе она либо не поможет, либо навредит.

Третья ошибка, отсутствие мониторинга. Если ты не видишь, когда и как срабатывает обратная связь, ты не можешь понять, работает ли она правильно. Сколько запросов отклоняется rate limiterом? Как часто circuit breaker банит ресурс? Насколько часто пользователи видят устаревшие данные из кеша? Какой вообще hit rate по кешу? Без метрик обратная связь становится чёрным ящиком, и ты не знаешь, помогает она или мешает.

Четвёртая ошибка, обратная связь только на одном уровне. Защитил вход rate limiterом, но забыл про чтение. Или настроил кеширование, но не подумал про фоновые задачи. Система всё равно падает, просто в другом месте. Нужна комплексная система обратных связей, которая покрывает все уровни архитектуры.

Что в итоге?

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

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

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

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

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

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

И в этом вся суть system design: не нарисовать красивую схему, а построить систему, которая выживет.

Увидимся в Новом году. Кушайте салатики и читайте книги. Хейтеры, люблю вас!

Что ещё почитать: