Восстановление и репликация
В OrioleDB используется распределенный механизм восстановления, при котором каждому рабочему процессу (worker) назначается собственный набор значений первичного ключа для управления, что обеспечивает масштабируемое и эффективное восстановление и репликацию за счет разделения больших транзакций между несколькими рабочими процессами.
Разделение работы между несколькими процессами
В OrioleDB реализовано многопроцессное восстановление и репликация (технически, репликация в PostgreSQL — это основанное на сети восстановление, допускающее конкурентные запросы только для чтения). Основной процесс восстановления считывает поток WAL и распределяет сообщения между рабочими процессами восстановления (recovery workers) посредством очередей.
В отличие от других решений, работа не распределяется между рабочими процессами по транзакциям. Вместо этого каждый рабочий процесс отвечает за собственный набор ключей (значения первичного ключа для каждой таблицы). Таким образом, большая транзакция разделяется на фрагменты для каждого рабочего процесса. Существенным преимуществом данного подхода является возможность масштабирования восстановления и репликации независимо от степени параллелизма транзакций.
На рисунке ниже представлена схема восстановления с участием основного процесса восстановления и четырех рабочих процессов восстановления. Операции DML в примере относятся к некоторой единичной таблице, в которой первичный ключ является столбец id целочисленного типа. Поток WAL содержит транзакцию 1, состоящую из вставки с id = 1 и обновления с id = 2, а также транзакцию 2, состоящую из удаления с id = 2 и вставки с id = 3. Основной процесс восстановления распределяет эти операции по очередям на основе хеша столбца id (id = 1 в очередь 1, id = 2 в очередь 2, id = 3 в очередь 3).
Следует отметить, что основной процесс не распределяет сообщение о начале транзакции, поскольку он не знает, какие рабочие процессы будут задействованы в транзакции. Вместо этого к сообщениям о модификации строк прикрепляется идентификатор транзакции. Также следует отметить, что транзакции в OrioleDB не обязательно представляют собой непрерывные фрагменты в WAL. Они могут чередоваться.
Основной процесс восстановления отслеживает, какой рабочий процесс участвует в какой транзакции. После того, как транзакция получает подтверждение (commit) или отмену (abort) в потоке WAL, основной процесс распространяет это сообщение среди участвующих рабочих процессов.
Рабочие процессы восстановления не синхронизируются по завершении каждой транзакции. Таким образом, рабочий процесс #2 может обработать сообщение delete до того, как рабочий процесс #2 завершит обработку сообщения commit. Это возможно благодаря тому, что каждый рабочий процесс имеет собственное представление о завершенных транзакциях.
Процессы восстановления хранят статусы транзакций восстановления в recovery_xid_state_hash. При необходимости уточнения статуса транзакции процесс восстановления сначала проверяет recovery_xid_state_hash, и только затем — общую память. Основной процесс восстановления обновляет статус транзакции в общей памяти только после того, как все рабочие процессы уже обработали сообщение о завершении транзакции. Соответственно, после обновления статуса транзакции в общей памяти рабочий процесс может удалить свою запись из хеша.
Учитывая, что транзакция фактически разделена между основным процессом и несколькими рабочими процессами во время восстановления, соответствующий журнал отката (undo) также разделяется. На рисунке ниже это проиллюстрировано. Транзакция может иметь цепочку отката (undo chain) в основном процессе. Эта цепочка отражает записи отката транзакции, существовавшие во время создания контрольной точки, а также откат действий, воспроизведенных основным процессом восстановления (например, DDL). Одновременно транзакция может иметь одну или несколько цепочек отката в рабочих процессах восстановления.
Первичные ключи, TOAST и вторичные индексы
Восстановление должно привести все деревья таблицы к согласованному состоянию: первичный ключ, TOAST и вторичные индексы. Первичный ключ и TOAST являются основной информацией, тогда как вторичные индексы могут быть получены из них. Именно поэтому журнал WAL в OrioleDB содержит только изменения деревьев первичных ключей и TOAST.
Ключ дерева TOAST в OrioleDB содержит:
- Значение первичного ключа.
- Номер атрибута для TOAST-значения.
- Смещение внутри TOAST-значения.
Таким образом, одиночное значение представляется одним или несколькими кортежами листов в деревьях TOAST с разными смещениями (начиная с нуля).
В одной транзакции могут существовать несколько версий одного и того же кортежа. Соответственно, может существовать несколько версий TOAST-значений (если они были обновлены). Поэтому необходимо правильно сопоставить версию кортежа первичного ключа с версией TOAST-кортежа. Для этого к кортежу прикрепляется номер версии, как показано ниже.
Номер версии имеет область действия в пределах одной транзакции. Таким образом, в каждой новой транзакции номер версии начинается с нуля. Нулевой номер версии является значением по умолчанию. Если кортеж не содержит номер версии, то версия равна нулю. При обновлении кортежа первичного ключа, принадлежащего выполняемой транзакции, в пределах той же транзакции его версия увеличивается. TOAST-поля получают обновление, а TOAST-кортежи получают ту же версию, что и новый кортеж первичного ключа. Таким образом, при необходимости найти TOAST-кортеж, соответствующий данному кортежу первичного ключа, следует найти кортеж с наибольшей версией, меньшей или равной версии кортежа первичного ключа.
В OrioleDB требуется восстановление вторичных индексов из деревьев TOAST и деревьев первичных ключей. Вторичные индексы могут быть построены на TOAST-полях, что усложняет задачу.
Поэтому в OrioleDB контрольные точки записываются в следующем порядке:
- Деревья TOAST.
- Деревья первичных ключей.
- Деревья вторичных индексов.
Также в WAL сначала записываются TOAST-кортежи, а затем — кортежи первичных ключей.
По завершении создания контрольной точки деревьев первичных ключей текущая позиция WAL отмечается как «точка согласованности TOAST» (toast consistency point). Смотрите рисунок ниже.
Во время восстановления записи WAL применяются только к деревьям TOAST и деревьям первичных ключей до точки согласованности TOAST. В указанный период невозможно «потерять» какие-либо изменения вторичных индексов, поскольку контрольная точка для деревьев вторичных индексов создавалась позже. Таким образом, вторичные индексы уже содержат все изменения, внесенные до точки согласованности TOAST.
После точки согласованности TOAST начинается применение изменений ко вторичным индексам. Поскольку записи WAL для TOAST идут первыми, можно получить все необходимые TOAST-значения (если таковые имеются) и применить изменения ко вторичным индексам при применении записи WAL первичного ключа.