В истории Tesla и YDB есть одно интересное сходство. В Tesla первой машиной стал Roadster, главной чертой которого были разгон и скорость, то есть performance. Аналогично развивалась и YDB: мы начали с масштабирования до бесконечности и отказоустойчивости, и только потом, решив эти задачи внутри Яндекса, пришли на более «консьюмерский» рынок.

Чтобы сделать YDB более удобной для обычных пользователей с небольшими данными (или вообще без них), мы сейчас работаем над двумя проектами:

  • Tiny YDB: конечная цель — запускаться на половине ядра CPU. Сейчас в Yandex Cloud доступен Managed YDB с 4 ядрами, а внутри мы уже активно тестируем YDB на 2 ядрах;

  • Zero CPU YDB: уменьшаем потребление CPU, когда пользователи ничего не делают с YDB. Стремимся к нулю, а если уйдём в минус, то будем считать, что просто перевыполнили план.

В этом посте мы подробно расскажем о текущем статусе Zero CPU и о первых, но значительных улучшениях. Сразу вынуждены признаться, что обычно запускаем YDB где угодно, кроме наших рабочих ноутбуков: на разработческих железных серверах и в облаке. О том, что YDB, запущенный на ноутбуке и не выполняющий никаких запросов, ощутимо сажает батарейку, мы узнали от наших пользователей. Мы проверили и обнаружили, что YDB действительно в фоне потребляет 6.5% ядра CPU. Поэтому сразу занялись этой проблемой. Забегая вперёд: мы уже смогли уменьшить это значение в 3.5 раза до 1.8%, работа продолжается.

Чем занята YDB, когда с ней ничего не делают

На первый взгляд может показаться, что когда нет запросов, то базе нечего делать. Но это совсем не так. Для хранения данных мы используем LSM-деревья, причём на пути к диску деревья встречаются два раза: одно находится в DataShard, второе в VDisk. Кстати, если вы используете SSD/NVMe, то и там почти наверняка будет ещё одно LSM-дерево. Компакшен LSM-дерева улучшает его структуру и удаляет мусор. В частности, уменьшается число блоков с данными, которые необходимо хранить в кэше или читать с диска, что позволяет ускорить чтение.

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

Мы убедились, что дело не в компакшене, и продолжили поиски. Важно понимать, что в ДНК YDB встроен принцип observability. Если говорить простым языком, то у нас полным-полно метрик, а агрегация метрик внутри процессов YDB является активным процессом, который никогда не прекращается. Ведь даже когда база ничем не занята, то у неё 0 запросов, выполняемых за 0 мс — вполне себе валидные значения метрик. Ещё и у наших пулов потоков, например, есть метрика Parked, чтобы понимать, сколько времени поток ничего не делал. И так почти со всеми метриками.

С помощью наших вн��тренних метрик мы в первую очередь установили, что есть компоненты, которые by design должны периодически сохранять своё состояние на диск, даже если ничего не происходит. Например, координатор распределённых транзакций должен продвигать время и записывать его, чтобы после рестарта не выдать более старое. Кроме того, компоненты Hive, Scheme shard и Stats aggregator сохраняют статистику, полученную от таблеток, чтобы на старте сразу иметь всё под рукой.

Оказалось, что в бездействующей системе эти компоненты пишут 16 KiB/s (4 IOPS). Но это не оказывает значительного влияния на батарейку ноута. Тем не менее мы, конечно, планируем улучшить это место. А вот метрики, когда они собираются из YDB внешней системой, очень даже потребляют CPU и энергию. Поэтому в дальнейшем мы отказались от сбора метрик в рамках данной задачи. Тем более что чаще всего, когда YDB запускают на ноутбуке, то не собирают метрики с него.

Следующим этапом мы проверили внутренние сервисы, особенно связанные с метриками. У нас есть специальный режим запуска YDB, который называется --tiny-mode. Он отключает некоторые сервисы, которые крайне важны для продакшена, но бесполезны на ноутбуке. Например, у нас есть сервис SelfPing: раз в 10 мс мы отправляем задачу перемножения матриц 11x11 (uint64) в каждый из пулов. И получаем две метрики: время доставки сообщения (~5–10 мкс в незагруженной системе) и затраченный CPU (~15 мкс в зависимости от железа). Это крайне важные в продакшене метрики и абсолютно бесполезные на ноутбуке. Например, если увеличивается время перемножения матриц, то это свидетельствует либо о переподписке на CPU (когда на одном сервере запускают несколько YDB), либо о том, что появился шумный сосед (в случае облачных инстансов). Как выяснилось, с точки зрения CPU/батарейки эти сервисы почти незаметны.

Дальше на помощь пришли FlameGraph и strace. Мы увидели, что наш внутренний планировщик таймеров/событий просыпается 15 000 раз в секунду вместо ожидаемых нами 1000. В основе нашей реализации планировщика лежат следующие принципы:

  • Запланированны�� события хранятся аналогично описанному в статье «Hashed and hierarchical timing wheels: data structures for the efficient implementation of a timer facility»: «грубое» хранение далёких событий (intrasecond) и «точное» — событий, которые вот-вот (с настраиваемым разрешением) выполнятся (instant).

  • Не используем локи, поэтому с планировщиком связаны N очередей типа SPSC (single producer single consumer).

  • Быстрый и точный event loop на основе комбинации spin & sleep. Здесь опять возникает настраиваемое разрешение.

Мы ожидали, что разрешение нашего таймера 1 мс (отсюда упомянутые выше 1000 просыпаний в секунду). И это значение используется по умолчанию. В какой-то момент даже заподозрили баг в реализации, и пришлось внимательно проверить код и тесты. Но оказалось, что глубоко в автоконфигурации системы выставляется значение 64 мкс — значение, которое используется у нас в продакшене. 64 мкс является адекватной настройкой, когда у вас крутые NVMe-диски, а не игрушечная tiny/zero-инсталляция поверх файловой системы и простого SSD. Для сравнения: время доступа у NVMe составляет десятки мкс, а у SSD — сотни мкс. В обычных инсталляциях такое высокое разрешение не имеет смысла. Мы сравнили 64 мкс и 1000 мкс на ноутбуке с SSD, используя наш любимый бенчмарк TPC-C, и не обнаружили никакой разницы.

Основные результаты

Мы перешли на разрешение планировщика 1000 мкс в случае, если включён --tiny-mode, или когда включена автоконфигурация и для YDB выделено 4 ядра и меньше. Это дало значительный выигрыш в экономии батарейки. Как мы уже отметили в начале поста, мы снизили потребление CPU, когда YDB бездействует, с 6.5% до 1.8%. Ради любопытства посмотрели, сколько у PostgreSQL 17: оказалось, что 0% — то, к чему мы стремимся.

Для наглядности мы решили провести эксперимент и посмотреть, сколько процентов аккумулятора ноутбука останется после одного и двух часов работы без YDB, со старым YDB1 и новым YDB2 (--tiny-mode и новое разрешение планировщика). Ramfs — это сетап, в котором диск находится в RAM, что позволяет оценить влияние дисковых операций на батарейку. Достаточно шумная метрика с высокой погрешностью, но это именно то, что мы оптимизируем. Ниже приведены результаты, полученные на ноутбуке Dell Latitude 7420 с процессором Intel i5-1145G7 @ 2.60GHz.

Для наглядности на основе этих измерений посчитали среднее время разрядки аккумулятора в час и перевели его в расчётное время работы ноутбука в часах:

Хорошо видно, что старая версия YDB сокращала среднее время работы аккумулятора почти в два раза. Мы смогли ощутимо увеличить время автономной работы ноутбука с YDB. Нам ещё, конечно, есть над чем работать, но даже сейчас разница уже не такая драматичная, как раньше. И чем новее ноутбук и его аккумулятор, тем меньше будет эта разница.

Docker, Apple Silicon, x86-64 YDB

Пока мы занимались YDB Zero на x86-64, к нам за помощью обратились другие пользователи, которые запускают на макбуках с Apple Silicon наш Docker-образ с YDB. Когда вы запускаете x86-64 на ARM, требуется эмулировать x86-64. Благодаря чему 6.5% CPU увеличиваются до 30–50% в зависимости от процессора и типа эмуляции. Конкретно у нас на M1 потребление CPU составило 45–50% со старым Docker-образом YDB и Colima с Rosetta 2.

Мы обнаружили, что с Docker, кроме описанных выше проблем, есть ещё одна. В healthcheck контейнера мы задали интервал в 1 секунду, при этом проверка достаточно тяжеловесная. Мы увеличили интервал до одной минуты и взяли новый YDB — потребление CPU снизилось до 12–18%, если использовать Colima и Rosetta 2 (рекомендуем вместо QEMU), что в 3 раза лучше, чем было изначально. Для сравнения мы запустили контейнер с x86-64 PostgreSQL 17: получилось всего 1.5%.

Отдельно отметим, что мы рекомендуем избегать QEMU, когда это возможно: его полная эмуляция x86-64 на ARM даёт наиболее высокие накладные расходы. Даже после наших улучшений мы видели очень высокое потребление CPU, если использовать QEMU. Вместо этого лучше использовать Colima с Rosetta 2, которая обеспечивает более лёгкую динамическую трансляцию и потребление CPU заметно ниже. Отличный вариант — OrbStack. Вероятно, Tidy тоже будет работать очень эффективно, хотя мы его пока не тестировали.

Заключение

Теперь вы смело можете носить с собой ноутбук с YDB и не бояться, что он мгновенно сядет. Новая версия YDB (> 25.2.1.10) работает ощутимо дольше и продолжит работать, когда ноутбук со старым YDB уже на исходе. Даже если у вас новый макбук, а YDB скомпилирован под x86-64.

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

Мы общаемся с нашими пользователями в Telegram и на Хабре. Если вы отлаживаете приложения с СУБД на ноутбуке, то в комментариях к этой статье мы будем рады обсудить возникающие проблемы с энергопотреблением.