Привет, Хабр! Меня зовут Игорь Березняк, и мы с командой делаем Техплатформу Городских сервисов Яндекса. Я уже писал на Хабре про архитектуру платформы, рассказывал на «Хайлоаде» (и на Хабре) про шардирование и миграцию на YDB.

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

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

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

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

О Техплатформе Городских сервисов за одну минуту

Техплатформа — базовая инфраструктура, на основе которой продуктовые разработчики Такси, Доставки, Лавки и других сервисов строят свою логику. Они описывают в YAML-файлах правила продвижения заказов по бизнес-логике: как обновлять данные, искать водителей, когда отправлять оповещения, выполнять платежи и так далее.

С помощью декларативных описаний отдельный сервис Техплатформы, который мы называем сервисом процессинга, оркестрирует продуктовые системы. Это mission-critical-сервис с нагрузкой около 10K RPS, высокой доступностью и низкой задержкой. Если он прекратит работать — заказы в Такси или Лавке перестанут обрабатываться. А если будет долго отвечать, то это скажется на времени заказа такси.

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

Изначально сервис использовал PostgreSQL, но со временем мы столкнулись с тремя проблемами, для решения которых пришлось выбрать другую СУБД и переписать часть кода. Что это за проблемы и как YDB позволила их решить?

Проблема № 1: смена лидера при репликации

Чтобы обеспечить надёжность 99,99%, сервис процессинга развёрнут в трёх дата-центрах и должен переживать отказ любого из них. Мы регулярно проводим учения, во время которых один из дата-центров отключается. Так что такое отключение — это не что-то редкое, что случается раз в несколько лет, а обычная работа наших сервисов.

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

Проблема в том, что если не резервировать избыточные серверные мощности, то смена лидера занимает несколько десятков секунд, в течение которых сервис не может записывать данные в базу и не может оркестрировать продуктовые системы, такие как Яндекс Такси. Нам не удалось решить проблему даже с помощью мощных серверов, на которых утилизация CPU лидерами при нормальной нагрузке составляла всего 15%.

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

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

Решает ли YDB проблему долгой смены лидера? Безусловно. Бесплатно ли такое решение? Не совсем. Сетевая топология распределённой СУБД Яндекса сложнее, чем репликация лидера в вертикально масштабируемых системах.

Для выполнения распределённой транзакции YDB требуется в 4,5 раза больше времени, чем для отправки запроса и получения ответа между двумя узлами по сети. Большая нагрузка на сеть и более сложная сетевая топология приводит к нестабильному времени отклика в высоких перцентилях и увеличивает среднее время выполнения запросов к СУБД с ~30 до ~70 миллисекунд.

Время выполнения запросов можно уменьшить, адаптировав код к распределённой СУБД. Если для выполнения запроса не требуется чтение, то такой запрос выполняется быстрее. Мы переписали часть запросов на YQL таким образом, чтобы они не подразумевали чтение (и не использовали распределённые транзакции), и в результате время выполнения большинства за��росов снова вернулось к привычным нам 30 миллисекундам. Одна проблема была решена, осталось две.

Проблема № 2: количество подключений к шардам

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

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

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

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

На каждом узле YDB выполняются планировщики запросов, которые распределяют запросы на нужные шарды. В зависимости от нагрузки и объёма данных YDB автоматически решардирует каждую таблицу на части, которые называются таблетками (от tablet — «часть table»).

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

В нашем случае заказов в Такси сильно больше, чем в Лавке. Эти заказы забирают себе большую часть префикса первичного ключа и равномерно распределяются по шардам. В результате много заказов Лавки могут попасть на один шард, создав «горячую партицию» с высокой нагрузкой.

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

Чтобы распределение по шардам было равномерным, мы добавили в первичный ключ хеш-колонку и настроили параметры шардирования: AUTO_PARTITIONING_PARTITION_SIZE_MB и AUTO_PARTITIONING_MAX_PARTITIONS_COUNT. Это решило вторую проблему из трёх, осталась последняя!

Параметр AUTO_PARTITIONING_PARTITION_SIZE_MB устанавливает рекомендуемый порог (в мегабайтах), по превышении которого партиция может быть разделена. А параметр AUTO_PARTITIONING_MAX_PARTITIONS_COUNT задаёт максимальное количество партиций для одной таблицы. По умолчанию партиции начинают делиться, если их размер превышает 2 гигабайта, и одна таблица может быть разделена не более чем на 50 партиций.

Проблема № 3: холодное хранилище

Хранить бесконечную ленту событий в транзакционной СУБД — дорого. В реализации на PostgreSQL мы использовали отдельное холодное (для хранения редко используемых исторических данных) хранилище и отдельный сервис-репликатор для перекладывания данных. Такое хранилище решает проблему хранения сотен терабайт данных, но усложняет архитектуру приложения.

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

Проблему решили, воспользовавшись функцией YDB по разделению хранения данных на SSD и HDD. Всей таблице или даже отдельным колонкам можно назначить «класс хранения». Для исторических данных мы сделали отдельную таблицу холодного хранилища с классом хранения на HDD.

Использование одной СУБД для обоих хранилищ существенно упростило архитектуру, так как запросы можно делать сразу и в горячее, и в холодное хранилище, а затем просто склеивать данные. А запись и удаление при такой архитектуре тривиальны. Да, запросы к HDD медленнее, чем к SDD, — ~400 против ~70 миллисекунд. Но если код сервиса точно знает, что данных в холодном хранилище нет, то таких запросов можно избежать. Для этого мы используем отдельную таблицу tombstone с информацией о том, какие данные переехали в холодное хранилище.

Сервис-репликатор поменяли на сервис-охладитель, который периодически выполняет в фоне полное сканирование таблицы, для этого в YDB есть интерфейс ReadTable. Такое фоновое сканирование занимает один-два дня для нескольких десятков терабайт данных, а сам сервис — это около тысячи строк Python-кода.

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

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

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

Итоги миграции: YDB — это не PostgreSQL

Для сервисов с высокими требованиями к надёжности использование YDB позволяет решить все вопросы масштабирования за счёт распределённой архитектуры этой СУБД. Но важно понимать, что YDB не является drop-in-заменой PostgreSQL или Oracle.

При больших нагрузках и объёмах данных YDB нужно правильно использовать — как со стороны кода приложения, так и со стороны настройки серверов. Распределённая СУБД предлагает больше тонких настроек, которые отсутствуют в вертикально масштабируемых системах. Разные варианты запросов, синхронные и асинхронные вторичные индексы, классы хранения данных, стриминг результатов запроса с помощью gRPC, фоновый compaction для удаления данных, параметры автошардирования — всё это нужно изучить и правильно использовать. А в случае миграции с другой СУБД — продумать заранее, чтобы посреди миграции не оказалось, что что-то работает не так, как вы ожидали.

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

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