-
EmirVildanov authored
this adr is a proposal of implementing TRUNCATE sql command, its context and possible problems
EmirVildanov authoredthis adr is a proposal of implementing TRUNCATE sql command, its context and possible problems
- TRUNCATE нужно считать DDL-запросом и исполнять через стандартный алгоритм DDL с применением map_callrw
- Описание запроса
- Продуктовые требования к распределённому TRUNCATE
- Ребалансировка
- Линеаризация с остальными DDL
- Нынешний механизм исполнения DDL и DML-запросов
- Связанные задачи
- Выбранное решение
- Альтернативные (отвергнутые) варианты решения
- DDL и ребалансировка
status: accepted
decision-makers: @kostja, @darthunix, @gmoshkin
consulted: @Gerold103
map_callrw
TRUNCATE нужно считать DDL-запросом и исполнять через стандартный алгоритм DDL с применением Описание запроса
-
TRUNCATE t
-- это SQL-запрос, который очищает всё содержимое таблицы (по сутиDELETE FROM t
) - Для исполнения TRUNCATE-запроса на стороджах необходимо вызвать метод
box.space.truncate
- Он не вызывает
on_replace
тригеры Тарантула (в отличие от INSERT, UPDATE и DELETE) - Тарантул относит этот запрос к категории schema-change (DDL), а не data-change (DML) (см. документацию SQL), хотя в нашем форке исполнение операции номер схемы не меняет. Нужно иметь в виду, что локальный TRUNCATE Тарантула -- это операция, к которой в рамках транзакции нельзя применить rollback
- При вызове на сторадже функции
box.space.truncate
, она исполняется в фоне, не блокируя последующие вызовы (см. документацию space-api) - Для исполнения TRUNCATE на таблице t пользователю необходимо получить привилегию WRITE (в будущем её стоит заменить на отдельную привилегию TRUNCATE)
Продуктовые требования к распределённому TRUNCATE
Исходная задача на GitLab.
Ниже представлен список продуктовых требований к реализации TRUNCATE:
- TRUNCATE должен корректно исполняться с работающим ребалансером
- TRUNCATE должен быть линеаризован с другими DDL
- За счёт проверки привилегий TRUNCATE должен быть линеаризован с GRANT/REVOKE
- TRUNCATE должен либо полностью применяться, либо полностью не применяться. Нельзя удалить часть данных и вернуть ошибку. Ошибку можно вернуть, только если мы гаранитрованно не удалим ничего или удалим всё
- TRUNCATE не должен блокировать изменения топологии. Если TRUNCATE застрял из-за того, что часть узлов не могут выполнить какой-то из его шагов, то должна быть возможность удалить эти узлы из кластера или иначе их починить (например, узлы опущены и надо их просто поднять) так, чтобы TRUNCATE завершился успешно. В идеале TRUNCATE не должен ждать только применения на всех мастерах репликасетов, и не блокироваться о недоступные read-only реплики
Пожелания:
- TRUNCATE не должен блокировать DDL-запросы к другим таблицам
Ребалансировка
Необходимо избежать следующего аномального сценария с ребалансировкой:
- Начинается vshard-ребалансировка бакетов. Данные с одних инстансов начинают переезжать на другие (инстанс удаляет у себя данные таблицы и отправляет запрос, пересылающий эти данные на другой инстанс)
- Исполнение TRUNCATE t начинается на узле-координаторе
- До вершин кластера доходит TRUNCATE t, затирая все данные
- До таблиц на инстансах доходят данные, переехавшие из-за ребалансинга
- Получается, что TRUNCATE исполнился, а в таблицах всё равно остались данные
Линеаризация с остальными DDL
Необходимо избежать следующей ABA-проблемы:
- Исполняется CREATE TABLE t
- Таблица наполняется данными (через INSERT)
- Исполнение TRUNCATE t начинается на узле-координаторе
- Исполняется DROP TABLE t
- Ещё раз исполняется CREATE TABLE t
- Ещё раз таблица наполняется данными (через INSERT)
- До вершин кластера доходит TRUNCATE t, которая исполняется на новой таблице
исполнение TRUNCATE должно упасть из-за того, что версия новой таблицы не совпадает с версией таблицы, над которой необходимо исполнить TRUNCATE.
Нынешний механизм исполнения DDL и DML-запросов
Исполнение DDL-запроса добавляет запись (CaS-опкод) в рафт-журнал и увеличивает номер схемы, консистентно меняя состояние всего кластера. Общий сценарий работы с рафт-журналом и обработки опкода сейчас такой:
- Добавляется DdlPrepare-опкод в рафт-журнал на лидере
- raft-main-loop (
advance
->handle_committed_entries
->handle_committed_normal_entry
) на всех узлах кластера обрабатывает DdlPrepare. Выполняет специфичную для опкода работу (например, для CreateTable создаёт записи в _pico_table и _pico_index) и создаёт запись в _pico_property с PendingSchemaChange (это значит, что операция начала применяться) - Губернатор, просыпаясь в очередной раз, видит PendingSchemaChange, создаёт
Plan::ApplySchemaChange с указанием всех мастеров репликасетов. Через
proc_apply_schema_change
он посылает всем мастерам запрос на применение этого изменения - Мастера, исполняя
apply_schema_change
, выполняют специфичную для опкода работу (например, для CreateTable создаёт таблицу и primary key в локальных таблицах _space и _index) и черезset_local_schema_version
обновляют локальную версию схемы до PendingSchemaVersion (в системной таблице Тарантула _schema мы храним своё значение с ключом "local_schema_version") - Получив положительные ответы со всех мастеров, говернор добавляет в
raft-журнал DdlCommit-опкод. В противном случае добавляет DdlAbort-опкод.
Стоит обратить внимание, что в случае получения
OnError::Retry(e)
(например, по таймауту) говернор будет в бесконечном цикле пытаться заново исполнить сетевые вызовы. - raft-main-loop на всех узлах обрабатывает DdlCommit/DdlAbort
- Для DdlCommit исполняет
apply_schema_change
(в случае если этот мастер отстал от рафт-лидера), выставляет флагis_operable
в true (предполагается, что Dml-запросы к таблице с невыставленным флагом не будут обрабатываться) и удаляет PendingSchemaChange - Для DdlAbort исполняет
ddl_abort_on_master
(например, для CreateTable подчищает записи в локальных таблицах _space и _index)
- Для DdlCommit исполняет
Логика исполнения DML-запроса зависит от того, с какой таблицей мы работаем:
- Запрос над глобальными пользовательскими таблицами выполняется через raft-журнал в виде CaS DML-опкодов (так же консистентно меняя состояние кластера, как в случае с DDL-запросами).
- Для шардированных таблиц выполняется vshard-функция replicaset::callrw. Сейчас
идёт переход на новую, более консистентную функцию
map_callrw
, поэтому будем дальше ссылаться на неё. В эту функцию передаётся сгенерированный ExecutionPlan, который на стороджах трансформируется в транзакционный вызов функций space-api либо в исполнение локального SQL.- DML-запросы над таблицей t с узла-координатора на стораджи отправляются с
указанием table_version, которая была прочитана на координаторе на момент
вызова
map_callrw
. Любой DDL-запрос, затрагивающий таблицу t, после успешного исполнения увеличивает table_version этой таблицы (в системной таблице _pico_table на каждом инстансе). В случае, если между отправкой DML-запроса с узла-координатора и исполнением этого запроса на стороджах над этой таблицей был исполнен DDL-запрос (мы заметим, что отправленная черезmap_callrw
версия таблицы отличается от той, что мы прочитали локально), DML-запрос исполнен не будет и мы получим ошибку. - Функция
map_callrw
до исполнения пишушего запроса на стораджах рефает (ref) все затрагиваемые стороджа. См. комментарии к функции. Делает она это для того, чтобы убедиться в отсутствии ребалансировки во время пишущего запроса. Иначе мы бы могли столкнуться с НЕприменением пишущих запросов к данным таблиц переезжающих с одного стораджа на другой.
- DML-запросы над таблицей t с узла-координатора на стораджи отправляются с
указанием table_version, которая была прочитана на координаторе на момент
вызова
Скажем сразу, чем при описанном механизме исполнения хорошо считать TRUNCATE -- DDL-запросом:
- мы будем отбивать часть DML-запросов. Таких, для которых между отправкой с
узла-координатора до момента исполнения на сторадже уже успел исполниться
TRUNCATE t. Или в целом все запросы на таблицу t, если учесть, что мы
собираемся проверять
is_operable
флаг - отбивать все остальные DDL-запросы к этой таблице до тех пор, пока
is_operable
не будет выставлен в true
Связанные задачи
Реализация TRUNCATE, удовлетворяющая описанным выше требованиям, затрагивает другие сейчас незакрытые задачи:
- Неконсистентное исполнение DDL-операций при работающем ребалансировщике. См. тикет
- Неконсистентное исполнение через вызов
map_callrw
для DML-запросов на шардированных таблицах. Запрос может провалиться на части из узлов (например, по таймауту при разрыве сети), и мы с этим ничего не делаем. Эту проблему нужно будет решить в том случае, если мы решим реалзивывать TRUNCATE как DML-операцию
Выбранное решение
В качестве решения предлагается считать TRUNCATE DDL-запросом и пускать
исполнение TRUNCATE по описанному выше алгоритму DDL, но на этапе обработки
Plan::ApplySchemaChange на гаверноре вызывать proc_apply_schema_change
не
напрямую через rpc, а через map_callrw
. Таким образом мы избежим необходимости
отдельно чинить ребалансировку бакетов для DDL, потому что map_callrw
уже
поддерживает ref стораджей (pin бакетов). Механимзм рафт-журнала обеспечит нам
автоматическое применение TRUNCATE на упавших узлах, которые вовремя не получили
запрос (то есть во время исполнения TRUNCATE может и не исполниться на всех
узлах, но зато исполнится в момент восстановления узла кластера при чтении
рафт-журнала лидера). Алгоритм целиком будет выгледеть так:
- Добавить DdlPrepare-опкод в рафт-журнал на лидере
- При обработке DdlPrepare создать PendingSchemaChange и выставить для таблички флаг is_operable в false
- На губернаторе при обработке Plan::ApplySchemaChange через
map_callrw
вызватьproc_apply_schema_change
для применения на всех узлахbox.space.truncate
. Повторять запрос до тех пор, пока он не удастся. После успешного исполненияmap_callrw
добавить DdlCommit-опкод в рафт-журнал - При обработке DdlCommit выставить флаг
is_operable
в true и удалить PendingSchemaChange
Данное решение требует внесения наименьшего количества изменений и использует
уже реализованные алгоритмы. Из-за работы с PendingSchemaChange исполнение
TRUNCATE t
будет блокировать остальные DDL-запросы, даже не связанные с
таблицей t. Однофазное исполнение будет более оптимальным решением, но пока что
было решено отложить эту оптимизацию.
Альтернативные (отвергнутые) варианты решения
-
Считать его DML-запросом и исполнять через
map_callrw
. Функция сама останавливает ребалансировку, не блокирует другие DDL-запросы, но может исполниться неконсистентно (см. проблему выше). Единственное, что мы можем предпринять в случае получения ошибки с одного из мастеров -- это бесконечно повторять исполнение до тех пор, пока оно не удастся на всех мастерах, что кажется плохим решением -
Считать его DDL-запросом, исполнять по схеме DDL, починить исполнение DDL при работающем ребалансировщике (см. секцию ниже). Нужно будет добавить специфичную для TRUNCATE логику в процесс обработки DDL:
- На этапе DdlPrepare выставить флаг is_operable в false
- На этапе
proc_apply_schema_change
проверять, что таблица существует - На этапе DdlCommit исполнять
box.space.truncate
(добавить эту логику вapply_schema_change
в случае выставленного флагаis_commit
) - На этапе DdlAbort исполнять
box.space.truncate
-
Реализовать исполнение TRUNCATE через одну запись в рафт-журнал:
- Записать TRUNCATE в рафт-журнал. В этой же транзакции повысить версию схемы
- Во время apply исполнить
box.space.truncate
В момент исполнения TRUNCATE или DROP TABLE мы уверены в том, что не получим Abort ответ, потому что подобный ответ мы можем получить только в случае отсутствия таблицы, над которой работаем (а raft гарантирует, что этого не произойдёт, потому что все операции линеаризованы). В таком варианте мы не будем блокировать остальные DDL-запросы из-за установленного PendingSchemaChange, но придётся реализовать новый механизм исполнения. Было предложено отложить реализацию этого алгоритма.
DDL и ребалансировка
Проблему с DDL и ребалансировкой предлагается решить следующим способом.
- Перед исполнением любой DDL-операции вручную останавливать ребалансинг,
дёргая ручку
vshard.storage.rebalancer_disable
на каждом из мастеров. Например, при обработке ApplySchemaChange добавить второй цикл, который пробежиться по всем мастерам и дёрнет на них эту ручку. А при завершении обработки позватьvshard.storage.rebalancer_enable
- Пропатчить vshard таким образом, чтобы при ребалансировке (см., например,
bucket_send_xc
иbucket_recv_xc
в vshard/storage/init.lua) отправлять помимо данных таблиц ещё и номер схемы (наш local_schema_change), прочитанный на узле-отправителе в момент вызоваreplicaset:callrw
. В случае, если номер схемы на получателе оказался выше номера схемы на отправителе (то есть за это время, например, успел исполниться какой-то DDL-запрос) предлагается останавливать процесс ребалансировки.-
rebalancer_worker_f
-- это тело файберов, которые запускаются для ребалансировки. Все они делят так называемый dispenser, который представляет собой контейнер путей между узлами, по которым необходимо совершить ребалансировку.rebalancer_worker_f
крутится в цикле до тех пор, пока dispenser выдаёт пути для ребалансировки - При вызове
bucket_send
в случае возникновения ошибки НЕShardingError или ошибки ShardingError кроме TOO_MANY_RECEIVING (воркер заснёт в качестве троттлинга) путь из dispenser удаляется, потому что считается, что кластер поломан. В случае иной ошибки, путь кладётся обратно в dispenser. Кажется, что наподобие TOO_MANY_RECEIVING можно добавить ещё и ошибку вроде OLD_SCHEMA_VERSION, которая так же положит путь обратно в dispenser, чтобы он потом вызвалbucket_send
с новой версией схемы
-