Внутреннее устройство YDB: акторы и таблетки

Когда мы начинали разрабатывать собственную СУБД, перед нами стояли чёткие задачи, продиктованные требованиями Яндекса. И тогда, и сейчас в компании параллельно запускаются десятки внутренних стартапов — и большинство из них быстро вырастает с тысяч пользователей до миллионов.

Одно из основных требований к YDB — бесконечная линейная горизонтальная масштабируемость. Горизонтальная — значит расширение вычислительных ресурсов и объёма хранения достигается за счёт добавления серверов, а не наращивания мощности одного. Линейная — значит прирост производительности пропорционален числу добавленных машин. А «бесконечная» — значит, масштабируемость ограничена только бюджетом и количеством доступного на рынке оборудования. Сегодня крупнейшие инсталляции YDB в Яндексе обрабатывают миллионы запросов в секунду и работают с петабайтами данных.

Сделать СУБД с такими характеристиками — не самая тривиальная задача. Не буду пересказывать историю разработки архитектуры и сразу перейду к тому, что получилось и было в 2022 году выложено как открытое ядро, которое сейчас также доступно для клиентов в виде коммерческой сборки. В архитектуре YDB есть несколько ключевых решений, о которых я хотел бы рассказать, прежде чем поговорить о менеджере смешанной нагрузки.

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

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

Получив входящее сообщение, код актора выполняет свою задачу и может как создавать новых акторов, так и отправлять сообщения существующим. Акторы выполняют роль строительных блоков для механизмов масштабирования YDB: код акторов максимально предсказуем, они одинаково работают как локально на одном сервере, так и при отправке сообщений по сети.

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

Таблетки умеют сохранять и считывать из распределённого хранилища не только данные таблиц СУБД, но и своё собственное состояние. Если сервер, на котором выполнялась таблетка, выходит из строя, то YDB достаточно пересоздать эту таблетку на другом сервере: свежесозданная таблетка вычитает из распределённого хранилища своё состояние и продолжит работу с того места, где она была прервана.

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

Менеджер смешанной нагрузки — один из механизмов высокого уровня, позволяющих распределять нагрузку так, как нужно пользователям YDB. Если этого не делать, то даже одного OLAP запроса может быть достаточно, чтобы нагрузить любой кластер и заметно снизить его способность к обработке большого потока OLTP-запросов.

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

Как YDB выполняет запрос

Менеджер смешанной нагрузки
Менеджер смешанной нагрузки

В распределённой базе данных выход из строя любого сервера не останавливает её работу. Поэтому все серверы слоя вычисления YDB умеют выполнять любые запросы пользователей. Когда клиентский SDK подключается к YDB, то он устанавливает gRPC-подключение к одному из вычислительных серверов. Этот сервер будет получать от клиента запросы и запускать их выполнение. А если с сервером что-нибудь случится во время работы, то SDK автоматически переподключится к другому.

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

Мы уже писали на Хабре, как работает оптимизатор запросов YDB. А результатом его работы является граф выполнения, который описывает, из каких этапов будет состоять выполнение запроса и в каком порядке эти этапы исполнять. После построения плана YDB запускает акторы, которые исполняют запрос: получают данные от таблеток, выполняют JOIN и другие операции, отдают данные обратно таблеткам на запись, возвращают пользователю результат.

Акторы, которые выполняют спланированный запрос, мы называем «тасками». Это не «таблетки», потому что у тасок нет персистентного состояния: если во время работы такого актора с сервером что-нибудь случится, то запрос завершится с ошибкой и клиенту нужно будет выполнить его ещё раз. Таски отправляют сообщения таблеткам, запрашивая у тех нужные для выполнения запроса данные. А таблетки, в свою очередь, могут отдавать данные из кеша в памяти и запрашивать их по сети из распределённого хранилища.

Такое большое количество участников процесса нужно для того, чтобы обеспечить масштабируемость. Чем больше серверов YDB используется, тем больше тасок и таблеток могут выполнять запросы — они выступают единицей параллелизма. При этом клиенты подключаются к разным серверам, а акторы распределяются так, чтобы сеть не становилась «узким местом» в системе.

Акторы, за которыми надо следить

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

Акторам надо держать в памяти промежуточные данные. И чем больше акторов одновременно выполняется — тем больше памяти нужно. Если памяти кластера перестанет хватать, запросы начнут завершаться с ошибками выполнения.

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

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

Особенно опасна для кластера аналитическая нагрузка. OLAP-запросы часто выполняются над огромными таблицами и содержат десятки JOIN. Для их выполнения YDB создаёт много акторов-тасок, которые обмениваются сообщениями с акторами-таблетками. И чем больше данных нужно обработать — тем больше таких сообщений. Если никак не управлять нагрузкой на кластер, то запущенный аналитический запрос способен понизить RPS для всей остальной OLTP-нагрузки.

Маленький кораблик транзакционной нагрузки и девятый вал аналитической иллюстрируют, как именно аналитическая нагрузка влияет на транзакционную. Их тяжело совмещать:

Слайд презентации
Слайд презентации

Чтобы пользователи YDB могли управлять работой своих кластеров, мы научили планировщик акторов собирать статистику того, как акторы выполняются. Для каждого актора замеряется время выполнения кода обработки сообщений и занимаемая ими память. Основываясь на заданных пользователем ограничениях, YDB может вызывать код актора чаще или реже. Или вообще перестать создавать новые акторы, если превышена квота по использованию памяти.

Устройство менеджера смешанной нагрузки

Настройки балансировки задаются для пулов ресурсов, и YDB автоматически выбирает один из пулов для каждого запроса. Пул выбирается по пользователю, который сделал запрос. А если пользователь находится в списках разных пулов, YDB выбирает тот из них, ранг которого минимален (чем меньше числовое значение ранга — тем выше его приоритет).

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

Другие настройки влияют на каждый узел по отдельности. Это максимальная используемая пулом ресурсов память, процессор и веса распределения ресурсов между пулами на одном узле (документация).

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

Всю коммуникацию с менеджером смешанной нагрузки берут на себя сами акторы. Первое, что делает код актора-таски, — запрашивает у менеджера смешанной нагрузки разрешение на выполнение части запроса. Менеджер смешанной нагрузки по алгоритму Max-min Fairness определяет, может ли актор выполняться. И если не может — то актор отправляет сам себе сообщение, чтобы движок через нужное время снова вызвал обработчик и актор мог повторить попытку.

Так, синхронизируя ключевую информацию между узлами слоя вычисления и ставя на паузу акторы-таски, YDB управляет OLAP и OLTP-нагрузкой кластера.

Сейчас в разработке новая версия YDB, где в менеджере смешанной нагрузки сделано два улучшения. Во-первых, алгоритм Max-min Fairness изменён на Hierarchical Dominant Resource Fairness, который обеспечивает лучшие результаты. А во-вторых, пулы ресурсов могут образовывать иерархии — это позволяет алгоритму гибче распределять нагрузку между пулами.

Дерево балансировки
Дерево балансировки

Будущее менеджера смешанной нагрузки

Мы постоянно получаем обратную связь от разных пользователей YDB. Кто-то использует базу данных в Yandex Cloud, кто-то — коммерческие сборки. Некоторые читатели Хабра используют опенсорс-версию на своих серверах. Общаясь со всеми ними, мы формируем список фич, которые стараемся реализовать в первую очередь:

  • Распределённое квотирование ресурсов, чтобы ограничение можно было поставить на весь кластер в целом, а не на ресурсы каждого сервера по отдельности.

  • Умные классификаторы, которые позволят задавать более сложные правила, нежели «все запросы от такого-то пользователя относятся к такому-то ресурс-пулу».

  • Тесная интеграция планировщика и пулов ресурсов, чтобы акторы-таски распределялись по серверам не только равномерно, но и с учётом их ограничений по нагрузке.

  • Иерархия пулов ресурсов и приоритеты выполнения запросов внутри пулов.

Если вы пользуетесь YDB или другой базой данных, которая умеет управлять смешанной нагрузкой, — поделитесь вашими впечатлениями в комментариях!

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


  1. Tenphi
    18.08.2025 10:55

    Спасибо за статью! В чём именно вы видите основную пользу от внедрения HDRF в контексте YDB и насколько ощутимый буст это может дать в общем?


    1. dorooleg Автор
      18.08.2025 10:55

      Спасибо за вопрос. Сейчас используется алгоритм который пересчитывает лимиты в зависимости от того используют конкретный Resource Pool или нет. Соответственно он выступает как некоторый лимитер из-за чего может не достигаться 100% утилизация cpu, если один пул перегружен, а второй недогружен, то перераспределения ресурсов между Resource Pool'ами не произойдет, при этом такой подход обладает минимальными накладными расходами. После перехода на HDRF будет честное распределение ресурсов между пулами в соответствии с весами, что позволит достигать 100% утилизации cpu, также это позволит строить иерархически пулы ресурсов и по предварительным результатам накладные расходы незначительно больше чем у подхода через лимитер.