Транспорт бэкенда
Backpressure и Flow Control
Черная пятница. Нагрузка выросла в 10x. Сервис обработки заказов начинает тормозить. Каждый запрос ждёт в unbounded очереди всё дольше - latency растёт с 50ms до 30 секунд. Через 5 минут очередь в памяти занимает 16GB - OOM. Сервис упал. Все заказы потеряны. Backpressure и bounded queues - страховочная сетка против этого.
- **Netflix** Hystrix и adaptive concurrency limiting отклоняют запросы при перегрузке вместо полного краша. В 2012 этот механизм спас сервис во время регионального AWS сбоя.
- **Node.js Streams** с автоматическим backpressure через `pipe()` - стандарт для обработки больших файлов без загрузки в память. Без backpressure `fs.readFile()` + `res.write()` убьёт процесс на 10GB файле.
- **Kafka** Consumer Lag - метрика backpressure на уровне брокера. Если lag растёт - потребители не справляются. Решение: scale out consumers или уменьшить producer rate.
Что такое backpressure
Backpressure - механизм когда потребитель сигнализирует производителю «медленнее». Без backpressure быстрый производитель переполняет буферы медленного потребителя: память исчерпывается, система падает. Backpressure - это контроль потока от получателя к отправителю.
Twitter в 2013 open-source'ил понятие backpressure применительно к Reactive Streams. Проблема встречается везде: Kafka consumer lag, Node.js writable streams, gRPC flow control, TCP receiver window.
Система с unbounded queue начинает потреблять всю доступную память. Это называется:
TCP Flow Control и Window
TCP имеет встроенный backpressure: receive window (rwnd). Получатель сообщает отправителю сколько байт он может принять прямо сейчас. Если буфер заполнен - rwnd=0, отправитель ждёт. Это механизм уровня ядра ОС, прозрачный для приложений.
HTTP/2 добавляет flow control на уровне потоков (streams): каждый stream имеет свой window. Это позволяет контролировать приоритет разных запросов независимо. gRPC использует HTTP/2 flow control для gRPC-stream методов.
TCP receive window (rwnd) = 0. Что должен делать отправитель?
Reactive Streams и RxJS
Reactive Streams (2013) - спецификация для async stream processing с backpressure: Publisher, Subscriber, Subscription, Processor. В Java: Project Reactor (Spring WebFlux), RxJava. В Node.js: RxJS, встроенные Streams. В Go: channels с буфером.
Kotlin Coroutines Flow, Java 9 Flow API, Akka Streams - все реализуют Reactive Streams спецификацию и совместимы между собой. Netflix использует RxJava для orchestration API responses на бэкенде.
Пользователь быстро печатает в поиске. Какой RxJS оператор правильно применить для ограничения API вызовов?
Bounded Queues и стратегии отклонения
Bounded queue - очередь с ограниченным размером. При достижении лимита производитель должен принять решение: заблокироваться, бросить исключение, или применить стратегию overflow. Правильная стратегия зависит от требований к latency и data loss.
Java ThreadPoolExecutor с LinkedBlockingQueue(capacity) - классический пример bounded queue. DiscardOldestPolicy удаляет самое старое задание при переполнении - подходит для real-time данных где свежесть важнее полноты.
Сервис обрабатывает live-данные с биржевого стакана. Очередь заполнена. Какая стратегия overflow правильная?
Load Shedding
Load Shedding - осознанное отбрасывание запросов при перегрузке для защиты системы. Лучше отклонить 10% запросов с 503, чем упасть целиком под нагрузкой. Google SRE рекомендует load shedding как первую линию защиты при перегрузке.
Google CRE (Customer Reliability Engineering) рекомендует: при нагрузке > 70% CPU активировать load shedding. Лучше отклонять запросы детерминированно, чем иметь непредсказуемый рост latency под нагрузкой.
Backpressure - это только про медленных потребителей, нужно просто добавить серверов
Backpressure - фундаментальный принцип: система должна уметь сигнализировать 'медленнее' на всех уровнях. Добавление серверов помогает, но без backpressure новые серверы тоже упадут.
Горизонтальное масштабирование не устраняет backpressure: если downstream БД не масштабируется, добавление app-серверов только увеличит нагрузку на БД. Bounded queues и load shedding нужны независимо от числа серверов.
Почему Load Shedding (отклонение 503) предпочтительнее, чем ставить все запросы в очередь при перегрузке?
Итоги
- **Backpressure** - сигнал от потребителя производителю: 'медленнее'. Встроен в TCP (rwnd), HTTP/2 (flow control), Reactive Streams, Kafka consumer groups.
- **Bounded queues** - единственная защита от OOM при перегрузке. Стратегия overflow зависит от домена: block для важных данных, DiscardOldest для real-time.
- **Load Shedding** - осознанное отклонение с 503 лучше полного краша. Priority-based shedding сначала отклоняет low-priority запросы.
Связанные темы
Backpressure пронизывает все уровни стека от TCP до application layer:
- TCP и HTTP/2 — TCP receive window - встроенный backpressure. HTTP/2 добавляет stream-level flow control поверх TCP
- Kafka: producer и consumer — Consumer lag - application-level backpressure индикатор. max.in.flight.requests и linger.ms контролируют producer flow
Вопросы для размышления
- Как обнаружить, что система испытывает backpressure проблемы, до того как она упадёт?
- В каких случаях лучше блокировать производителя (blocking), а не дропать данные при перегрузке?
- Как организовать priority-based load shedding в microservices где нагрузка приходит через единый API Gateway?