Feature Store: centralized ML feature management with online + offline serving. Solves training-serving skew via single feature definition synced to two stores. Tools: Feast (OSS standard), Tecton (managed streaming-first), Hopsworks, Vertex Feature Store. Online store (Redis/DynamoDB) for low-latency model inference. Offline store (S3/Parquet) for training with point-in-time joins. Materialization engine syncs both stores from batch (Spark) and streaming (Flink) compute. Includes ADR considerations for build vs Feast vs managed Tecton.
ML-модель в проде потребляет фичи — предвычисленные числовые сигналы про юзера, айтем, контекст: user_avg_purchase_30d, item_ctr_7d, last_5min_clicks. Без централизованного хранилища каждая команда считает их сама — и сразу появляются три боли.
Train/serve skew. Дата-сайентист тренирует на батч-агрегатах из Snowflake (AVG(amount) WHERE ts < label_ts). Бэкенд в проде считает то же самое на лету через SQL к Postgres, но округляет по-другому, или берёт окно последних 30 суток вместо календарного месяца. Модель в офлайне даёт AUC 0.87, в проде — 0.71. Никто не понимает почему. Это самая дорогая ошибка в ML-системах, и она почти неуловима без инструмента.
Дублирование работы. Команда фрода пишет customer_lifetime_value. Команда рекомендаций пишет её же чуть-чуть иначе. Команда churn — третий вариант. Три SQL, три батча, три бага.
Медленный time-to-prod. Новая фича = новый pipeline = новый Airflow DAG = новый деплой serving-слоя. Месяц вместо дня.
Feature store решает это одной декларацией фичи на два sync'нутых стора: офлайн (исторический, для training) и онлайн (low-latency, для inference). Один источник правды, training-serving skew исчезает по построению.
Git + data warehouse для ML-фич. Одна definition (
@feature_view def user_avg_purchase_30d ...) → materialization engine кладёт значения в два стора одновременно: parquet в S3 для training point-in-time joins и Redis/DynamoDB для онлайн-инференса. Тренировка и продакшен читают байт-в-байт одинаковые значения — потому что считал их один и тот же код.
Главная инвариантa: значение фичи для (entity_id, timestamp) не зависит от того, кто читает — training job или serving API. Если этого нет — feature store сломан.
Четыре функциональных кластера:
Клиент шлёт запрос → serving-api сходил в registry за списком фич для модели v3 → mget из online-store → вектор фич → инференс → ответ. Параллельно фоном крутится materialization: batch и stream считают фичи и пишут в оба стора одновременно (dual-write).
Materialization engine — сердце feature store. Раз в сутки Spark достаёт последние 30 дней транзакций из Postgres (CDC), джойнит с warehouse-дименшнами, считает groupBy(user_id).agg(avg(amount)) на 10M юзеров. Параллельно Flink на стримах окнами по 30 секунд агрегирует last_5min_clicks. Оба пишут результат в одни и те же два стора: S3 (parquet, partitioned by date) и онлайн (Redis MSET с TTL). Ключевое — обе записи делает тот же код, поэтому байт-в-байт идентично.
Онлайн-инференс. Мобильное приложение шлёт POST /predict {user_id: 42, item_id: 99}. Serving API смотрит в кэш registry: «для модели v3 нужны эти 50 фич». Делает один MGET user:42 item:99 к Redis (cluster) → за 1ms получает feature vector. Отдаёт в XGBoost → 5ms скоринг → ответ клиенту с p99 < 15ms end-to-end. Без feature store этот запрос делал бы SQL к warehouse на 5 секунд — катастрофа.
Point-in-time correctness — самое тонкое место. Training set: 1M строк (user_id, item_id, label, event_ts). Для каждой строки нужно подтянуть фичи как они выглядели на момент event_ts, а не сейчас. Если возьмёшь текущие — получишь data leakage: фича purchased_this_item известна только после события, модель «угадает» лейбл в трене и провалится в проде. Feature store делает as-of join: feature_ts <= event_ts AND feature_ts > event_ts - ttl. Тренировка получает исторически корректный фрейм.
Failure mode. Spark batch упал в 03
UTC (OOM). Materialization не отработала. TTL фич — 24 часа, так что online store ещё отдаёт вчерашние значения — без ошибок, тихо. Serving API получаетuser_avg_purchase_30d = 145.20 (вчерашнее) вместо 162.80 (сегодня). Predictions деградируют постепенно, AUC падает на 0.03. Никто не замечает несколько часов, пока не сработает model performance alert. Это типичный сценарий — почему feature freshness — критическая SLI.
Дву-сторовая архитектура vs single store. Один Redis для всех нужд проще, но не выдержит point-in-time training joins (S3 + columnar storage там на порядок дешевле и быстрее на full scan). Один Snowflake — наоборот, не даст 1ms latency. Поэтому дублирование данных в двух сторах — фича, не баг. Цена — materialization engine должен поддерживать invariant.
Batch vs streaming materialization. Batch (Spark daily) проще, дешевле, но фичи устаревают на сутки. Streaming (Flink) — sub-second свежесть, но в 5-10× дороже compute, плюс сложная on-call. Гибрид: critical features (recency, last_5min_*) на стримах, остальное батчем. Tecton делает это first-class, Feast — через два разных feature view.
TTL на online фичах. Без TTL — stale data копится тихо и подтачивает модель. С агрессивным TTL — кеш-промахи на «холодных» юзерах, инференс деградирует. Компромисс: TTL = 2× ожидаемого materialization interval (батч раз в сутки → TTL 48 часов), плюс отдельный alert на feature freshness lag.
Build vs buy. Feast (OSS) даёт спецификацию и тонкий слой, но всю инфру (Spark, Airflow, мониторинг) пишешь сам — это месяцы работы. Tecton/Hopsworks/SageMaker Feature Store — managed, но дорого и vendor lock. Точка перелома: ~3 продакшен-моделей или ~5 инженеров на ML — раньше Feast хватает, после — managed окупается.
Точность point-in-time joins vs скорость. Полный as-of join на 1M строк × 100 фич — это десятки минут на Spark. Approximate (берём ближайший snapshot за день) — секунды, но добавляет leakage риск. Feast и Tecton предлагают exact, многие in-house платформы — approximate с явным warning.
avg_30d), не для сырых событий. Иначе serving делает aggregation на лету = 100ms latency = провал.user_avg_purchase_30d (с 30 дней на 28 для удобства) — все обученные на старой версии модели начали считать другое. Нужны immutable feature view versions.user.age, user.country). Достаточно DB-lookup в serving.