Метаданные S3 В Postgresql. Лекция Яндекса

Это вторая лекция Я.

Субботник по базам данных - первый мы опубликовали пару недель назад. Руководитель группы СУБД общего назначения Дмитрий Сарафанников рассказал об эволюции хранилища данных в Яндексе: как мы решили сделать S3-совместимый интерфейс, почему выбрали PostgreSQL, на какие ошибки наступили и как с ними справились .

- Всем привет! Меня зовут Дима, я работаю с базами данных в Яндексе.

Я расскажу вам, как мы делали S3, как мы пришли к созданию S3 и какие хранилища у нас были раньше.

Первый из них — Elliptics, он имеет открытый исходный код и доступен на GitHub. Многие, возможно, сталкивались с этим.



Метаданные S3 в PostgreSQL. лекция яндекса

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

Если вы хотите добавить туда машины, происходит перераспределение ключей и происходит ребалансировка.

У этого хранилища есть свои проблемы, связанные, в частности, с ребалансировкой.

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

Это была довольно большая проблема.

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

Это идеальный вариант для таких решений.

Давайте двигаться дальше.

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



Метаданные S3 в PostgreSQL. лекция яндекса

В чем его суть? Это не хранилище значений ключей, это хранилище значений.

Когда вы загружаете туда какой-то объект или файл, он отвечает вам ключом, с помощью которого вы потом можете забрать этот файл.

Что это дает? Теоретически 100% доступность записи при наличии свободного места в хранилище.

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

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

Это очень просто и надежно.

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

Это неудобно для всех.

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

Но в то же время мне не хочется терять надежность и простоту этого хранилища; на самом деле он работает на скорости сети.

Потом мы начали смотреть на S3. Это хранилище «ключ-значение», клиент управляет ключом, все хранилище разделено на так называемые сегменты.

В каждом сегменте ключевое пространство находится в диапазоне от минус бесконечности до плюс бесконечности.

Ключ — это какая-то текстовая строка.

И мы остановились на этом варианте.

Почему S3? Все довольно просто.

На данный момент уже написано много готовых клиентов для различных языков программирования, уже написано много готовых инструментов для хранения чего-либо в S3, скажем, резервных копий баз данных.

Андрей сказал об одном из примеров.

Уже есть достаточно продуманное API, проверенное годами на клиентах, и не нужно там ничего придумывать.

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

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

При чтении мы просто найдём ключи и хранилище в нашей базе, и дадим клиенту то, что он хочет. Если изобразить это схематично, как происходит наполнение?

Метаданные S3 в PostgreSQL. лекция яндекса

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

Все довольно просто.



Метаданные S3 в PostgreSQL. лекция яндекса

Как вы это получите? Прокси находит нужный ключ в базе данных, идет с ключом в хранилище, скачивает оттуда объект и отдает его клиенту.

Все также просто.



Метаданные S3 в PostgreSQL. лекция яндекса

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

Здесь тоже все довольно просто.

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

С переездом Яндекс.

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

S3 хорошо вписался в один из них с небольшими доработками, но и туда вписался хорошо.

Какие есть варианты шардинга? Это большое хранилище, в масштабах Яндекса сразу надо думать, что объектов будет очень много, нужно сразу думать, как это все шардировать.

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

Одно ведро может находиться внутри одного сегмента базы данных.

Другой вариант — сегментировать по диапазонам ключей.

Внутри ведра пространство от минус бесконечности до плюс бесконечности, мы можем разделить его на сколько угодно диапазонов, мы называем этот диапазон чанком, он может жить только в одном шарде.



Метаданные S3 в PostgreSQL. лекция яндекса

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

Тут будут большие проблемы, поэтому разрежем и разложим по осколкам как захотим.

Все здесь.



Метаданные S3 в PostgreSQL. лекция яндекса

Что случилось? Вся база данных состоит из трех компонентов.

S3 Proxy — это группа хостов, которая также содержит базу данных.

PL/Proxy находится под балансировщиком, туда отправляются запросы от того бэкенда.

Далее идет S3Meta, басовая группа, хранящая информацию о сегментах и чанках.

И S3DB, шарды, где хранятся объекты, очередь на удаление.

Если изобразить это схематически, то это выглядит так.



Метаданные S3 в PostgreSQL. лекция яндекса

Приходит запрос на S3Proxy, он идёт на S3Meta и S3DB и отправляет информацию вверх.



Метаданные S3 в PostgreSQL. лекция яндекса

Давайте посмотрим поближе.

S3Proxy, внутри него функции созданы на процедурном языке PLProxy, это язык, позволяющий удаленно выполнять хранимые процедуры или запросы.

Вот как выглядит код функции ObjectInfo, по сути, это запрос Get. В кластере LProxy есть оператор Cluster, в данном случае это db_ro. Что это значит?

Метаданные S3 в PostgreSQL. лекция яндекса

В типичной конфигурации сегмента базы данных есть мастер и две реплики.

Мастер является частью кластера db_rw, все три хоста входят в состав db-ro, именно сюда можно отправить запрос только на чтение, а на db_rw отправляется запрос на запись.

Кластер db_rw включает в себя все мастера всех шардов.

Следующий оператор — RUN ON, он принимает либо значение all, что означает выполнение на всех шардах, либо на массиве, либо на каком-то шарде.

В данном случае на вход он принимает результат функции get_object_shard — это номер шарда, на котором лежит этот объект. И target — какую функцию вызывать на удаленном шарде.

Он вызовет эту функцию и подставит туда аргументы, поступившие в эту функцию.



Метаданные S3 в PostgreSQL. лекция яндекса

Функция get_object_shard также написана на PLProxy, уже кластере мета_ро, запрос полетит к шарду S3Meta, который вернет эту функцию get_bucket_meta_shard. S3Meta тоже можно шардировать, мы это тоже планировали, пока не актуально, но возможность есть.

И вызовет функцию get_object_shard в S3Meta.

Метаданные S3 в PostgreSQL. лекция яндекса

get_bucket_meta_shard — это просто хэш текста от имени бакета; мы сегментировали S3Meta просто по хешу имени корзины.



Метаданные S3 в PostgreSQL. лекция яндекса

Давайте посмотрим на S3Meta и что в ней происходит. Самая важная информация, которая есть, — это таблица с кусками.

Вырезал немного ненужной информации, осталось самое главное — это Bucket_id, начальный ключ, конечный ключ и шард, в котором лежит этот чанк.



Метаданные S3 в PostgreSQL. лекция яндекса

Как будет выглядеть запрос на основе такой таблицы, который вернет нам фрагмент, содержащий, например, тестовый объект? Что-то вроде этого.

Минус бесконечность в текстовом виде мы представили как нулевое значение, есть такие тонкие моменты, что нужно проверять start_key и end_key на предмет наличия Null.

Метаданные S3 в PostgreSQL. лекция яндекса

Запрос выглядит не очень хорошо, а план выглядит еще хуже.

В качестве одного из вариантов такого плана запроса BitmapOr. И стоит такой план 6000 затрат.

Метаданные S3 в PostgreSQL. лекция яндекса

Как может быть иначе? В PostgreSQL есть такая замечательная вещь, как gist index, которая может индексировать тип диапазона, диапазон — это, по сути, то, что нам нужно.

Мы создали этот тип, функция s3.to_keyrange по сути возвращает нам диапазон.

Мы можем использовать оператор contains, чтобы проверить и найти фрагмент, содержащий наш ключ.

Именно так здесь построено ограничение исключения, которое гарантирует, что эти куски не пересекаются.

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

Иначе будет не то, что мы хотели.

Вот как выглядит план такого запроса, обычный index_scan. Это условие полностью укладывается в индексное условие, а затрат у такого плана всего 700, в 10 раз меньше.



Метаданные S3 в PostgreSQL. лекция яндекса

Что такое ограничение исключения?

Метаданные S3 в PostgreSQL. лекция яндекса

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

Приравняв два оператора, была построена следующая таблица.



Метаданные S3 в PostgreSQL. лекция яндекса

Далее пытаемся вставить две одинаковые строки, получаем ошибку о нарушении уникальности ключа по первому ограничению.

Если мы отбросим его, то мы уже нарушили ограничение исключения.

Это общий случай уникального ограничения.



Метаданные S3 в PostgreSQL. лекция яндекса

По сути, ограничение уникальности — это то же самое ограничение исключения с операторами равенства, но в случае ограничения исключения можно сконструировать несколько более общих случаев.



Метаданные S3 в PostgreSQL. лекция яндекса

У нас есть такие индексы.

Если вы присмотритесь, то увидите, что это оба индекса по сути, и в целом они одинаковы.

Вы, вероятно, спросите, зачем вообще дублировать этот вопрос.

Я вам скажу.



Метаданные S3 в PostgreSQL. лекция яндекса

Индексы — это такая штука, особенно gist index, что таблица живет своей жизнью, происходят обновления, происходят обновления и так далее, индекс там ухудшается и перестает быть оптимальным.

И есть такая практика, в частности расширение pg repack, индексы перестраиваются периодически, через раз перестраиваются.

Как перестроить индекс под уникальным ограничением? Создайте индекс создания в данный момент, создайте один и тот же индекс незаметно рядом, без блокировки, а затем используйте выражение таблицы изменения из ограничения user_index такого-то и такого-то.

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

Это неприемлемо; Построение индекса сущности может занять довольно много времени.

Поэтому мы держим рядом второй индекс, который меньше по объему, занимает меньше места, планер его использует, и мы можем конкурентно перестроить этот индекс, не блокируясь.



Метаданные S3 в PostgreSQL. лекция яндекса

Вот график потребления процессора.

Зеленая линия — потребление процессора в user_space, оно скачет от 50% до 60%.

В этот момент потребление резко падает, это момент перестройки индекса.

Мы перестроили индекс, удалили старый, и потребление процессора у нас резко снизилось.

Это основная проблема индекса, она существует, и это наглядный пример того, как это может произойти.

Когда мы все это сделали, мы начали с версии 9.5 S3DB, по плану мы планировали укладывать по 10 миллиардов объектов в каждый шард. Как известно, больше 1 миллиарда и даже раньше начинаются проблемы, когда в таблице много строк, все становится намного хуже.

Существует практика разделения.

На тот момент было два варианта, либо стандартный через наследование, но это работает не очень хорошо, так как есть линейная скорость выбора раздела.

И судя по количеству объектов, партиций нам понадобится очень много.

Ребята из PostgreSQL тогда активно работали над расширением pg_pathman.

Метаданные S3 в PostgreSQL. лекция яндекса

Мы выбрали pg_pathman, другого выбора у нас не было.

Даже версия 1.4. И как видите, мы используем 256 разделов.

Всю таблицу объектов мы разделили на 256 разделов.

Что делает pg_pathman? С помощью этого выражения вы можете создать 256 разделов, которые будут разделены по хешу из столбца ставки.



Метаданные S3 в PostgreSQL. лекция яндекса

Как работает pg_pathman?

Метаданные S3 в PostgreSQL. лекция яндекса

Он регистрирует свои хуки в планировщике, а затем по сути подменяет план на запросах.

Видим, что при обычном поисковом запросе объекта с именем test он не искал в 256 партициях, а сразу определил, что нужно зайти в таблицу Objects_54, но здесь не все гладко, у pg_pathman есть свои проблемы.

Во-первых, поначалу, пока его вырезали, было довольно много ошибок, но спасибо ребятам из PostgreSQL, они их оперативно починили и исправили.

Первая проблема — сложность его обновления.

Вторая проблема — подготовленные заявления.

Давайте посмотрим поближе.

В частности обновление.

Из чего состоит pg_pathman?

Метаданные S3 в PostgreSQL. лекция яндекса

По сути, он состоит из кода C, который упакован в библиотеку.

И состоит он из SQL-части, всяких функций по созданию разделов и так далее.

Кроме того, в библиотеке есть интерфейсы для функций C. Эти две части не могут обновляться одновременно.

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

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

Потом отключаем pathman, перезапускаем базу, говорим ALTER EXTENSION UPDATE, в это время все попадает в родительскую таблицу.

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

А затем отключаем использование родительской таблицы и ищем в ней.



Метаданные S3 в PostgreSQL. лекция яндекса

Следующая проблема — подготовленные заявления.



Метаданные S3 в PostgreSQL. лекция яндекса

Если мы настроим тот же обычный запрос, поиск по ставке и ключу, мы попробуем его выполнить.

Делаем пять раз — все нормально.

Выполняем шестое – видим этот план.

И в этом плане мы видим всего 256 разделов.

Если внимательно посмотреть на эти условия, то мы видим здесь доллар 1, доллар 2, это так называемый родовой план, генеральный план.

Первые пять планов запросов строились индивидуально, для этих параметров использовались индивидуальные планы, pg_pathman мог сразу определить, поскольку параметр был известен заранее, он мог сразу определить таблицу, куда идти.

В данном случае он не может этого сделать.

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

Он просто теряет все свои преимущества, и любой запрос выполняется безумно долго.



Метаданные S3 в PostgreSQL. лекция яндекса

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

Вот как все это работает. Минус в том, что вам придется запихивать весь код в конструкции, влияющие на эти таблицы.

Здесь сложнее читать.



Метаданные S3 в PostgreSQL. лекция яндекса

Как распределяются объекты? В каждом шарде S3DB хранятся счетчики чанков, также есть информация о том, какие чанки находятся в этом шарде, и для них хранятся счетчики.

Для каждой операции изменения объекта — добавления, удаления, изменения, перезаписи — эти счетчики чанка изменяются.

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



Метаданные S3 в PostgreSQL. лекция яндекса

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

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



Метаданные S3 в PostgreSQL. лекция яндекса

Следим, чтобы эти куски были небольшими.

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

Метаданные S3 в PostgreSQL. лекция яндекса

Передача фрагмента — немного более сложная операция; это двухфазная фиксация трех баз данных: S3Meta и двух шардов S3DB, перенесенных из одной и добавленной в другую.



Метаданные S3 в PostgreSQL. лекция яндекса

В S3 есть такая функция как листинги, это самая сложная вещь, и с ней тоже были проблемы.

По сути, в списках вы говорите S3 — покажите мне предметы, которые у меня лежат. Параметр, который в настоящее время имеет значение NULL, выделен красным.

Этот параметр, разделитель, разделитель, вы можете указать листинги с каким разделителем вы хотите.



Метаданные S3 в PostgreSQL. лекция яндекса

Что это значит? Если разделитель не указан, мы видим, что нам просто дан список файлов.

Если мы установим разделитель, S3, по сути, должен показать нам папки.

Он должен понимать, что здесь есть такие папки, и фактически показывает все папки и файлы в текущей папке.

Текущая папка указывается префиксом, этот параметр имеет значение Null. Видим, что там 10 папок.

Все ключи не хранятся в какой-то иерархической древовидной структуре, как в файловой системе.

Каждый объект хранится в виде строки и имеет простой общий префикс.

S3 должен сам понять, что это приклад.

Метаданные S3 в PostgreSQL. лекция яндекса

Эта логика довольно плохо сочетается с декларативным SQL; это довольно легко описать с помощью императивного кода.

Первый вариант был сделан именно так, просто хранимые процедуры в PL/pgSQL. Он императивно обрабатывал эту логику в цикле и требовал повторяемого уровня чтения.

Мы должны видеть только один снимок, все запросы должны выполняться с одним снимком.

В противном случае, если кто-то загрузит туда что-то после первого запроса, мы получим противоречивые листинги.

Потом удалось все это переписать в Recursive CTE, получилось очень громоздко со сложной логикой, без пол-литра не разберешься, и все это завернуто в выполнение внутри PL/pgSQL. Но мы получили ускорение, в некоторых случаях до ста раз.

Вот, например, графики процентилей и времени отклика функции списка объектов.

Что было до и после.



Метаданные S3 в PostgreSQL. лекция яндекса

Эффект заметен визуально, в том числе и по нагрузке.

Оптимизацию мы проводили в несколько этапов.

Вот еще один график другой оптимизации, где наши высокие квантили просто упали до низких.



Метаданные S3 в PostgreSQL. лекция яндекса

Для тестирования мы используем Docker, pro. Вести себя и тестирование поведения — это замечательно отчет Александра Клюева.

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

Нам еще есть что оптимизировать.

Самая острая проблема, как я вам показал, — это загрузка ЦП на S3Meta. Индекс Gist потребляет много ресурсов ЦП, особенно когда он становится неоптимальным после многочисленных обновлений и делитов.

Процессора на S3Meta явно не хватает. Можно штамповать реплики, но это будет неэффективное использование железа.

У нас есть группа хостов с PLProxy под балансировщиком, которые стоят и удаленно вызывают функции на S3Meta и S3DB. Фактически там процессор можно заставить сжечь прокси.

Для этого вам необходимо организовать логическую репликацию этих чанков из S3Meta на все прокси.

В принципе, мы планируем это сделать.

В логической репликации существует ряд проблем, которые мы решим и попытаемся перенести в восходящий поток.

Второй вариант — отказаться от сути и попробовать поместить этот текстовый диапазон в btree. Это не одномерный тип, а btree работает только с одномерными типами.

Но условие, что наши чанки не должны пересекаться, позволит разместить наш кейс в btree. Буквально вчера мы сделали прототип, который работает. Это реализовано с использованием функций PL/pgSQL. Мы получили заметное ускорение, будем оптимизировать в этом направлении.

Теги: #Администрирование баз данных #postgresql #s3 #хранилище объектов

Вместе с данным постом часто просматривают: