23:59 четверга. Нагрузка прыгает в десять раз за секунды. Дашборды радостно светятся зелёным, показывая рекордный прогон событий. Вы спокойно пьёте кофе, считая, что архитектура выдержала. В 00:15 звонит безопасник: деньги утекают, а система блокирует карты по транзакциям, которые случились полчаса назад. Я наблюдал это изнутри. Потеря событий в потоковой аналитике под нагрузкой — это не случайный сбой, это закономерный финал архитектуры, которую строили оптимисты. Запомните: real-time ML измеряется исключительно тем, как он ведёт себя на пике деградации, а не его средней пропускной способностью в тепличных условиях.
Хотите гарантированно повторить этот war story, где real-time ML захлебнулся в Чёрную пятницу, лаг консьюмера вырос в 40 раз, а антифрод начал терять фрод в моменте? Вот вам чёткий алгоритм действий.
Синхронный REST-инференс и слепые мониторинги
Делайте синхронный вызов к модели на каждое событие. Никакого батчинга. На горячем пути обработки просто кидайте HTTP-запрос в Python-контейнер. В 00:01, когда хлынет трафик, сеть между Kafka-консьюмером и сервингом ляжет под тяжестью TCP-соединений. В 00:03 JSON-сериализация каждого отдельного чиха сожрёт весь CPU. В 00:07 сборщик мусора в Java или Go консьюмере встанет в жёсткую stop-the-world паузу, пытаясь собрать миллионы мелких объектов из памяти. В 00:10 очередь на инференс пробьёт все таймауты, и польются 504-е ошибки.
Почему мониторинг промолчит? Потому что вы смотрите на throughput. Ваш самописный консьюмер исправно молотит свои максимальные 5000 RPS. Линия на графике ровная. То, что на входе сейчас 50000 RPS, а брокер покорно складирует разницу на диск — вы узнаете слишком поздно. Инфраструктура в порядке, всё работает идеально. Просто consumer lag растёт с секунд до десятков минут. А метрика latency инференса под пиком либо не настроена на p99, либо не генерит алерт. В это время хайповые курсы продолжают учить поднимать трансформеры за REST API, умалчивая, что законы физики сети умножат этот сетап на ноль при первом же всплеске.
Когда мы в Morana Labs катили стриминг-скоринг транзакций для одного финтех-процессинга, первое, что пошло под нож — поштучная обработка событий; если не реализовать агрессивный микро-батчинг на уровне консьюмера и не перевести вызовы в асинхронный пайплайн поверх gRPC или разделяемой памяти, система неизбежно ляжет под собственным весом.
Продолжаем сборку катастрофы. Игнорируйте backpressure. В нормальной инженерной картине мира бэкпрешер flink kafka работает как тормозная система: если консьюмер не справляется, он сообщает об этом вверх по потоку. Источник притормаживает или начинает дропать низкоприоритетный трафик. Если этой связи нет, консьюмер копит бесконечные внутренние буферы, пока не поймает OOM. Затем он падает, инициируется ребалансировка партиций, остальные ноды получают двойную нагрузку и тоже падают. Каскадный отказ.
Ни в коем случае не закладывайте механизм деградации. Зачем переключаться на лёгкую модель, отрабатывающую за миллисекунду? Лучше героически мучить тяжёлую нейросеть, требующую 50 мс на скоринг, пока бизнес теряет реальные деньги. И забудьте про exactly-once семантику. Когда поднявшийся после падения консьюмер вычитает старый офсет, он сгенерирует безопасникам дублирующиеся алерты, парализовав ручной разбор инцидентов.
Алерты по лагу и анатомия нагрузочного тестирования
Если вас не устраивает вариант с провалом, придётся менять подход. Начинать нужно с переписывания правил мониторинга. Средняя пропускная способность — мусорная метрика. Вас интересуют consumer lag алерт, p99 скоринга, drop rate и время сборки мусора.
groups:
- name: streaming_ml_alerts
rules:
- alert: CriticalConsumerLag
expr: kafka_consumergroup_lag > 5000
for: 1m
labels:
severity: critical
annotations:
summary: "Lag > 5000. Real-time is dead, fraud is passing."
- alert: InferenceP99Spike
expr: histogram_quantile(0.99, rate(inference_duration_seconds_bucket[1m])) > 0.05
for: 1m
labels:
severity: warning
- alert: GCPauseTooHigh
expr: rate(jvm_gc_pause_seconds_sum[1m]) / rate(jvm_gc_pause_seconds_count[1m]) > 0.01
for: 2m
labels:
severity: warningКод алертов не спасёт, если архитектура не проверена боем. Ваша инфраструктура не готова к запуску, пока вы не провели нагрузочный тест с профилем x10 от дневного максимума и не подтвердили каждый пункт:
- Динамический размер батча: консьюмер должен адаптивно объединять от 10 до 500 событий за окно в 10 миллисекунд перед отправкой в инференс.
- Нулевая аллокация: под потоком в 100k сообщений в секунду JSON убьёт память, нужен переход на Protobuf, FlatBuffers или бинарный формат, чтобы избежать GC-шторма.
- Прошитый backpressure: сигнал о перегрузке модели должен за миллисекунды доходить до входного шлюза, заставляя его отбрасывать некритичные события, а не копить их в памяти.
- Graceful degradation по таймауту: если p99 инференса пробивает 30 мс, трафик автоматически переводится на fallback-модель (простые правила), пока основная не остынет.
- Идемпотентность: шардирование и exactly-once (или строгая дедупликация на стороне хранилища), чтобы при неизбежных рестартах нод не задвоить транзакции.
Инженерия в высоконагруженном ML — это не выбор красивого фреймворка для нейросети. Это жёсткий контроль над тем, где именно лопнет труба, и уверенность в том, что когда это произойдёт, система начнёт сбрасывать давление, а не копить его до взрыва. Принимайте стриминг по поведению на грани отказа.