Как стать автором
Обновить
162.54

Шардированный не значит распределённый: что важно знать, когда PostgreSQL становится мало

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров5.2K

Год назад мы опубликовали пост «Когда одного Postgres'a мало: сравнение производительности PostgreSQL и распределённых СУБД». PostgreSQL показал исключительную производительность в случае, когда нет высоких требований к доступности сервиса и сохранности данных. Мы назвали такую конфигурацию «отказонеустойчивый и фантастически быстрый PostgreSQL». Но при включении write ahead log'a (WAL) и репликации производительность ощутимо снижается. Как оказалось, репликация может быть узким местом и ограничивать возможности вертикального масштабирования. По результатам бенчмарка TPC-C «отказоустойчивый PostgreSQL» обошёл распределённую СУБД YDB всего на 5% по throughput (tpmC), имея при этом ощутимые проблемы с latency.

Мы ожидали активного обсуждения конфигурации Postgres'a, бенчмарков и нашего подхода к сравнению производительности. Но самая большая дискуссия развернулась вокруг нашего утверждения, что PostgreSQL масштабируется только вертикально и Citus-подобные решения не дают гарантий ACID в случае многошардовых (широких/распределённых) транзакций.

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

Шардирование монолита

В основе большинства шардированных решений для PostgreSQL лежит очень простая идея: вместо одного PostgreSQL берут N, где каждый из Postgres'ов отвечает за определённый диапазон ключей таблицы. Знанием об этих диапазонах обладает специальный слой маршрутизации (координатор), который теперь для пользователя становится точкой входа. Слой маршрутизации может как находиться на стороне сервера (Citus-подобные решения), так и быть частью клиентского приложения. Важно понимать, что эти N инстансов PostgreSQL ничего не знают друг о друге и никак не взаимодействуют между собой.

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

Свойства ACID и двухфазный коммит

Когда речь идёт о транзакциях в базах данных, то обычно мы предполагаем, что транзакции всегда имеют свойства ACID:

  • Atomicity (Атомарность): все части транзакции либо применяются (commit) целиком, либо отменяются (abort).

  • Consistency (Согласованность): исторически добавлено для создания более благозвучной аббревиатуры и скорее специфично для приложений, чем для СУБД. Означает, что транзакция переводит систему из одного корректного состояния в другое в соответствии с заданными правилами (ограничениями целостности, триггерами и т. п.). Однако на практике это свойство часто интерпретируется на уровне приложения — СУБД лишь обеспечивает инструменты для его соблюдения, но не всегда гарантирует его автоматически.

  • Isolation (Изоляция): параллельно выполняемые транзакции не влияют друг на друга. Изменения, сделанные одной транзакцией, становятся видимыми другим только после её успешного завершения (в момент коммита). И результаты выполнения каждой транзакции должны выглядеть так, как если бы они выполнялись сериализованно — последовательно одна за другой. Однако многие СУБД поддерживают более слабые уровни изоляции, оставляя за разработчиком выбор подходящего.

  • Durability (Устойчивость): успешно записанные данные никогда не теряются.

Уровням изоляции мы посвятили отдельный пост «Стоит ли бояться serializable-транзакций больше, чем труднонаходимых багов?». Сегодня же мы обсудим свойство атомарности. Автор известной книги «Высоконагруженные приложения» (Designing Data-Intensive Applications) Мартин Клеппманн считает, что название «Отменяемость» (Abortability) точнее отражает смысл и помогает избежать путаницы между атомарным коммитом и атомарной видимостью. И это звучит крайне логично, так как такое название лучше всего отражает суть этого свойства: всё или ничего.

Обратите внимание, что свойство «Атомарность» описывает поведение каждой отдельно взятой транзакции. В то время как «Изоляция» касается того, как транзакции взаимодействуют между собой. Двухфазный коммит (2PC), широко используемый в распределённых системах, является протоколом атомарного коммита: по завершении коммита либо все участники применили изменения, либо отменили их. При этом нет никаких гарантий атомарной видимости. Другими словами, даже если все узлы согласны с итогом транзакции, нет никакой гарантии, что изменения станут видимыми для всех участников одновременно — или что это произойдёт так, будто транзакция была выполнена как единая, сериализуемая операция.

Давайте рассмотрим пример. Пусть в некоторой базе данных хранится информация о двух счетах Алисы: X и Y. База шардирована по номеру счёта: счёт X хранится в одном шарде, а счёт Y — в другом. Изначально баланс каждого счёта равен 100 рублей. Алиса переводит 50 рублей со счёта X на счёт Y. Известно, что транзакция выполняется с помощью 2PC и читать можно только закоммиченные данные. В это время муж Алисы Вася проверяет общий баланс семьи и видит, что баланс составляет 150 рублей вместо 200, а ведь они договорились не тратить деньги и копить — семейная идиллия под угрозой! На иллюстрации ниже показано, как такое могло случиться.

Первый шард уже получил коммит от транзакции Алисы, поэтому транзакция Васи прочитала с него 50. Сообщение о коммите до второго шарда ещё не дошло, и он вернул исходное значение — 100. Поэтому Вася увидел общий баланс 150, а всего было три возможных результата: 150, 200 и 250. Рано или поздно всё сойдётся (eventual consistency) к 200. Получается, что из-за отсутствия распределённого снепшота у нас уровень изоляции «read committed».

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

Широкие транзакции в Citus не ACID

Citus — очень популярное решение для шардирования PostgreSQL, выполненное в виде его расширения. Его очень легко установить и использовать. Наверное, именно поэтому люди часто упускают из виду то, что фактически данное расширение преобразовывает PostgreSQL в СУБД другого типа и с другими гарантиями: всё хорошо, пока транзакция затрагивает один шард или же вам не принципиальна консистентность в широких транзакциях. Попробуем разобраться, почему так происходит. Стоит отметить, что мы не совершили никакого открытия и не первые, кто пишет об этом. Очень рекомендуем эти два поста Фрэнка Пашота: «Citus is not ACID but Eventually Consistent», «How ACID is Citus? (compared to YugabyteDB)». И «Sharded Does Not Imply Distributed» Дениса Магды. Мы не повторяем уже написанное, а пробуем посмотреть на это под другим углом.

Авторы Citus в своей статье «Citus: Distributed PostgreSQL for Data-Intensive Applications» описывают поведение широких транзакций следующим образом:

3.7.4 Компромиссы в многошардовых транзакциях. Многошардовые транзакции в Citus обеспечивают гарантии атомарности, согласованности и долговечности, но не обеспечивают изоляции распределённого снепшота (distributed snapshot isolation). Параллельный многошардовый запрос может получить локальный MVCC-снимок до коммита на одном шарде и после коммита — на другом. Для устранения этой проблемы потребуются изменения в PostgreSQL, чтобы сделать менеджер снепшотов (snapshot manager) расширяемым.

Это и привело к тому, что Вася не смог посчитать общую сумму на всех счетах, так как они лежат на разных шардах. К сожалению, на наш взгляд, в документации Citus этому не уделяется достаточного внимания. Так, в разделе «Table Management / Limitations» указано: «Отсутствует поддержка уровня изоляции serializable». При этом ни слова о других уровнях и многошардовых транзакциях.

Ещё больше путаницы вносит возможность указывать уровень изоляции транзакции, добавленная в 11.2. Только «Release Notes» содержат ясное объяснение, что по дефолту в Citus «Read Committed»:

По умолчанию, когда в транзакции участвуют несколько шардов, Citus всегда устанавливает уровень изоляции удалённой транзакции как BEGIN TRANSACTION ISOLATION READ COMMITTED

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

Обратите внимание, что это не означает, что Citus поддерживает полноценную семантику repeatable read или serializable для всех типов нагрузок

Интересно, что в Citus есть механизм консистентного бекапа и работает он следующим образом:

Citus поддерживает периодическое создание согласованной точки восстановления, которая представляет собой запись в WAL каждого узла. Точка восстановления создаётся при блокировке записей в таблице коммитов (commit records) на координаторе(-ах), что предотвращает коммиты выполняющихся 2PC-транзакций во время создания точки восстановления

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

Почему же Citus устроен именно таким образом? Вернёмся к архитектурной статье:

Существующие методы реализации изолированности распределённого снепшота (distributed snapshot isolation) имеют существенные накладные расходы по производительности из-за необходимости дополнительных походов по сети (network round trips) или ожидания синхронизации часов, что увеличивает время отклика и снижает достижимую пропускную способность. В контексте синхронного протокола PostgreSQL пропускная способность в конечном итоге ограничивается выражением: количество подключений / время отклика. Поскольку создание очень большого количества подключений к базе данных зачастую непрактично с точки зрения приложения, низкое время отклика остаётся единственным способом достичь высокой пропускной способности. Таким образом, если мы в дальнейшем и будем реализовывать распределённую snapshot изоляцию, то, скорее всего, сделаем её опциональной

Справедливости ради отметим, что не только в Citus столкнулись с подобным непростым выбором. Вот перевод выдержки из отчёта The Seattle report on database research от 2022 года:

Идёт противостояние между двумя школами: (a) В системах с большой пропускной способностью и требованиями к высокой доступности и низкой задержке обработку распределённых транзакций трудно масштабировать, не жертвуя при этом традиционными транзакционными гарантиями. Поэтому гарантии согласованности и изоляции ослабляются, что приводит к росту сложности со стороны разработчиков приложений. (b) Сложность реализации приложения без ошибок чрезвычайно высока, если система не обеспечивает строгую согласованность и изоляцию. Следовательно, система должна обеспечивать наилучшую возможную пропускную способность, доступность и низкую задержку — без жертв со стороны гарантий корректности. Это противостояние, скорее всего, окончательно не закончится в ближайшее время, и индустрия продолжит предлагать системы, соответствующие каждой из этих точек зрения

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

Стоимость распределённых транзакций

Когда мы говорим о реализации транзакций в СУБД, их время выполнения принято измерять в числе RTT между хостами базы и количестве операций ввода-вывода. Часто вводом-выводом можно пренебречь с учётом распространения NVMe. В случае PostgreSQL без синхронной репликации у нас всё супербыстро — 0 RTT. Как только мы добавляем синхронную репликацию, у нас становится 1 RTT.

Транзакции в Citus уже имеют 3 RTT: двухфазный коммит добавляет ещё 2 RTT. В распределённых СУБД консистентный глобальный снепшот достигается не бесплатно. Так, в YDB на выполнение распределённой (широкой) транзакции требуется 4.5 RTT плюс 0.5 мс на планирование транзакции и объединение транзакций в батчи.

Получается, что в теории Citus сильно хуже PostgreSQL, а распределённые СУБД ещё хуже Citus. Однако на практике всё зависит от значения RTT. Внутри одного дата-центра RTT пренебрежительно мал. Когда используется инсталляция с несколькими зонами доступности (дата-центрами), часто они расположены близко друг к другу и RTT между ними измеряется всего лишь сотнями микросекунд или единицами миллисекунд в зависимости от расстояния. На графике, приведённом ниже, показано время транзакций в зависимости от RTT.

При RTT в 7 мс разница между Citus и распределёнными СУБД составляет всего 10 мс, при этом у обоих типов СУБД время выполнения транзакции не превышает 50 мс, что достаточно для большинства приложений. Стоит ли жертвовать гарантиями корректности (консистентности), предоставляемыми приложениям со стороны СУБД, ради такого небольшого выигрыша?

Заключение

PostgreSQL — без сомнений, отличная и крайне эффективная СУБД. Однако с ростом нагрузки в какой-то момент её производительности становится недостаточно. И тогда встаёт выбор между переходом на шардированный Postgres или распределённую СУБД. И тут важно помнить о ключевом различии этих двух решений: обычно Citus-подобные решения в случае широких транзакций не ACID и не дают тех же гарантий, что PostgreSQL. Безусловно, не всем приложениям нужны как широкие транзакции, так и строгие гарантии (надеемся, что банки не из числа таких). При этом распределённые СУБД обеспечивают свойства ACID для любых транзакций и гораздо более эффективны, чем принято считать, — обратите на них внимание, если PostgreSQL вам станет мало.

На написание этого поста нас вдохновили публикации и выступления Фрэнка Пашота и Дениса Магды. Кроме того, мы выражаем особую благодарность Евгению Ефимкину, эксперту по PostgreSQL, и Андрею Бородину, активному контрибьютору в PostgreSQL.

Теги:
Хабы:
+31
Комментарии24

Публикации

Информация

Сайт
ydb.tech
Дата регистрации
Численность
101–200 человек
Местоположение
Россия