Мультиверсионность и транзакционность динамических таблиц

В данном разделе описаны транзакции применительно к динамическим таблицам. Транзакционная модель статических таблиц описана в разделе Транзакции статических таблиц.

Транзакции делятся на мастер-транзакции и таблет-транзакции. Мастер-транзакции позволяют выполнять операции с метаинформацией мастера. Таблет-транзакции — только записывать данные в динамические таблицы.

Чтение и модификация данных в динамических сортированных таблицах производится с помощью таблетных транзакций.

Для изоляции транзакций и разрешения конфликтов используется модель MVCC. Уровень изоляции по умолчанию — snapshot (не serializable snapshot).

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

Ограничения

При использовании транзакций следует иметь в виду:

  • транзакции должны быть короткими (ограничение по умолчанию 1 минута);
  • транзакции должны записывать ограниченные объемы данных (ограничение по умолчанию 100 000 строк).

Вычисление момента последней записи нетривиально, так как часть данных уже может быть записана на диск. Попытка прочитать с диска такие данные приводила бы к тому, что запись по ключу превратилась бы в случайное чтение по ключу. Дизайн системы YTsaurus предусматривает, что все записи в неё выполняются быстро. С точки зрения дисковой активности доступ последовательный. Поэтому реализована следующая логика:

  • динамические сторы (dynamic_store), хранящие свежезаписанные данные, удерживаются в памяти узла кластера некоторое гарантированное время, даже если они уже записаны в чанки на диск;
  • проверка по таким динамическим сторам быстрая, так как не требует обращения к диску;
  • система запрещает коммит транзакций, время жизни которых превышает некоторую настраиваемую границу.

Временные метки

Каждое значение, сохранённое в сортированной динамической таблице, аннотировано временной меткой timestamp. Временные метки представляют собой 64-битные целые числа, порождаемые кластером по необходимости.

При старте любой транзакции происходит порождение стартовой временной метки. Все чтения динамических таблиц в пределах транзакции производятся относительно данной метки, тем самым порождая срез (snapshot) состояния данных в этих таблицах.

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

Временные метки в пределах кластера выдаются централизовано на мастер-серверах. Для лучшей масштабируемости клиенты запрашивают временные метки не напрямую, а через специальные прокси-серверы, которые буферизуют запросы. С практической точки зрения генерация новой временной метки стоит менее 10 ms в случае, когда кластер и клиент находятся в рамках одного ДЦ. Суммарная пропускная способность их генерации практически не ограничена.

Для временных меток гарантируются:

  • уникальность — все полученные временные метки будут различны;
  • монотонность — если A обозначает событие получения временной метки T(A), а B обозначает событие заказа временной метки T(B), причем A случилось до B, то T(A) < T(B)).
  • консистентность — если транзакция A имеет метку коммита Tc(A), а транзакция B — метку начала Ts(B), причем Tc(A) < Ts(B), то транзакция B "увидит" все изменения, выполненные транзакцией A (и только такие). Верно и то, что любая транзакция в процессе работы "не видит" собственных изменений (т. к. изменения ещё не получили никакой временной метки).

Дополнительно временные метки устроены так, что по ним легко вычислить приближённое физическое время момента порождения с погрешностью порядка секунды.

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

Ослабление гарантий

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

Для обеспечения атомарности используется техника двухфазного коммита, а для обеспечения сохранности данных сервер дожидается записи данных на диск. Суммарно данные гарантии вносят существенный вклад в латентность операций записи (сотни миллисекунд). Тем не менее бывают пользовательские сценарии, когда допустимы менее строгие гарантии, но требуется заметно меньшая латентность операций записи. Для поддержки таких сценариев в системе есть ряд параметров, позволяющих задавать пониженные уровни атомарности и сохранности.

Атомарность

Атомарность транзакций означает, что если пользователь в момент времени t наблюдает эффект (записанные данные) от транзакции T, то в любой последующий момент времени t' он наблюдает все эффекты от транзакции T. Например, если пользователь 1 поменял строки с ключами k_1 и k_2 в одной или разных динамических таблицах, то невозможна ситуация, когда пользователь 2 прочитает измененную строку k_1, но неизмененную строку k_2.

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

  • full — означает, что транзакции полностью атомарны (режим по умолчанию). В режиме full изменения становятся атомарно видимыми в любой момент времени после commit timestamp (как было указано ранее, в этом режиме система обеспечивает уровень изоляции snapshot isolation).
  • none — означает, что система не дает никаких гарантий об атомарности внесения изменений. В режиме none возможны ситуации, когда коммит завершился ошибкой (к примеру, по причине недоступности какого-либо таблета), но часть изменений была зафиксирована и стала видна пользователям.

Неатомарные транзакции

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

Записываемые неатомарными транзакциями значения получают метки времени, но метки формируются клиентами самостоятельно на основе данных системных часов. Система YTsaurus при этом проверяет, что данные метки не отличаются от серверных, формируемых сериализуемым образом, более чем на значение client_timestamp_threshold (по умолчанию одна минута) в конфигурации системы. Если разница между клиентскими и серверными часами превышает указанный порог, клиент получит ошибку Transaction timestamp is off limits, check the local clock readings.
Даже при неатомарных модификациях в пределах строки гарантируется уникальность и монотонность меток. Для этого система YTsaurus при необходимости исправляет в большую сторону метки, получаемые от клиента.

Неатомарные транзакции не берут блокировок, поэтому при одновременной записи учитывается та транзакция, которая коммитится последней (last write wins). Успешное завершение неатомарной транзакции означает применение всех изменений на сервере.

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

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

  • указать для таблицы атрибут atomicity равным none.
  • указать режим atomicity=none при старте транзакции в её настройках.

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

  • идеологическая — для того чтобы места в коде, где происходят неатомарные коммиты, были явно отмечены;
  • техническая — при старте атомарной транзакции происходит дополнительная задержка, связанная с вычислением стартовой метки времени. Для неатомарных транзакций данная метка не имеет смысла. Тем самым, оказывается невозможным бесплатно стартовать неатомарную транзакцию и выяснить факт ее неатомарности лишь в момент коммита.

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

Поэтому для таблиц с atomicity=none рекомендуется выставить следующие опции:

min_data_ttl = 0
merge_rows_on_flush = %true

В режиме неатомарных транзакций система ведет себя практически аналогично Apache HBase. За счет ослабления гарантий каждая запись требует лишь одного round-trip к серверу и одного цикла записи данных на диск (десятки миллисекунд).

Сохранность

Сохранность транзакции означает, что если пользователь в момент времени t успешно закоммитил транзакцию T (получил подтверждение от сервера), то в любой последующий момент времени t' он наблюдает все эффекты от транзакции T.

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

  • async — транзакция была принята сервером, сохранена в памяти, и, при отсутствии сбоев, когда-нибудь будет записана на диск (только для неатомарных транзакций);
  • sync — транзакция была принята сервером, подтверждена кворумом серверов и сохранена в журналы на диск; тем самым, при правильных настройках репликации и отсутствии серьёзных проблем, переживает сбои (режим по умолчанию).

Поддерживаемые операции

Создание транзакции

В системе YTsaurus существует два вида транзакций:

  • мастер-транзакции — создаются и поддерживаются на мастере; дают возможность работы только с Кипарисом;
  • таблет-транзакции — создаются клиентами без участия мастера; дают возможность работы с динамическими таблицами, но не с Кипарисом.

Вид транзакции указывается клиентом при ее создании. При запуске транзакции система генерирует временную метку начала транзакции. В случае мастер-транзакции временная метка регистрируется на мастере и периодически опрашивается клиентом. В случае таблет-транзакции её состояние обслуживает клиентская библиотека, с точки зрения системы YTsaurus транзакция не видна и ничего не стоит.

В пределах одной таблетной транзакции можно выполнять запись в произвольный набор строк одной или нескольких динамических таблиц. Запись в динамические таблицы из мастер-транзакций невозможна.

Для таблет-транзакций можно указать настройки atomicity и durability.

Запись строки

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

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

При указании части полей существуют режимы:

  • overwrite — все неуказанные поля обновляют свои значения на null (режим по умолчанию);
  • update — включается опцией update == true. В таком случае сохранится предыдущее значение. В этом режиме необходимо передать все колонки, помеченные атрибутом required.

Примечание

При чтении и выполнении SQL запроса видны данные только на момент начала транзакции. Изменения, записанные в пределах той же транзакции, для чтения недоступны.

Завершение транзакции

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

Прерывание транзакции

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

Проверка конфликтов транзакций

Для проверки и предотвращения конфликтов между атомарными транзакциями, которые работают (блокируют на чтение или модифицируют) с пересекающимися по ключам с данными, на строках таблиц существуют блокировки или локи:

  • в случае блокировок на чтение используются shared-локи.
  • для модификации данных используются exclusive-локи.

В простейшем случае без дополнительной настройки с каждым ключом связан один основной shared-exclusive lock. Данная функциональность реализована в виде счетчика shared-блокировок и флага exclusive-блокировки.

По умолчанию блокировки распространяются сразу на целые строки. В системе существует возможность повысить гранулярность блокировок, для этого следует разметить колонки в схеме атрибутом lock, указав в нём имя дополнительного лока. Тем самым можно разбить колонки на группы, каждая из которых управляется своим локом. Если для колонки не указан атрибут lock, то считается, что она управляется основным локом. Если транзакция записывает по ключу, то она берет локи, управляющие всеми затронутыми колонками.

Нельзя взять дополнительный лок, если другой транзакцией уже взят основной. При удалении строки транзакция берет основной лок.

Блокировка на модификацию

Гарантируется, что не может быть двух транзакций, одновременно модифицирующих один и тот же ключ.
При модификации данных по ключу — записи или удалении — транзакция в момент коммита берет exclusive-лок по данному ключу. Если указанный лок уже взят другой модифицирующей транзакцией или некоторым количеством читающих транзакций, взявших shared-локи, то возникает конфликт и транзакция прерывается.

Гарантируется, что по одному и тому же ключу не могут записывать данные транзакции, время жизни которых пересекается.
Локи на строки берутся и удерживаются только на протяжении коммита. До этого времени данные находятся на клиенте. Коммит в системе двухфазный, и его процесс начинается с временной метки prepare, а заканчивается временной меткой commit.
Например, транзакции A и B стартовали примерно одновременно, затем A выполнила запись по ключу k и закоммитилась. Теперь транзакция B могла бы выполнить запись по k и закоммититься, так как никакого лока на k больше нет. Но такое поведение противоречит уровню изоляции snapshot, обеспечиваемому системой.

Поэтому в момент модификации транзакцией B ключа помимо проверки наличия лока на этом ключе система также проверяет:

  • не было ли по данному ключу записей с метками времени больше Tstart(B);
  • не был ли данный ключ заблокирован другой транзакцией до времени, большего Tstart(B).

Если хотя бы одно из условий не выполняется, возникает конфликт.

Блокировка на чтение

В случае блокировки на чтение кроме проверки возможности взятия shared-лока проверяется только отсутствие модификаций по данному ключу с метками времени больше Tstart(B). Структура для проверки конфликтов транзакций определяется счетчиком shared-блокировок, флагом exclusive-блокировки и двумя временными метками — временем до которого строка была заблокирована на чтение и временем последней модификации строки.

По окончании модификации строки exclusive-лок отпускается, а в значение времени последнего изменения записывается временная метка commit. При завершении транзакции, берущей блокировки на чтение (shared-локи), по окончании процесса коммита отпускается shared-лок, и в случае strong-блокировки записывается временная метка коммита транзакции. В случае weak-блокировки временная метка не записывается.