Pull to refresh
157.52
Postgres Professional
Разработчик СУБД Postgres Pro

Мифы и реалии «Мультимастера» в архитектуре СУБД PostgreSQL. Часть. 2

Reading time11 min
Views4.7K

Привет, Хабр! Это снова мы — Павел Конотопов (@kakoka) и Михаил Жилин (@mizhka), сотрудники компании Postgres Professional. Напомню, что Павел занимается архитектурой построения отказоустойчивых кластеров, а я анализом производительности СУБД. У каждого из нас за плечами более десяти лет опыта в своей области.

В первой части статьи «Мифы и реалии «Мультимастера» в архитектуре СУБД PostgreSQL» мы посмотрели как развивалась технология «Мультимастер» в экосистеме PostgreSQL. Обсудили существует ли «Честный Мультимастер», какие у него реализации и как его следует применять. Теперь поговорим о надёжности хранения данных.

Часть 2. Надёжность: гарантии согласованности данных и разрешение конфликтов

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

Конфликты бывают разные. Самое распространенное определение конфликта в БД может выглядеть так: две транзакции пытаются обновить одни и те же данные в один момент времени и могут друг друга взаимно заблокировать. Каждая транзакция будет ожидать, когда другая завершит свою работу. Как в этом случае должна вести себя БД, как ей решить какая транзакция «главнее»? Прервать обе? Действовать по принципам «прав тот, кто первый» или «прав тот, кто последний»?

Увы, не существует универсальной стратегии для разрешения всех возможных вариантов конфликтов. Что же с ними происходит в «Мультимастере»? Как правильно с ним работать? 

Первый и очевидный ответ: не нужно создавать ситуации, когда возникают конфликты. Правило «не создавать конфликты» относится прежде всего к проектированию приложения и к организации его работы с распределённым кластером. Общие рекомендации от вендоров (Postgres Professional, EnterpriseDB) звучат так: писать и изменять некоторые данные следует только в один из узлов кластера «Мультимастера», читать данные можно через все узлы.

Еще можно использовать бесконфликтные типы данных (в английской литературе используется аббревиатура CRDT, от Conflict-free Replicated Data Types). Кстати, на эту тему уже были хорошие публикации на Хабре, например, CRDT: Conflict-free Replicated Data Types, неплохая статья, которая может быть отправной точкой в понимании, что это такое.

Бесконфликтный реплицируемый тип данных не так давно «изобрели», если смотреть на время его появления в контексте академических исследований по теме баз данных. Марк Шапиро в 2011 году выпустил своё исследование по данной теме. 

Кратко расскажем про эти типы данных. Бесконфликтные реплицируемые данные можно разделить на две большие группы: конвергентные (CvRDT — state-based, синхронизация состояния) и коммутативные (CmRDT — operation-based, синхронизация операциями). 

Дефиниция бесконфликтного типа выглядит как сочетание математических операций и свойств, которые может удовлетворять избранный тип данных. Например, одновременные обновления данных могут быть коммутативны и удовлетворять равенству a+b=b+a. 

Если применить вышеописанное к базе данных, то между узлами кластера БД можно передавать не записанные транзакцией значения, а изменения (дельты). При этом не важно, в каком порядке они приходят на каждый узел: финальный результат всегда будет одинаков. Получается, что данные гарантированно сходятся, при том, что обновления данных могут выполняться на каждом из узлов. Заметим, что данный подход к репликации, скорее свойственен БД, в которых реализована Eventual Consistency (согласованность в конечном счете). По итогу, на какой-то момент времени БД будет гарантированно консистентна. Эта концепция не всегда подходит для реляционных БД, но это не значит, что она в них не применяется.

Репликация состояния возможна, когда мы оперируем, например, целыми числами или вектором состояния. Обновление состояния может происходить на любом из узлов кластера. Далее состояние распространяется, выполняется изменение на всех узлах слиянием. Каждый узел в конкретный момент времени при транзакции распространяет состояние на другой узел. Выборка состояния, применение его к другой БД, к другой реплике заключается в выборе наибольшего значения, например, по времени. Пусть у нас вектор состоит из целых чисел: x, y, z и метки времени timestamp, v = [x, y, z, timestamp], тогда последнее актуальное значение для сохранения будет последним временем измерения — max(t)(v1,v2)

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

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

Первый пример, который сразу приходит в голову — монотонно возрастающая последовательность, например, счётчик. Изменение данных можно описать как a = a + 1. При этом счётчик может обновляться на нескольких узлах кластера. Если на каком-то узле кластера происходит изменение счётчика, то можно тут же передать это изменение на другой узел и прибавить пришедшее изменение к значению или записать его в специальную колонку. Получится то, что изображено на схеме ниже: для получения значения счётчика на какой-то момент времени каждый раз происходит вычисление значения по имеющимся дельтам. Но так как реализация этого скрыта, то для клиента это выглядит как обычное получение данных с помощью операции SELECT. 

На сегодняшний день известно некоторое количество CRDT, для некоторых в PostgreSQL существует реализация, что упрощает и ускоряет репликацию данных для «Мультимастера» (EDB, pgEdge). Как уже было сказано, не для всех типов данных подходит бесконфликтная репликация, следовательно, всё равно требуется каким-то образом выявлять конфликты, которые неизбежно появляются.

Как выявлять конфликты?

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

1. При изменении данных в какой-либо строке в БД на одном узле (назовём его «узел-1») изменения передаются на другие узлы кластера с инструкцией: «найди эту строку в БД и поменяй в такой-то колонке значение X на значение Y». Чтобы найти эту строку на стороне приемника («узел-1» — источник, другие узлы кластера — приемники данных), нужен первичный ключ

2. Далее первичный ключ используется при проверке строк на блокировку. Если кто-то ещё попытается изменить строку, но на другом узле, то на «узел-1» придёт сообщение, похожее на предыдущее: «найди эту строку в БД и поменяй в такой-то колонке значение X на значение Z». Так как у нас образуются две транзакции, изменяющие одни и те же данные, то конфликт, который  нужно как-то разрешить. 

3. «Узел-1» может проигнорировать блокировку, пришедшую из другого узла кластера, а может установить блокировку или ожидать, пока транзакция, пришедшая с другого узла, не будет зафиксирована, в надежде, что не случится глобальной взаимоблокировки (deadlock). Такое тоже возможно, например, когда какая-то из транзакций «откатилась».

4. В моменты, когда проверяются или берутся блокировки, происходит определение конфликта (conflict detection) и его последующее разрешение (conflict resolution). Последнее может происходить и асинхронно, когда узлы сохранили данные в один момент времени, при этом разрешение конфликта отложили «на потом», сохранив и информацию о конфликте.

Конфликт определён: что же делать дальше?

Как разрешать конфликты?

Важно понять, какая из транзакций в конфликте «главнее»: для этого в «Мультимастере» реализован своеобразный «суд». При вынесении вердикта используется различная информация, например, откуда пришла строка, origin — источник реплицируемых данных, приоритет узла, или версия строки (row version), если версия старше, то отбрасываем младшее значение.

В реализации «Мультимастера» Postgres Professional пользователь принимает решение о способе разрешения самостоятельно (то есть настраивает способы разрешения конфликтов). При конфликте можно:

  • вернуть ошибку транзакции в сессию (ERROR);

  • пропустить эти изменения (SKIP) — лучше ничего не блокировать;

  • обновить эту строку (UPDATE).

Помимо этого, есть специальная настройка, контролирующая процесс проверки и разрешения конфликтов — multimaster.deadlock_prevention. Она необходима, чтобы не случилось взаимоблокировки (deadlock). Если установлено значение «off» — предотвращение взаимоблокировок отключено. Если установлено значение «simple» — конфликтующие транзакции отклоняются. Если установлено значение «smart», то для улучшения доступности ресурсов специальный алгоритм выбирает, какие транзакции фиксировать, а какие отклонить. 

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

Один из довольно остроумных способов избегания блокировок и борьбы с ними реализован в EnterpriseDB PGD (в Postgres Pro пока, увы, нет) — Column-Level Conflict Resolution (разрешение конфликтов на уровне колонок).

Column-Level Conflict Resolution позволяет двум транзакциям бесконфликтно обновлять одну и ту же строчку, но обновляемые данные должны находиться в разных колонках. Под «капотом» этого метода используется отслеживание времени изменения данных для каждого столбца в таблице. 

Для таблицы, где нужно таким образом разрешать конфликты, нужно выполнить ALTER TABLE <table_name> REPLICA IDENTITY FULL. Сама по себе эта функциональность не нова, она реализована в ванильном PostgreSQL для корректной работы логической репликации. Атрибут IDENTITY FULL позволяет изменять информацию, записываемую в журнал предзаписи для идентификации изменяемых или удаляемых строк.

Рассмотрим краткий пример. Создадим таблицу t(id bigint primary key, a int, b int), вставим в нее строку с id=1, попробуем обновить одну и ту же строку, но будем обновлять отдельными запросами данные в колонках a и b. Посмотрим на разные стратегии разрешения конфликта при обновлении строки. 

Итак:

  1. create table t(id bigint primary key, a int, b int);

  2. insert into t values (1,0,0);

  3. ALTER TABLE t REPLICA IDENTITY FULL;

  4. Узел 1: update t set a = 5 where id = 1;

  5. Узел 2: update t set b = 7 where id = 1;

При стратегии «ошибка» (error) у нас откатятся обе транзакции на обоих узлах.

При стратегии «последний коммит выигрывает» (last commit wins) получим результат — (1,0,7).

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

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

При стратегии «column-level» у каждой колонки будет временная метка, отражающая время последней ее модификации. Если временные метки не совпадают, то можно разрешить конфликт и получить консистентные данные: две транзакции обновили одну и ту же строку, но разные колонки, результат — (1,5,7). 

Непреднамеренное повреждение данных (Silent Data Corruption)

Представим, что у нас есть кластер «Мультимастера» из двух узлов и размер БД в 10 Тб. Так как в жизни бывает всё что угодно, мы не можем быть на 100% уверены, что в БД одного из узлов случайно не прилетит «излучение из космоса» и не испортит данные. Может возникнуть ситуация, когда в одном кластере на разных узлах находятся разные данные, что нарушает принцип консистентности.

Не так давно, в октябре 2023 года, коллеги из Alibaba Cloud опубликовали статью «Understanding Silent Data Corruptions in a Large Production CPU Population». Наиболее близкий по смыслу перевод может выглядеть как «Изучение скрытых повреждений данных на большом числе вычислительных узлов»  с результатами проверки своих дата-центров.

Исследователи измеряли, как часто может происходить Silent Data Corruptions (скрытые повреждения данных). Проверка дисков и процессоров показала, что некоторые процессоры из общего их количества неправильно вычисляют контрольные суммы. Отклонение составляет несколько базисных пунктов (для справки: % — процент = 1/100, ‰ — промилле = 1/1000, ‱ — базисный пункт = 1/10000). Как можно доверять процессорам, которые в одном случае из 10000 неправильно считают контрольную сумму? Понятно, что у Alibaba дата-центры занимают огромную площадь, поэтому попадание условного «космического луча» в какую-нибудь ячейку памяти более вероятно, чем при эксплуатации единичного сервера. Всё вышесказанное навело разработчиков на мысль, что в реализации «Мультимастера» должен быть инструментарий для проверки согласованности данных. Эти инструменты помогут убедиться, что данные на всех узлах консистентны. 

Проверка согласованности данных на узлах

В EnterpriseDB PGD для этого есть отдельный продукт Live Compare, который может сравнивать несколько баз данных кластера «Мультимастера» целиком на предмет аномалий. 

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

В Postgres Pro Enterprise для такой проверки есть отдельная функция: mtm.check_query('SELECT * FROM table ORDER BY id'). Она позволяет сделать снимок данных (транзакционный снимок) выбранной для верификации таблицы на всём кластере «Мультимастера», после чего сравнивает данные в пределах одного снимка.

Какие еще есть способы разрешения конфликтов?

Partner или Smart. Для этого варианта разрешения конфликтов кластер «Мультимастера» должен состоять из двух узлов, каждый из которых проверяет список статус транзакций другого. Он имеет представление о том, какие транзакции прошли, и с каким результатом они завершились. Если транзакция откатилась или, наоборот, повторяется, то  может возникнуть конфликт. Узел на котором она выполняется может посмотреть в память соседнего узла и принять решение о текущей транзакции на основе этого знания.

CAMO (Commit At Most Once). Это логичное развитие предыдущей идеи, которое решает вопрос производительности: при таком способе фиксация транзакции должна выполняться не более одного раза. Представим, что есть приложение, которое делает COMMIT, но при этом ответа от сервера не дожидается (например, что-то с сетью): что мы должны предпринять в следующий раз? Скорее всего, заново повторим транзакцию. Но вдруг эта транзакция уже зафиксирована? Тогда возникнет конфликт. В данном случае приложение является третьим участником нашего кластера, который может принять участие в консенсусе при коммите транзакции и знает о судьбе предыдущего коммита. От рабочего узла кластера он получает сведения, что коммит предыдущей транзакции прошёл успешно.

Eager Replication — определение потенциально конфликтующих транзакций с помощью консенсуса между узлами кластера. Реализованы распределенные транзакции с использованием протокола распределённого консенсуса RAFT.

Conflict triggers  создание специальных триггеров для разрешения конфликтов в особенных ситуациях.

Устойчивость к сбоям 

В «Мультимастере» при фиксации транзакций (глобальный коммит) для решения проблемы устойчивости к сбоям используют протоколы распределенного консенсуса:

  • Протоколы консенсуса RAFT (EDB) и PAXOS (Postgres Pro);

  • 2PC (двухфазный коммит);

  • Различные их комбинации с описанными выше способами разрешения конфликтов.

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

Транзакции должны быть зафиксированы на всех узлах кластера, использование для этого протоколов консенсуса может привести к двукратному или трехкратному сетевому обмену между узлами кластера. Схемы глобального коммита постоянно оптимизируются, для снижения количества шагов при глобальной фиксации транзакций в «Мультимастере» Postgres Professional на сегодняшний день это всего два шага.  

Резервное копирование и восстановление

Допустим, в кластере «Мультимастера» есть 3 узла (как мы надеемся, с одними и теми же данными). С какого узла нужно сделать резервную копию? Как следует восстанавливать  весь кластер ?

В кластере «Мультимастера» добавление нового узла происходит так:

  • снимается физическая резервная копия с любого рабочего узла;

  • резервная копия разворачивается на новом узле и запускается сервер СУБД;

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

Очевидно, что идентификатор БД всех узлов кластера — один и тот же. Поэтому резервную копию можно сделать с любого узла. Восстановление узла из резервной копии (если только мы напрямую не клонируем каталог БД с других узлов кластера) происходит для «Мультимастера» Postgres Professional следующим образом:

  • восстанавливаем каталог БД из резервной копии на нужном узле;

  • запускаем экземпляр сервера СУБД;

  • удаляем служебные метаданные «Мультимастера» из восстановленного узла;

  • выполняем добавление узла в кластер — SELECT mtm.join_node(node-id,'LSN');

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

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

Tags:
Hubs:
Total votes 19: ↑19 and ↓0+19
Comments37

Articles

Information

Website
www.postgrespro.ru
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Иван Панченко