Real-Time Backend
Backpressure
В 2016 году во время распродажи на Amazon consumer сервиса обработки заказов начал отставать. Без backpressure producer продолжал заливать события - очередь в памяти выросла до нескольких гигабайт, JVM выбила OOM, сервис упал. С backpressure producer бы замедлился, система деградировала бы gracefully, а не рухнула полностью.
- **Kafka consumer lag** - Uber мониторит lag платежного consumer: при lag > 30 секунд автоматически запускается дополнительный consumer instance через Kubernetes HPA.
- **Netflix Hystrix / Resilience4j** - при недоступности сервиса профилей circuit breaker переходит в OPEN, и страница отдает кэшированный профиль вместо ошибки 503 для 200+ млн пользователей.
- **TCP receive window** - браузер скачивает видео по HTTP/1.1: receive window сжимается когда видеодекодер не успевает читать буфер - TCP автоматически тормозит отдачу с сервера без единой строки кода на уровне приложения.
- **RxJava onBackpressureDrop** в Android: сенсор акселерометра генерирует 100 событий/сек, но UI обновляется в 60fps. Лишние события дропаются - батарея и CPU экономятся без потери плавности.
Backpressure Concept
Backpressure - механизм, при котором **медленный потребитель сигнализирует производителю замедлить отправку**. Без этого сигнала производитель накапливает события в памяти или теряет их, когда буфер переполняется.
Классическая иллюстрация - TCP receive window. Получатель объявляет в заголовке TCP, сколько байт он готов принять прямо сейчас (rwnd). Отправитель не имеет права послать больше этого числа без подтверждения. Если приложение на стороне получателя читает данные медленно, rwnd сжимается до нуля - и отправитель полностью останавливается. Это встроенный backpressure на транспортном уровне.
Reactive Streams (стандарт для Java/Kotlin: RxJava 2+, Project Reactor, Akka Streams) формализует backpressure через метод `request(n)`: подписчик сам сообщает, сколько элементов он готов обработать. Издатель не вправе отправить больше запрошенного числа.
TCP receive window (rwnd) равен 0. Что произойдет с отправителем?
Buffering
Буферизация - первый и самый интуитивный ответ на backpressure: сохранить события в очереди, пока потребитель не догонит. Kafka consumer lag - живая метрика этого буфера: разность между последним записанным offset и последним прочитанным offset показывает, насколько consumer отстал от producer.
У буфера есть три критических параметра: **размер** (сколько событий помещается), **политика переполнения** (что делать когда буфер полон) и **время жизни** (когда событие становится бесполезным). Kafka хранит события на диске, поэтому буфер может быть огромным - но это не бесплатно: больший lag означает задержку обработки и риск out-of-order processing.
- **In-memory queue** - минимальная задержка, теряется при рестарте (RxJava `onBackpressureBuffer`)
- **Disk-backed queue** - Kafka, RabbitMQ; выживает при рестарте, но требует сериализации
- **Bounded queue** - ограниченный размер; при переполнении применяется dropping policy
- **Unbounded queue** - растет до OOM; опасно в prod без мониторинга lag/heap
Kafka consumer lag мониторят через метрики `records-lag-max` (JMX) или Prometheus kafka-exporter. Алерт на lag > N секунд (не абсолютное число, а lag / consumption rate) дает предупреждение до того, как очередь переполнится.
Kafka consumer lag вырос с 1 000 до 500 000 за 10 минут. Producer rate - 10 000 сообщений/сек. Что это означает?
Dropping
Когда буфер полон и замедлить producer невозможно, система выбирает: что выбросить. Dropping policy - это явный выбор, какие данные менее ценны. Неявный dropping (например, OOM killer убивает процесс) хуже любой explicit policy.
- **DROP_OLDEST** - выбрасывает самые старые события; актуально для телеметрии, где свежие данные важнее
- **DROP_LATEST** - выбрасывает новые события; защищает уже накопленные данные от потери
- **DROP_ALL** - очищает весь буфер разом; используется для state-based данных (последняя позиция GPS)
- **SAMPLE** - передает каждый N-й элемент; подходит для метрик с высокой частотой обновления
UDP - архетипический пример DROP_LATEST без буфера: если приложение не успело прочитать датаграмму из сокета, ядро просто перезаписывает её следующей. Поэтому UDP используют для видеоконференций (устаревший кадр бесполезен) и DNS (проще повторить запрос, чем буферизировать).
Система получает обновления GPS-координат 50 раз/сек, но UI рендерит 30 fps. Какая dropping policy оптимальна?
Flow Control и Circuit Breakers
Flow control - активное управление скоростью producer на основе сигналов от consumer. Circuit breaker (автомат защиты) - крайний случай: когда downstream деградирует, upstream полностью прекращает отправку запросов, чтобы дать системе восстановиться.
Netflix Hystrix (теперь Resilience4j) реализует circuit breaker с тремя состояниями. CLOSED - нормальная работа, запросы проходят. OPEN - автомат сработал, все вызовы немедленно возвращают fallback без обращения к downstream. HALF-OPEN - периодические probe-запросы проверяют, восстановился ли сервис.
Reactive Streams `request(n)` - pull-based flow control: consumer явно «тянет» данные по мере готовности. Это противоположность push-модели, где producer шлет данные без оглядки на consumer. Project Reactor (Spring WebFlux) строит всю реактивную цепочку на этом механизме.
Kafka consumer контролирует flow через `max.poll.records` и `pause()/resume()` TopicPartition API. При обнаружении что downstream перегружен, consumer вызывает `consumer.pause(partitions)` - polling продолжается (heartbeat не прерывается, rebalance не триггерится), но данные не возвращаются до `resume()`.
Backpressure - это просто добавление большего буфера
Буфер лишь откладывает проблему. Настоящий backpressure - сигнал producer замедлиться или механизм, который защищает систему от каскадного отказа (circuit breaker, dropping policy, pull-model).
Unbounded buffer при постоянно перегруженном consumer гарантированно приведет к OOM. Правильное решение - либо ограничить producer скоростью consumer (flow control), либо явно выбрать что терять (dropping policy).
Circuit breaker перешел в состояние OPEN. Что происходит с входящими запросами?
Ключевые идеи
- Backpressure - сигнал от consumer к producer: замедлись. TCP rwnd, Reactive Streams `request(n)`, Node.js `readable.pause()` - все это одна идея на разных уровнях.
- Буфер откладывает проблему, но не решает её. Kafka consumer lag - метрика размера «долга» consumer перед producer. Unbounded buffer ведет к OOM.
- Dropping policy - явный выбор что терять: DROP_OLDEST для свежих данных (телеметрия), DROP_LATEST для накопленных (транзакции), onBackpressureLatest для state (GPS, UI).
- Circuit breaker защищает downstream от перегрузки: OPEN состояние возвращает fallback мгновенно, давая сервису время восстановиться без thundering herd при открытии.
Связанные темы
Backpressure пронизывает все уровни realtime-архитектуры - от протоколов до паттернов отказоустойчивости:
- Rate Limiting — Rate limiting ограничивает producer на входе в систему; backpressure - сигнал изнутри системы от consumer к producer
- Message Queues — Очереди (Kafka, RabbitMQ) реализуют буферизацию для backpressure; consumer lag - прямое измерение backpressure в системе
- Circuit Breaker — Circuit breaker - крайний случай backpressure: полная остановка flow при деградации downstream
Вопросы для размышления
- Сервис обрабатывает платежи и принимает события из Kafka. Consumer lag начал расти. Какие три шага по порядку приоритета стоит предпринять до масштабирования consumer instances?
- В чем принципиальная разница между `onBackpressureDrop` и `onBackpressureLatest` в RxJava? В каком сценарии каждый из них опасен?
- Circuit breaker находится в HALF-OPEN. Первый из 5 probe-запросов вернул ошибку. Как должен вести себя автомат и почему именно так?