Привет, Хабр! Меня зовут Дмитрий Наумов, я работаю в команде Yandex Cloud Postbox. Это API для надёжной доставки писем. В статье поделюсь опытом, как мы развивали аналитику в нашем сервисе: на старте она спокойно уживалась в общей транзакционной СУБД с операционными данными, но с ростом нагрузки всё изменилось. Данные переместили в отдельное хранилище, и нужно было выбрать способ трансфера данных между операционной базой и этим хранилищем.

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

Фаза 1: молодой сервис с одной OLTP-СУБД

Yandex Cloud Postbox — это сервис, который получает по API задачи на доставку и быстро раскладывает миллионы писем в сутки по почтовым сервисам. Доставлять письма в современном мире так, чтобы они не попадали в спам, — задача нетривиальная. В первую очередь потому, что любым публичным сервисом пытаются воспользоваться спамеры. Чтобы бороться с ними и не допускать попадания сервиса в блэклисты, нужно много аналитики.

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

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

Нам нужна была пакетная обработка, поэтому первые версии сервиса использовали самый простой подход: аналитическая информация сохранялась в строковые OLTP-таблицы нашей основной базы данных, работавшей на Managed Service for YDB. СУБД Яндекса выбрали для сервиса из-за нового и перспективного подхода к определённым транзакциям и масштабируемости, а когда команда разработки рядом — всегда можно на них влиять. У такого подхода есть весомые плюсы: весь код для работы с данными и СУБД уже написан, для работы с аналитикой нужно минимум изменений, и все аналитические данные доступны сразу же.

Однако аналитические запросы могут влиять на выполнение транзакционных. По своей природе транзакционные запросы обращаются к небольшой части данных и выполняются быстро. Большинство из них пишет или читает одну строку в таблице и отрабатывает за несколько миллисекунд.

Аналитический запрос, наоборот, может читать таблицу целиком и выполняться за минуты, а то и за часы. Если параллельно транзакционные запросы дописывают данные в ту же таблицу, то они конкурируют с выполняющимся аналитическим запросом за ресурсы: ядра процессора, объём свободной памяти и пропускную способность дисков.

Когда объём аналитических таблиц превысил несколько миллионов строк, мы начали замечать, что аналитические дашборды открываются всё медленнее. Если раньше они строились практически сразу, то со временем ожидание доходило до нескольких десятков секунд. При этом деградация была заметна и в транзакционной части сервиса: тяжёлые аналитические запросы начинали конкурировать с боевой нагрузкой за ресурсы. Это стало сигналом к тому, что пора выносить аналитические данные в отдельную базу.

Фаза 2: отдельная база с автоматической загрузкой данных

Первый вариант, который мы решили попробовать, — это вынести аналитическую нагрузку в отдельный инстанс СУБД с собственными таблицами, индексами, балансировкой нагрузки и лимитами. Это позволило разделить ресурсы между боевой и аналитической нагрузкой, а перенос данных мы автоматизировали с помощью Yandex Data Transfer — готового механизма облачной платформы.

Мы установили второй инстанс СУБД, включили перенос данных, переключили аналитические запросы в коде на новый инстанс — и время отклика транзакционных запросов вернулось к минимальным значениям.

Сама миграция оказалась довольно простой: за пару дней мы настроили перенос данных в фоне без изменения схемы и без остановки сервиса. Позже оказалось, что для аналитиков ценно менять схему и настраивать миграцию под свои нужды. Наши потребности выходили за рамки типовых сценариев, которые предлагал Yandex Data Transfer.

Мы установили минимальный TTL для данных в операционной базе, а в аналитической базе стали хранить данные до года. Длительное хранение данных, в свою очередь, позволило нам настроить новые дашборды в DataLens (мы уже писали о поддержке DataLens на Хабре).

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

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

Про разные типы нагрузки разработчики YDB уже писали на Хабре.

Фаза 3: одна строка SQL, чтобы перейти на колоночные таблицы

YDB, которую мы используем, поддерживает и строковые, и колоночные таблицы. Для создания колоночной таблицы достаточно в CREATE TABLE указать STORE = COLUMN:

CREATE TABLE article_column_table (
    id Int64 NOT NULL,
    author String,
    title String,
    PRIMARY KEY (id)
)
WITH (STORE = COLUMN);

После перехода на колоночные таблицы нам не пришлось менять клиентский код, зато сами аналитические запросы стали выполняться заметно быстрее. Если раньше дашборд с 5–7 графиками открывался за 30–60 секунд, то после перехода — примерно за 3–5 секунд.

В клиентском коде при этом ничего менять не нужно: тот же SDK (на момент написания статьи доступно 8 SDK и более 70 фреймворков разной степени проработанности), то же подключение к базе, тот же код для выполнения запросов.

В новом сетапе мне нравилось всё, кроме зависимости от Yandex Data Transfer. Его возможности по настройке через админку ограничены типовыми сценариями: можно было быстро настроить простой трансфер данных, но для других настроек приходилось создавать тикеты для команды поддержки, и цикл проверки гипотез заметно увеличивался. Например, задача сделать составной ключ из datetime и guid для аналитики уже требовала тикета в поддержку.

По моему опыту, дата-аналитикам комфортнее всего работать, когда они могут проверять свои гипотезы быстро. Для этого им нужно самим настраивать трансфер данных под свои задачи и сразу же видеть результат в Python Notebook. Обычно такие пайплайны разрабатывают дата-инженеры с использованием Kafka, микросервисов, отдельных ресурсов на железо и бюджетов на разработку. Мы решили протестировать ещё один способ, как копировать данные из строковых таблиц в колоночные.

Фаза 4: топики и трансферы — когда СУБД закрывает потребность в брокере сообщений

Для нас оказалось полезным, что под управлением одной СУБД можно использовать и строковые таблицы, и колоночные таблицы, и топики. Благодаря этому данные можно передавать между ними без появления отдельной инфраструктуры вроде Kafka.

Работает это по такой схеме. В исходной базе данных формируется топик, содержащий поток изменений (CDC‑события) для целевой таблицы. Компонент трансфера, развёрнутый в контексте целевой базы данных, устанавливает подключение к указанному топику. Трансфер извлекает поступающие изменения и применяет их к таблице в целевой базе, обеспечивая синхронизацию состояния.

Копирование данных между ними можно организовать средствами самой СУБД — командой CREATE TRANSFER (механизм называется YDB Transfer):

CREATE TABLE example_table (
    partition Uint32 NOT NULL,
    offset Uint64 NOT NULL,
    message Utf8,
    PRIMARY KEY (partition, offset)
);

CREATE TOPIC example_topic;

$transformation_lambda = ($msg) -> {
    return [
        <|
            partition: $msg._partition,
            offset: $msg._offset,
            message: CAST($msg._data AS Utf8)
        |>
    ];
};

CREATE TRANSFER example_transfer
    FROM example_topic TO example_table USING $transformation_lambda;

Обратите внимание на transformation_lambda в примере кода из документации. С помощью этой функции можно задать собственные правила трансформации данных при трансфере — нам это позволяет быстро проверять гипотезы.

После перехода на нативные трансферы YDB архитектура снова изменилась: вместо тонкой настройки TTL пришлось следить за самим трансфером — в первую очередь за лагом консьюмеров (отставанием доставки данных) и общим состоянием репликации.

Мои личные эксперименты с данными стали занимать не больше одного дня — и это с учётом других задач и перерывов на кофе. Можно быстро «поиграть с данными» и посмотреть, не прячется ли в них что-нибудь интересное. Такие находки случаются нередко и позволяют нам быстрее находить спамеров и улучшать сервис. В нашей документации есть примеры того, как можно использовать YDB Transfer.

У такого подхода тоже есть своя цена. В отличие от использования Data Transfer с готовым интерфейсом, здесь нужно понимать устройство топиков, CDC и самого механизма трансфера. Зато если команда уже работает с этими компонентами, появляется возможность самостоятельно описывать нужные преобразования данных и не ждать появления подходящей настройки в универсальном инструменте.

Попробуйте YDB Transfer в своих проектах

YDB (СУБД Яндекса) с её компонентом YDB Topics доступна как опенсорс-проект и как коммерческая сборка с открытым ядром. Вы можете запустить её на своих серверах или воспользоваться нашим managed-решением в Yandex Cloud. Всё, о чём я рассказал в этой статье, можно собрать, запустить и протестировать даже на ноутбуке (а один из разработчиков недавно писал на Хабре, как оптимизирует YDB для использования на ноутбуках для отладки и прототипирования).

Команда YDB общается с пользователями в Telegram и на Хабре. Если вы разрабатываете системы, где данные нужно перемещать между хранилищами и обрабатывать, то в комментариях к этой статье я буду рад обсудить архитектуру и другие вопросы.

Комментарии (2)


  1. Tenphi
    27.08.2026 13:47

    А рассматривали Cube/Cube Store как serving layer поверх колоночной YDB?

    OLTP YDB → Topic/Transfer → column YDB → Cube Store → dashboards

    YDB оставалась бы источником сырых данных для ноутбуков и новых гипотез, а Cube управлял бы семантической моделью и преагрегациями для фиксированных дашбордов. Полностью заменить аналитическую таблицу Cube Store, вероятно, не сможет: после TTL исходных данных нельзя построить новый rollup с ранее не сохранённым измерением. Но как дополнительный слой для высокой конкуренции и стабильной latency он выглядит интересно. Рассматривали такой вариант?


    1. dmitry_naumov Автор
      27.08.2026 13:47

      Нет, не рассматривали. Одной из задач было уменьшить стоимость/сложность эксплуатации всего решения, а производительности DataLens поверх колоночных таблиц нам более чем хватает.