Потоковые ML-пайплайны для real-time инференса на Kafka, Flink и ONNX

Проектируем и внедряем системы искусственного интеллекта: от прототипа до production-ready решения. Наша команда объединяет экспертизу в машинном обучении, дата-инжиниринге и MLOps, чтобы AI работал не в лаборатории, а в реальном бизнесе.
Показано 1 из 1Все 1564 услуг
Потоковые ML-пайплайны для real-time инференса на Kafka, Flink и ONNX
Сложный
~2-4 недели
Часто задаваемые вопросы

Направления AI-разработки

Этапы разработки AI-решения

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    957
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    646
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929

Fraud detection в реальном времени — типичный кейс, где latency > 100 мс означает потерю денег. Мы разрабатываем потоковые ML-пайплайны, которые обрабатывают события с задержкой до 50 мс, используя Kafka, Flink и ONNX Runtime. Один из проектов — система антифрода для финтех-компании, обрабатывающая 50 000 транзакций в секунду с p99 latency 45 мс. Другой пример — пайплайн для платежного агрегатора с throughput 20K событий/с и латентностью p99 55 мс, что сократило chargeback на 35%. Переход с batch-пайплайна на потоковый позволил этому клиенту снизить затраты на инфраструктуру на 40% — окупаемость проекта составила 3 месяца.

Мы реализовали более 50 подобных решений. Apache Flink — один из ключевых инструментов, обеспечивающих exactly-once семантику и отказоустойчивость. Сертифицированные инженеры гарантируют стабильность и масштабируемость пайплайна.

Проблемы, которые решаем

Латентность. Классические batch-пайплайны вносят задержки от минут до часов. Для real-time скоринга транзакций или обновления рекомендаций это неприемлемо. Мы используем sliding window агрегации и online feature store (Redis) с lookup < 5ms, чтобы признаки были доступны мгновенно.

State management. Потоковая обработка требует консистентного состояния при сбоях и перезапусках. Apache Flink предоставляет exactly-once семантику и checkpointing, что критично для финансовых применений.

Model versioning. При обновлении модели нельзя останавливать пайплайн. Реализуем A/B тестирование через теневые трафик и постепенную раскатку новых версий с помощью feature flags.

Архитектура потокового ML-пайплайна

[Kafka / Kinesis / Pulsar]
        ↓
[Feature Computation]     ← Flink / Spark Streaming / Kafka Streams
(агрегации, окна, joins)
        ↓
[Feature Store Online]    ← Redis / DynamoDB (< 5ms lookup)
        ↓
[Model Inference]         ← Triton / TorchServe / ONNX Runtime
(< 20ms)
        ↓
[Decision Engine]         ← бизнес-правила + ML score
        ↓
[Action / Output Kafka]   ← downstream системы

Почему Kafka Streams и ONNX?

Kafka Streams встраивается в любой микросервис и не требует отдельного кластера для обработки. Для высоконагруженных сценариев используем Flink с параллелизмом до 16. ONNX Runtime позволяет выполнять модели на CPU с latency < 5ms и поддерживает квантизацию INT8 для дополнительного ускорения.

Как обеспечить exactly-once семантику?

Apache Flink поддерживает exactly-once за счет checkpointing и согласованных snapshot'ов состояния. В документации Apache Flink описаны механизмы, которые мы применяем в продакшене. Это гарантирует, что при сбоях ни одно событие не будет потеряно или обработано дважды.

Реализация потокового пайплайна

Вычисление признаков в реальном времени

from confluent_kafka import Consumer, Producer
import json
import redis
import numpy as np
import time
from collections import deque, defaultdict
import threading

class StreamFeatureComputer:
    """Вычисление признаков в реальном времени"""

    def __init__(self, kafka_config: dict, redis_url: str):
        self.consumer = Consumer(kafka_config)
        self.producer = Producer({'bootstrap.servers': kafka_config['bootstrap.servers']})
        self.redis = redis.from_url(redis_url)
        self.window_store = defaultdict(lambda: deque(maxlen=1000))

    def compute_user_features(self, user_id: str, event: dict) -> dict:
        """Online-признаки для пользователя"""
        key_prefix = f"user:{user_id}"
        now = event['timestamp']

        # Sliding window агрегации через Redis
        pipe = self.redis.pipeline()

        # Транзакционные признаки
        event_key = f"{key_prefix}:events"
        pipe.lpush(event_key, json.dumps({
            'amount': event.get('amount', 0),
            'ts': now,
            'type': event.get('type', 'unknown')
        }))
        pipe.ltrim(event_key, 0, 999)  # Держим последние 1000 событий
        pipe.expire(event_key, 86400)  # TTL 24 часа

        pipe.execute()

        # Агрегации за разные окна
        raw_events = self.redis.lrange(event_key, 0, -1)
        events = [json.loads(e) for e in raw_events]

        # Сортировка по времени
        events.sort(key=lambda x: x['ts'], reverse=True)

        window_1h = [e for e in events if now - e['ts'] <= 3600]
        window_24h = [e for e in events if now - e['ts'] <= 86400]

        amounts_1h = [e['amount'] for e in window_1h]
        amounts_24h = [e['amount'] for e in window_24h]

        features = {
            'user_id': user_id,
            'tx_count_1h': len(window_1h),
            'tx_count_24h': len(window_24h),
            'tx_amount_sum_1h': sum(amounts_1h),
            'tx_amount_sum_24h': sum(amounts_24h),
            'tx_amount_avg_1h': np.mean(amounts_1h) if amounts_1h else 0,
            'tx_amount_max_1h': max(amounts_1h) if amounts_1h else 0,
            'tx_amount_std_1h': np.std(amounts_1h) if len(amounts_1h) > 1 else 0,
            'unique_merchants_1h': len(set(e.get('merchant_id') for e in window_1h)),
            'time_since_last_tx': now - events[0]['ts'] if events else 9999,
        }

        return features

    def compute_velocity_features(self, entity_id: str,
                                   event_type: str,
                                   windows: list[int] = [60, 300, 3600]) -> dict:
        """Velocity checks: частота событий за разные окна"""
        features = {}
        now = int(time.time())

        for window in windows:
            key = f"velocity:{entity_id}:{event_type}:{window}"
            # Increment и expire
            pipe = self.redis.pipeline()
            pipe.incr(key)
            pipe.expire(key, window)
            count, _ = pipe.execute()
            features[f"count_{window}s"] = count

        return features

Потоковый инференс с ONNX

import onnxruntime as ort
import asyncio
from aiohttp import ClientSession

class StreamMLInference:
    """Низколатентный инференс в потоке"""

    def __init__(self, model_path: str, feature_store: redis.Redis):
        # ONNX для максимальной скорости
        opts = ort.SessionOptions()
        opts.inter_op_num_threads = 2
        opts.intra_op_num_threads = 2
        opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

        self.session = ort.InferenceSession(
            model_path,
            sess_options=opts,
            providers=['CPUExecutionProvider']
        )
        self.feature_store = feature_store
        self.input_names = [inp.name for inp in self.session.get_inputs()]

    def predict(self, features: dict) -> dict:
        """Инференс < 5ms для tabular модели"""
        # Формирование input tensor
        feature_vector = np.array([[features.get(name, 0.0) for name in self.input_names]], dtype=np.float32)

        start = time.perf_counter()
        outputs = self.session.run(None, {self.input_names[0]: feature_vector})
        latency_ms = (time.perf_counter() - start) * 1000

        score = float(outputs[0][0][1])  # Probability of positive class

        return {
            'score': score,
            'decision': 'block' if score > 0.8 else 'review' if score > 0.5 else 'allow',
            'latency_ms': latency_ms
        }

    def batch_predict(self, features_list: list[dict]) -> list[dict]:
        """Батч-инференс для микробатчей"""
        if not features_list:
            return []

        feature_matrix = np.array([[f.get(name, 0.0) for name in self.input_names] for f in features_list], dtype=np.float32)

        outputs = self.session.run(None, {self.input_names[0]: feature_matrix})
        scores = outputs[0][:, 1].tolist()

        return [
            {'score': s, 'decision': 'block' if s > 0.8 else 'review' if s > 0.5 else 'allow'}
            for s in scores
        ]

Apache Flink пайплайн (Python API)

from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.connectors.kafka import KafkaSource, KafkaSink
from pyflink.common import WatermarkStrategy, Types
from pyflink.datastream.window import TumblingEventTimeWindows, SlidingEventTimeWindows
from pyflink.common.time import Time

def build_flink_ml_pipeline():
    env = StreamExecutionEnvironment.get_execution_environment()
    env.set_parallelism(4)

    # Kafka source
    source = KafkaSource.builder() \
        .set_bootstrap_servers("kafka:9092") \
        .set_topics("transactions") \
        .set_group_id("ml-pipeline") \
        .set_value_only_deserializer(JsonRowDeserializationSchema()) \
        .build()

    stream = env.from_source(
        source,
        WatermarkStrategy.for_monotonous_timestamps(),
        "Kafka Source"
    )

    # Вычисление агрегатов за 5-минутное скользящее окно
    windowed = stream \
        .key_by(lambda event: event['user_id']) \
        .window(SlidingEventTimeWindows.of(Time.minutes(5), Time.seconds(30))) \
        .aggregate(TransactionAggregator())

    # Присоединение к static features из базы
    enriched = windowed.map(EnrichWithStaticFeatures())

    # ML инференс
    scored = enriched.map(MLScoringFunction())

    # Sink: действия в реальном времени
    sink = KafkaSink.builder() \
        .set_bootstrap_servers("kafka:9092") \
        .set_record_serializer(JsonRowSerializationSchema("ml-decisions")) \
        .build()

    scored.sink_to(sink)

    env.execute("ML Streaming Pipeline")

Мониторинг и метрики

class StreamPipelineMonitor:
    """Метрики для real-time ML пайплайна"""

    def __init__(self, prometheus_port: int = 8000):
        from prometheus_client import Counter, Histogram, Gauge, start_http_server

        self.events_processed = Counter('ml_events_total', 'Total events processed', ['decision'])
        self.inference_latency = Histogram('ml_inference_latency_ms', 'Inference latency in milliseconds', buckets=[1, 5, 10, 20, 50, 100, 500])
        self.feature_lag = Gauge('feature_store_lag_ms', 'Time between event and feature availability')
        self.model_score_dist = Histogram('ml_model_score', 'Distribution of model scores', buckets=[0.1*i for i in range(11)])

        start_http_server(prometheus_port)

    def record_inference(self, result: dict):
        self.events_processed.labels(decision=result['decision']).inc()
        self.inference_latency.observe(result.get('latency_ms', 0))
        self.model_score_dist.observe(result['score'])

Сравнение подходов к потоковому инференсу

Подход Латентность (p99) Масштабирование Сложность внедрения
Kafka Streams + ONNX < 50ms Горизонтальное, до 100K ev/s Средняя
Apache Flink + Triton < 80ms До 500K ev/s, stateful Высокая
Spark Streaming + TensorFlow < 200ms До 1M ev/s, микробатчи Средняя

Сравнение online feature store

Решение Latency lookup Масштабирование Цена
Redis < 1ms До 100K ops/s Низкая
DynamoDB < 5ms Автоматическое Средняя
Aerospike < 1ms До 1M ops/s Высокая
Типичные ошибки при построении потокового ML
  • Отсутствие watermarking: события с задержкой могут исказить агрегации. Всегда настраивайте allowed lateness.
  • Игнорирование backpressure: используйте reactive streams или динамический parallelism.
  • Сохранение состояния только в памяти: обязательно используйте checkpointing и репликацию state backend.
  • Прямой вызов моделей в потоке: лучше вынести инференс в отдельный микросервис с очередью.
  • Отсутствие мониторинга: Prometheus + Grafana для latency, throughput, error rate.

Сроки и стоимость

Базовая реализация пайплайна (Kafka + Flink + Feature Store + ONNX) занимает 3-4 недели. Сложные сценарии с кастомными агрегациями и A/B тестированием — до 6 недель. Мы подбираем оптимальный стек под ваш сценарий. Свяжитесь — оценим проект в течение 2 дней.

Что входит в работу

  • Архитектурная схема
  • Код пайплайна с комментариями
  • Дашборды мониторинга (Prometheus/Grafana)
  • Документация
  • Обучение команды
  • Поддержка 1 месяц после запуска

Результаты и экономика

Экономическая эффективность

Переход с batch-пайплайна на потоковый позволяет сократить затраты на инфраструктуру на 30-40% за счёт отказа от промежуточного хранения данных. Средняя окупаемость проекта — 3 месяца. Например, один из клиентов после внедрения снизил chargeback на 35% и сократил время детекции мошенничества с 5 минут до 50 мс.

Пошаговый план внедрения

  1. Аналитика и аудит (2-3 дня): изучаем текущие данные, требования к latency и throughput, выбираем стек.
  2. Проектирование архитектуры (3-5 дней): разрабатываем схему пайплайна, определяем контракты.
  3. Реализация (1-2 недели): пишем код потоковой обработки, feature engineering, интеграцию с ML-моделью.
  4. Тестирование (3-5 дней): нагрузочное тестирование, проверка отказоустойчивости, оптимизация.
  5. Деплой и мониторинг (2-3 дня): разворачиваем в продакшн, настраиваем дашборды и алерты.

Закажите аудит текущей системы и получите коммерческое предложение. Свяжитесь с нами для обсуждения вашего кейса.

Data Engineering для ML: пайплайны, разметка и качество данных

«У нас много данных» — фраза, которая на деле часто означает «у нас много сырых логов в S3, которые никто не трогал два года». Перед тем как обучить модель, нужно понять, что вообще есть: какова структура, есть ли дубли, как часто меняется схема, насколько репрезентативна выборка.

Data Engineering для ML — не просто ETL. Это построение воспроизводимой инфраструктуры данных, которая делает обучение моделей надёжным, а переобучение — предсказуемым. По опыту нашей команды (8 лет в дата-инжиниринге, более 30 проектов в ML) каждая вторая проблема в продакшене связана не с архитектурой модели, а с качеством данных.

ETЛ-пайплайны для ML: чем отличаются от BI

ETL для аналитики и ETL для ML — разные задачи. В аналитике важна агрегация, в ML — индивидуальные записи с историей. В аналитике train/val/test split не нужен, в ML — критичен. В аналитике skew данных мешает интерпретации, в ML — напрямую влияет на качество модели.

Инструменты. Apache Spark (Wikipedia) для больших объёмов (10GB+): PySpark с DataFrames, оптимизации через partitioning и caching. dbt для трансформаций поверх DWH (Snowflake, BigQuery, Redshift) — декларативно, версионируется, тестируется. Pandas + Polars для объёмов до нескольких GB — Polars в 5-10x быстрее Pandas на типичных трансформациях.

Temporal splits. Для ML важно, что split по времени, а не случайный. Если данные временные (транзакции, события пользователей), случайный split даёт data leakage: модель видит «будущие» данные при обучении. Правило: train на периоде T1-T2, validation на T2-T3 (с gap для предотвращения leakage), test на T3-T4. Неправильный split может стоить 10–15% качества модели на валидации.

Инкрементальные пайплайны. Модель переобучается еженедельно на новых данных. Нужен пайплайн, который инкрементально добавляет новые записи к обучающей выборке, не перегружая всё с нуля. Delta Lake или Apache Iceberg — форматы с ACID-транзакциями, Change Data Capture, time travel.

Как избежать training-serving skew с помощью Feature Store

Feature Store решает проблему рассинхронизации между обучением и инференсом. Самая коварная ошибка в ML-инфраструктуре — training-serving skew: признак считается по-разному в обучении и в продакшене. Модель учится на «правильных» данных, а инференс получает другие.

Feast (open source) — офлайн store на Parquet/Delta в S3 для обучения, онлайн store на Redis для low-latency инференса (<10ms). Feature definitions как Python-код:

from feast import FeatureView, Field
from feast.types import Float32, Int64

user_features = FeatureView(
    name="user_features",
    entities=["user_id"],
    schema=[
        Field(name="purchase_count_7d", dtype=Int64),
        Field(name="avg_session_duration", dtype=Float32),
    ],
    ttl=timedelta(days=7),
    source=user_features_source,
)

Один definition, используется везде. Нет расхождений.

Потоковые признаки. Когда признак должен обновляться в реальном времени (количество транзакций за последние 10 минут), нужна потоковая обработка. Apache Kafka + Apache Flink или Kafka Streams для вычисления признаков в реальном времени → запись в онлайн store. Сложнее, дороже, нужно только когда staleness признаков критична для качества.

Разметка данных: как не потратить бюджет впустую

Разметка — самая трудоёмкая и недооцениваемая часть ML-проекта. Плохо размеченные данные не исправит никакая архитектура.

Label Studio — open source, поддерживает разметку изображений (bounding box, polygon, segmentation), текста (NER, классификация), аудио, видео. Поднимается за 10 минут через Docker. Для небольших команд — первый выбор.

Оценка качества разметки. Inter-annotator agreement — насколько согласны разметчики между собой. Cohen's Kappa > 0.8 — хорошо, 0.6-0.8 — приемлемо, < 0.6 — задача неоднозначна или инструкция плохая. Пересечение разметок (10-20% примеров размечают два независимых аннотатора) — обязательная практика.

Active learning. Не размечать случайные примеры, а выбирать те, на которых модель наиболее неуверена (low confidence, high uncertainty). Позволяет добиться того же качества при 50-70% объёма разметки. Modals, Prodigy, Label Studio поддерживают active learning workflows. На одном из проектов для NLP мы сократили бюджет на разметку в 2,5 раза за счёт active learning.

Синтетические данные. Когда реальных данных мало или получить их дорого. Для CV: рендеринг в Blender/Unity с реалистичными текстурами (domain randomization). Для NLP: parafrase через LLM, backtranslation. Риск: модель обучается на distribution синтетических данных, а не реальных — нужна осторожность и проверка на реальном holdout.

Качество данных: валидация и мониторинг

Great Expectations — de facto стандарт для data validation в ML-пайплайнах. Expectations — это декларативные утверждения о данных: «колонка age содержит значения от 0 до 120», «колонка user_id не содержит null», «распределение amount не отклоняется более чем на 20% от baseline». Запускается в пайплайне, при провале — блокирует прохождение.

Pandera — Pythonic alternative для pandas/polars DataFrames. Schema-based validation с type hints:

import pandera as pa

schema = pa.DataFrameSchema({
    "user_id": pa.Column(int, nullable=False),
    "score": pa.Column(float, pa.Check.between(0, 1)),
    "label": pa.Column(str, pa.Check.isin(["positive", "negative", "neutral"])),
})

Data freshness. Модель ожидает данные за последние N дней. ETL упал, данные не обновились — модель использует устаревшие признаки. Мониторинг свежести данных: timestamp последней записи в каждой таблице, алерт при задержке > порога.

Дедупликация. Дубликаты в обучающей выборке завышают метрики (одни и те же примеры в train и val) и искажают веса модели. MinHash LSH для приближённой дедупликации больших датасетов. Для точной — хэш по нормализованному контенту.

Инструменты валидации: сравнение

Инструмент Область применения Когда выбирать
Great Expectations Универсальная, таблицы, пайплайны Большие команды, много метаданных
Pandera pandas/polars DataFrames Python-centric проекты, type hints
Deequ Apache Spark, большие данные Если пайплайн уже на Spark

Хранилища и форматы

Формат Лучше для Особенности
Parquet Батчевое обучение, аналитика Columnar, эффективное сжатие
Delta Lake Инкрементальные апдейты, ACID Time travel, schema evolution
Apache Iceberg Enterprise, multi-engine Лучший catalog, hidden partitioning
HDF5 Числовые массивы (CV датасеты) Иерархическая структура
TFDS / datasets Стандартизованные ML датасеты Hugging Face datasets — удобен для NLP

Для большинства ML-проектов на старте: Parquet в S3 + DVC для версионирования. Delta Lake или Iceberg — когда появляется потребность в инкрементальных обновлениях или time travel.

Что входит в проект по дата-инжинирингу для ML

Мы предоставляем полный цикл:

  • Аудит существующих данных и пайплайнов (1 неделя).
  • Проектирование архитектуры: выбор инструментов, форматов, способов разметки.
  • Реализация ETL/ELT пайплайна с валидацией и мониторингом.
  • Документация кода и процессов (model card, data card).
  • Обучение вашей команды работе с пайплайном.
  • SLA на сопровождение и поддержку.

Как мы строим пайплайн: пошагово

  1. Аудит существующих данных. Профилирование: ydata-profiling (бывший pandas-profiling) генерирует HTML-репорт со статистиками, дистрибуциями, корреляциями, missing values за минуты.
  2. Проектирование пайплайна. Определяем источники данных, частоту обновления, требования к latency признаков, объёмы.
  3. Реализация и тестирование. Unit-тесты на трансформации, integration-тесты на пайплайн, data validation через Great Expectations.
  4. Деплой и мониторинг. Алерты на freshness, quality checks, аномалии в объёмах данных.

Почему стоит доверить это нам

Мы занимаемся дата-инжинирингом и ML с 2016 года. За это время реализовали более 40 проектов — от построения пайплайнов для NLP-моделей до разметки датасетов для компьютерного зрения. Гарантируем воспроизводимость пайплайнов и полную прозрачность процессов. В каждом проекте используем инструменты с открытым исходным кодом, чтобы вы не были привязаны к вендору.

Свяжитесь с нами для бесплатного аудита ваших данных — оценим текущий пайплайн и предложим roadmap. Закажите построение ML-пайплайна под ключ.