惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
Engineering at Meta
Engineering at Meta
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
D
Docker
Vercel News
Vercel News
云风的 BLOG
云风的 BLOG
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
DataBreaches.Net
IT之家
IT之家
V
V2EX
人人都是产品经理
人人都是产品经理
F
Fortinet All Blogs
J
Java Code Geeks
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
雷峰网
雷峰网
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
P
Privacy International News Feed
F
Full Disclosure
P
Proofpoint News Feed
B
Blog RSS Feed
T
Tor Project blog
T
The Blog of Author Tim Ferriss
B
Blog
Webroot Blog
Webroot Blog
腾讯CDC
T
Troy Hunt's Blog
T
Tailwind CSS Blog
H
Heimdal Security Blog
AWS News Blog
AWS News Blog
G
Google Developers Blog
Spread Privacy
Spread Privacy
NISL@THU
NISL@THU
A
About on SuperTechFans
SecWiki News
SecWiki News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
I
InfoQ
M
MIT News - Artificial intelligence
大猫的无限游戏
大猫的无限游戏
美团技术团队
L
LangChain Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Avalon: как построить эффективный Feature Store на YDB
Павел Попов · 2026-05-08 · via Все публикации подряд на Хабре

В современном развитии рекомендательных систем и алгоритмов принятия решений особое место занимают Feature Store — хранилища признаков, позволяющие быстро и централизованно управлять данными. В городских сервисах Яндекса для таких задач мы создали собственное решение под названием Avalon. Оно служит универсальным каталогом признаков, которым легко пользоваться разработчикам и аналитикам вне зависимости от того, что им нужно хранить — бинарные индикаторы или сложные метрики вроде количества поездок у водителя.

Наш Feature Store — Avalon — возник в момент, когда понадобилось масштабируемое и производительное хранилище с низкой задержкой, в котором можно структурировать признаки по иерархии «каталог/файл», получать быстрый доступ к ним из рантайма, автоматически отслеживать актуальность данных и контролировать жизненный цикл каждого признака. Роль СУБД для системы выполняет YDB, что позволяет достичь высокой отказоустойчивости и горизонтального масштабирования.

Всем привет! Меня зовут Паша, я руковожу группой разработки технологий эффективности Такси. В этой статье я расскажу, как мы проектировали и строили Avalon, какие вызовы пришлось решать команде по мере роста нагрузок и аудитории, почему прежние подходы перестали соответствовать задачам современного продуктового анализа и как в результате получился удобный и надёжный Feature Store для множества бизнес-сценариев.

С чего всё началось

Итак, начинается понедельник. Я открываю Яндекс Трекер и вижу такую задачу:

«Раз в сутки мы размечаем атрибуты пользователей. А дальше, в рантайме, хотим принимать решения в зависимости от того, проставлен атрибут на пользователя или нет».

К задаче прилагаются характеристики: RPS, объёмы данных и так далее. Всего признаков — 2 млрд, из которых ежедневно обновляются около 2 млн. Очевидно, им нужно место, где всё это хранить. Причём задача общая: такая функциональность может пригодиться и другим сервисам.

У команды уже была структура данных и тип ключа, по которому всё строится, а наша задача состояла в том, чтобы обеспечить консистентный доступ к этим данным в рантайме. Нам принесли YT-таблицу, в которой примерно 1,7 млрд записей. Нужно было сделать так, чтобы обращаться в рантайме к этим данным можно было с большим RPS, а при необходимости — кешировать их и быстро получать ответ, проставлен атрибут пользователю или нет.

Далее на основе признака «есть атрибут/нет атрибута» сервис решает, что показать пользователю. В более общем смысле наша задача — дать сервисам универсальный способ получать обновляемые данные с низкой latency, без участия разработчиков или других команд.

Разметка аудитории — это типовая задача, и до Avalon в Городских сервисах уже были другие решения. Его предшественник — сервис с PostgreSQL под капотом для хранения бинарных признаков. Он задумывался как способ быстро и экспериментально размечать аудиторию. И на основе полученных результатов заказывать доработки в нужных сервисах.

Сервис быстро стал популярным. Однако пользователи — аналитики и разработчики — столкнулись с проблемами:

  • Пользователи хотели загружать всё больше и больше данных. А PostgreSQL нужно масштабировать вручную.

  • Признаки в сервисе имели бинарную природу: отвечали на вопрос только «да» или «нет». Но если мы хотим узнать, например, количество поездок водителя за всё время, то такой подход уже не поможет.

Так появился Avalon — инструмент, позволяющий удобно хранить, структурировать и быстро получать самые разные пользовательские признаки. Название Avalon выбрал разработчик, который вдохновляется кельтской мифологией. Согласно современному толкованию, миф об Авалоне — отголосок сказаний об «островах блаженных». В общем, это что-то похожее на родные нашему уху «кисельные реки и молочные берега», отсылающие к тем же самым образам комфорта и изобилия, которые мы вкладывали в саму идею системы.

Требования к системе

При разработке Avalon у нас было несколько важных требований:

  • Система должна неограниченно масштабироваться. Если у пользователя есть железо, он приносит его нам, и мы без особых усилий заливаем его данные.

  • Должна быть возможность хранить не только бинарные признаки вроде «пользователь с атрибутом или нет», но и произвольные признаки, например количество поездок, совершенных пользователем за всё время.

  • Должен быть удобный реестр признаков: понимать, кто ими пользуется в рантайме, кому они нужны и сколько времени каждый признак должен жить. Если признак нужно удалить, это должно происходить безопасно и предсказуемо.

Также мы уделили особое внимание пользовательскому опыту. Пользователю всё равно, как именно устроено внутреннее хранилище признаков. Для него есть один эндпоинт — fetch. Пользователь делает запрос по идентификатору и получает значение одной или нескольких фич. Вот пример такого запроса и ответа:

curl --env testing avalon-proxy \
  -v avalon-proxy.somewhere/v1/fetch \
  -X POST -H "Content-Type: application/json" \
  -d '{"id": "123", "features": ["/service-general/{}/features/with_attribute"]}'

{
  "id": "123",
  "features": {
    "/service-general/{}/features/with_attribute": {
      "data": {
        "@type":
        "type.googleapis.com/NUgc.NSchema.NAvalon.TGgeneralFeature",
        "name": "with_attribute"
      }
    }
  }
}

Архитектура Avalon: общее устройство

Точка входа в систему — сервис Avalon Proxy. Мы строили систему так, чтобы данные хранились в абстрактных хранилищах, а для пользователя это было скрыто за прокси.

Вернёмся к поставленной в начале статьи задаче. Нам нужно решить вопрос загрузки данных по атрибутам на пользователей — это один из сценариев. С другой стороны, данные могут поступать через рантайм, API и другие источники. Главная цель остаётся прежней — обеспечить доступ к аналитическим данным в реальном времени.

Ядром системы выступает UGCDB Framework — это key-value хранилище, которое задаёт определённые правила работы с данными. UGCDB позволяет описывать логическую схему данных. Если у одной записи может быть несколько подзаписей (листов у узла), это называется таблицей в контексте UGCDB Backend. Внутри таблицы могут находиться другие таблицы — как папки внутри папок. Если у записи может быть только одно значение, это колонка. Эти понятия и ограничения задал сам фреймворк, который был написан до нас.

Под капотом — YDB. Для нас это удобно прежде всего тем, что YDB обеспечивает шардирование «из коробки». Кроме того, в системе есть автоматическая генерация API для доступа к данным. Данные у нас представлены в виде логической таблицы, и это тоже даёт преимущества. Мы можем описать структуру данных с таблицами и колонками, как она выглядит логически, но под капотом это одна физическая таблица. Благодаря этому таблицу удобно шардировать.

И ещё одно преимущество — наличие CDC (Change Data Capture). Так мы можем отслеживать изменения данных с течением времени, что полезно, например, для финансовых данных.

Архитектура Avalon: хранение данных

В физической таблице перечислены следующие поля:

Физическая схема таблицы

Физическая схема таблицы

В исходниках нашей задачи — 2 млрд записей. Для них мы описали логическую схему данных следующим образом:

  • есть таблица ServiceGeneral;

  • внутри неё — таблица features;

  • внутри features лежат данные.

message TRoot {
  repeated TGeneralFeatureStorage ServiceGeneral = 1;
  option (schema) = {
    Table: {
      Name: "service-general"
      Field: "ServiceGeneral"
      Key: "Id"
    }
  }
}

message TGeneralFeatureStorage {
  string Id = 1 [json_name = "id"];
  repeated TGeneralFeature Features = 2 [json_name = "features"];

  option (schema) = {
    Table: {
      Name: "features"
      Field: "Features"
      Key: "Name"
    }
  };
}

message TGeneralFeature {
  string Name = 1 [json_name = "name"];
  google.protobuf.Value Payload = 2 [json_name = "payload"];
}

Если вспомнить, в YT-таблице, которую приносили коллеги, прямо указано:

name = with_attribute

Таким образом мы загрузим в поле Name структуры TGeneralFeature значение with_attribute для каждого идентификатора из YT-таблицы.

Кроме того, есть произвольное поле payload, поскольку система решает задачи не только с бинарными признаками, но и с признаками, которые содержат произвольный набор данных.

Теперь посмотрим, как логическая схема соотносится с тем, как мы получаем данные. Для запроса вызывается endpoint

avalon-proxy.somewhere/v1/fetch

В качестве фичи пользователь передаёт путь:

/service-general/123/features/with_attribute

Это признак пользователя с атрибутом в домене service-general. Мы называем доменом тот префикс, который указан в начале — здесь это service-general. Таким образом, можно получить точное значение признака для пользователя с идентификатором 123.

Если нужно получить все признаки по пользователю, запрос выглядит так:

/service-general/123/features

В ответ возвращаются все признаки, которые есть у пользователя.

Архитектура Avalon: работа с СУБД

Теперь поговорим об обновлении и получении данных в YDB. Доступны следующие операции:

  • точечное чтение и range-based-чтение,

  • точечная запись,

  • модифицирующие запросы с сompare-and-swap,

  • точечное удаление,

  • batch-запросы.

Когда приходит запрос, то выполняется обычный SELECT. Мы определяем номер шарда хешированием и передаём в качестве ключа нужный признак.

DECLARE $shard_id AS Uint32; // CityHash(root level id -> 123)
DECLARE $key_id AS String; // "/service-general/123/features/with_attribute"
SELECT ShardId, Key, ValueProto
FROM `data`
WHERE ShardId = $shard_id AND Key = $key;

Кроме того, можно получить все признаки, а не только with_attribute. Для этого в запросе используется range-based-чтение:

DECLARE $shard_id AS Uint32;
DECLARE $start_key AS String;
DECLARE $end_key AS String;
SELECT ShardId, Key, ValueProto 
FROM `data`
WHERE ShardId = $shard_id AND Key >= $start_key AND Key < $end_key;
  • начало диапазона — "/service-general/123/features/ ", где " " — минимальный ASCII символ, разрешенный для использования в названиях.

  • конец диапазона — "/service-general/123/features/~", где "~" — последний ASCII-символ, разрешенный для использования в названиях.

Так можно получить все признаки разом.

Также поддерживается batch-чтение: это обычный SELECT по набору фич для одного идентификатора, чтобы получить всё одним запросом.

DECLARE $shard_id AS Uint32;
DECLARE $key_list AS List<Tuple<Uint32?,String?>>;
SELECT ShardId, Key, ValueProto
FROM `data`
WHERE (ShardId, Key) IN $key_list;

Модификации выполняются обычным upsert.

// Запись
DECLARE $shard_id AS Uint32;
DECLARE $key AS String;
DECLARE $value AS String;
...
UPSERT INTO `data` (ShardId, Key, ValueProto, ...)
VALUES ($shard_id, $key, $value, ...);
// для IfNotExist и IfNotEqual используются интерактивные транзакции
// логика их обработки реализована на уровне приложения
// Удаление
DECLARE $shard_id AS Uint32;
DECLARE $key AS String;
DELETE FROM `data` WHERE ShardId = $shard_id AND Key = $key;

Архитектура Avalon: загрузка и выдача данных

Чтобы решить задачу загрузки данных из YT-таблицы, используется процедура импорта. Процесс выглядит так:

  1. Читается внутреннее состояние системы.

  2. Читается состояние таблицы, полученной в данный момент.

  3. Внешний сервис считает разницу между ними.

  4. Подготавливается таблица для загрузки.

  5. Скрипт на C++ загружает данные в UGCDB Backend.

  6. Backend записывает данные в YDB.

  7. Загрузка выполняется батчами, а в атрибутах YT-таблицы записывается прогресс, чтобы при сбое можно было продолжить с того же места.

После загрузки данных пользователь должен иметь возможность их получить. Для этого написаны два сервиса: Avalon Proxy и Avalon Data Processor. Они решают задачу абстрагирования пользователя от внутреннего устройства хранилища. Кроме того, в них реализован ACL: определено, кто может потреблять конкретный признак, с каким RPS и на каком объёме железа, в зависимости от договорённости с командой.

В Avalon Proxy реализован усечённый API. Если UGCDB Backend позволяет произвольный доступ к данным, то в Proxy всё строже: здесь запросы только с одним верхеуровневым идентификатором, чтобы гарантировать одношардовость батчей. Это нужно, чтобы контролировать нагрузку и предсказуемо оценивать SLA.

Avalon Data Processor решает задачу ACL — управления доступом к признакам. В отличие от сервиса предшественника, где было неясно, кто что использует, здесь все пользователи известны и ограничения задаются явно. ACL определяет, какие сервисы могут потреблять конкретные признаки: например, service_loyalty получает один набор фич, а service_perks — другой. Avalon Proxy на этапе запроса проверяет права и ограничивает нагрузку.

{
  "service-loyalty": {
    "/service-general/{}/features": {
    }
  },
  "service-perks": {
    "/service-general/{}/features": {},
    "/passport-uids/{}/service-payment-methods": {},
    "/passport-uids/{}/service-prev-business-purchases": {},
    "/passport-uids/{}/personal-goals": {}
  }
}

Так замыкается контур: данные из YT-таблиц загружаются в систему, а клиент получает их через Avalon Proxy.

Интеграция оказалась быстрой: на стороне сервиса это всего около двадцати строк кода на C++.

std::vector<std::string> AvalonProxyService::GetAvalonGeneralFeatures() const {
auto response_opt = GetGeneralFeaturesData(avalon_get_features_task_);
if (!response_opt) {
  return {};
}
auto response = response_opt.value();
std::vector<std::string> result; result.reserve(response.GetFeatures().size());
for (const auto& feature : response.GetFeatures()) {
   if (feature.GetName() == kServiceGenderPromo) {
       continue;
   }
    if (feature.HasPayload()) {
        const auto& payload = feature.GetPayload();
        if (payload.has_struct_value()) {
           const auto& protoMap = payload.struct_value().fields();
           const auto* suffixes = MapFindPtr(protoMap, "suffixes");
           if (suffixes) {
              if (suffixes->has_list_value()) {
                 for (const auto& suffix : suffixes->list_value().values()) {
                     if (suffix.has_string_value()) {
                        result.push_back(feature.GetName() + '_' +             suffix.string_value());
                        boost::to_upper(result.back());
                 } 
               }
             }
             continue;
           }
         }
       }
     }
    result.push_back(feature.GetName());
    boost::to_upper(result.back());
   }
   return result
}

После этого аналитики сами добавляют новые данные, без участия разработчиков.

Архитектура Avalon: Change Data Capture

В современных распределённых системах данные редко лежат мёртвым грузом. Часто их настоящая ценность раскрывается в движении — когда изменения актуального состояния немедленно становятся событием, на которое могут подписаться другие сервисы. Это основа для аналитики и сложных бизнес-процессов. Но как надёжно и без потерь фиксировать историю изменений, если основная база данных хранит лишь последнюю версию записи?

Представьте себе сервис скидок. База данных хранит актуальный размер скидки для каждого пользователя. Это эффективно для быстрых запросов. Но для аналитиков и алгоритмов важно знать не только текущее значение, но и то, когда скидка изменилась, какой она была раньше и кто инициировал это изменение.

Возникает задача: нужно в реальном времени захватывать каждое изменение (INSERT, UPDATE, DELETE) в основной таблице, обогащать его метаданными и доставлять в виде потока событий для дальнейшей обработки. Именно эту задачу и решает CDC, который в нашей имплементации называется Feeder.

Поток данных выглядит так:

  • Перед применением изменения приложение (UGCDB Backend) читает актуальное состояние записи, формирует запись для истории изменений и затем вносит ее вместе с основным изменением в таблицу YDB.

  • Feeder читает специальные записи истории изменений и перекладывает их в YDB Topics.

  • Подписчики топика (другие сервисы) потребляют события.

// Упрощённая схема сообщения
message TFeedRecord {
  string Key = 1;
  TRecordMeta OldMeta = 2;
  google.protobuf.Any OldValue = 3;
  TRecordMeta NewMeta = 4;
  google.protobuf.Any NewValue = 5;
  TRequestMeta RequestMeta = 6; // Контекст запроса
}

В случае горизонтального масштабирования количество экземпляров Feeder всегда будет равно количеству шардов в исходной таблице YDB. Каждый Feeder закреплён за своим шардом. В рамках одного шарда Feeder гарантирует, что события будут отправлены в топик ровно в той последовательности, в которой изменения произошли в БД. Это критически важно для корректного восстановления состояния. Для этого используются монотонные счётчики YDB updateEpoch и updateNumber, которые увеличиваются при каждой новой записи.

Важно отметить, что после успешной записи в топик, Feeder удаляет эту запись из базы. Если в этот момент произошла ошибка и запись не удалилась, то при последующем запуске YDB topics на своей стороне отфильтрует её как уже обработанную. Таким образом у Feeder есть гарантии доставки «ровно один раз».

А для наших пользователей всё это выглядит простым добавлением атрибутов в protobuf-схеме.

message TRoot {
  repeated TGeneralFeatureStorage DiscountsGeneral = 1 [(topic) = "discounts-general-json"];

  option (schema) = {
    Table: {
      Name: "discounts-general"
      Field: "DiscountsGeneral"
      Key: "Id"
    }
  };
}

Результаты

Avalon решает сразу несколько важных задач: централизует работу с признаками, обеспечивает производительность и масштабируемость хранения, а также делает интеграцию новых данных и сценариев максимально простой как для разработчиков, так и для аналитиков. В результате мы создали не просто удобный каталог данных, а надёжную платформу, которая устойчиво работает даже под высокими нагрузками и в периоды максимальной активности.

Сейчас в системе хранится около 3 ТБ данных и 6,5 млрд ключей. Всего используется 2048 шардов YDB. Нагрузка — 100 тысяч RPS на чтение, 30 тысяч RPS на запись, при этом задержка в P95 — около 5 мс.

Главный показатель успешности системы — стабильность под нагрузкой. Даже при крупных запусках, когда признаки активно используются, дежурных не приходится тревожить, а мониторинг не подаёт сигналов. Это значит, что Avalon работает надёжно и справляется со своей задачей.

Развитие Avalon не останавливается: впереди — новые источники данных, автоматизация контроля качества и расширение возможностей для интеграций с внешними сервисами. Мы в команде уверены, что такой подход продолжит давать командам возможности быстрой проверки своих продуктовых гипотез.