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

Представьте, что вы пытаетесь перевести 100 долларов со счета А на счет Б, и оба счета находятся в одном банке. После того, как вы инициируете передачу, вы обновляете свой экран. Однако, когда вы обновляете свой экран, ваш общий баланс падает — эти 100 долларов, кажется, исчезают из воздуха. Вы видите, что счет А на 100 долларов меньше. Однако счет B не на 100 долларов больше. Затем вы обновляете экран пару раз, чтобы увидеть, что счет B заработал 100 долларов.

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

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

Однако перекос чтения становится проблемой при выполнении резервного копирования базы данных или аналитического запроса.

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

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

Устранение перекоса чтения

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

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

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

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

Эта функция называется изоляцией моментальных снимков и предлагается во многих реляционных базах данных, таких как PostgreSQL и MySQL.

Внедрение изоляции снэпшотов

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

Каждая транзакция имеет transactionId, а transactionId извлекается из базы данных. Таким образом, transactionId всегда увеличивается. База данных отслеживает каждый transactionId, записанный в базу данных, используя значения createdAt и deletedAt. База данных создала тег для этой операции с transactionId из транзакции после фиксации транзакции. Далее база данных делает снимок новой транзакции и помечает этот снимок последним идентификатором транзакции. Когда новая транзакция считывается из базы данных, база данных извлекает последнюю зафиксированную транзакцию перед транзакцией со следующими несколькими правилами:

  1. Любой идентификатор транзакции, который в настоящее время еще не зафиксирован в базе данных, не будет отображаться, даже если последующая транзакция будет зафиксирована.
  2. Любая прерванная транзакция также не будет отображаться.
  3. База данных не покажет никаких транзакций с transactionId позже (больше), чем текущий transactionId.
  4. База данных покажет любую другую транзакцию другим входящим транзакциям, читающим базу данных.

Давайте посмотрим, что происходит в сценарии Боба:

  1. Когда Боб инициирует транзакцию перевода, он запускает фоновый процесс перевода 100 долларов со счета A на счет B. Эта транзакция сначала вызовет базу данных или службу помощи, чтобы получить инкрементное transactionId, и инициирует транзакцию — скажем, транзакция 1234. .
  2. Последующая транзакция чтения должна будет сделать то же самое, получив инкрементное transactionId и вызвав запрос на чтение в базу данных — скажем, transactionId равно 1345.
  3. Пока передача еще не завершена, база данных не покажет Бобу данные, примененные transactionId 1234 (правило №1).
  4. Если другая письменная транзакция была инициирована после transactionId 1345 года, поскольку эта транзакция имеет большее transactionId значение, база данных не покажет эту транзакцию до transactionId 1345 года (правило номер 3).

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

Использование изоляции снэпшотов в распределенной среде

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

Как мы можем расширить изоляцию моментальных снимков в распределенной среде?

Будет трудно получить глобальный, постоянно увеличивающийся transactionId в распределенной среде. По одной причине каждая машина, которая потенциально находится в другой базе данных, может иметь свой счетчик UUID, и нам нужно иметь некоторую координацию для обеспечения причинно-следственной связи. Если транзакция B считывает значение из транзакции A, мы хотим гарантировать, что транзакция B будет иметь большее transactionId, чем транзакция A. Как нам быть с непротиворечивым моментальным снимком в реплицированной базе данных?

Можем ли мы использовать часы или время суток в качестве transactionId для записи в базу данных? Часы времени суток ненадежны, так как синхронизация NTP основана на ненадежных сетях. Следовательно, некоторые машины могут иметь перекос часов, произвольно движущийся назад во времени. Время одного узла также может отличаться от времени другого узла. Однако, если мы сможем сделать часы достаточно точными, они могут служить transactionId — более позднее время на часах означает, что события производятся позже. Как убедиться, что часы достаточно точны для transactionId?

При получении значений времени суток на каждой машине мы хотим, чтобы она возвращала доверительный интервал [Tbegin, Tlast] вместо получения одного значения. Доверительный интервал указывает, что часы имеют стандартное отклонение плюс или минус в диапазоне от Begin до Tlast. Если две транзакции, transactionX, transactionY входящие, [TbeginX, TlastX], [TbeginY, TlastY] и TlastX < TbeginY. Мы можем гарантировать, что transactionX предшествует tranasctionY. Однако, если значение перекрывается, мы не можем определить порядок. Этот подход используется Google Spanner для реализации изоляции моментальных снимков. Spanner будет намеренно ждать, пока не превысит доверительный интервал предыдущей транзакции, чтобы не перекрываться, чтобы зафиксировать текущую транзакцию. Таким образом, им нужно будет поддерживать доверительный интервал времени каждого такта на машине как можно меньше, чтобы избежать задержки. Google развертывает атомные часы или сервер GPS в каждом центре обработки данных, чтобы синхронизировать часы.

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

Подведение итогов

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

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

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

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

Как бы вы обеспечили согласованность чтения в распределенной системе? Как бы вы решили создать глобальный transactionId? Прокомментируйте их внизу!

Первоначально опубликовано на https://edward-huang.com.