Обслуживание MVCC: VACUUM и autovacuum
Введение
В СУБД Pangolin для изоляции параллельных транзакций выбрана модель PostgreSQL MVCC (Multi Version Concurrency Control, управление параллельным доступом посредством многоверсионн ости). Реализация PostgreSQL предполагает хранение непосредственно в таблицах и индексах СУБД множества копий каждой модифицируемой записи, по одной на каждую из модификаций.
Постепенное накопление полностью устаревших версий записей (dead tuples), хранимых вместе с активными данными изменяемых объектов - известный недостаток реализации, который называется «раздуванием» или bloating. При раздувании таблиц и индексов расходуется дисковое пространство, заполняется память, падает производительность сервера. Требуется все больше и больше процессорных операций для выделения хаотично разбросанных действительных записей.
Устаревшие записи не получается выделить и удалить сразу же в момент устаревания. Причина в том, что они на протяжении произвольного промежутка времени требуются для работы нескольких взаимосвязанных механизмов сервера. Как минимум, исторических записей требуют:
- соседние транзакции и их снимки операций (запросов);
- мультитранзакции;
- подготовленные транзакции;
- физические реплики в меру возможного отставания от лидера.
Правила видимости данных в каждой из тр анзакций MVCC Pangolin основаны на сравнении 32-битного идентификатора транзакции с идентификаторами соседних транзакций и сроками действия версионированной истории каждой из записей. Пространство идентификаторов транзакций строго ограничено, поэтому по мере поступления новых транзакций требуется постоянное высвобождение номеров транзакций «древней истори» СУБД.
Рассмотрим упомянутые объекты внимательнее и изучим настройки стандартных средств для их обслуживания.
Изоляция транзакций и история записей в MVCC Pangolin: подробности
Атомарная единица, по которой учитываются модификации (версии одной записи) в MVCC - одна транзакция.
Чтобы организовать очередь согласованных транзакций, сервер имеет внутренний счетчик транзакций - общий для экземпляра (кластера баз) системный объект, который работает как последовательность. Счетчик с момента инициализации кластера баз вначале монотонно возрастает, а затем допускает повторное использование значений. Почти как последовательность с атрибутом CYCLE, но есть особенности, которые описаны ниже.
Каждая транзакция на старте выбирает значение этого счетчика как следующий идентификатор из последовательности. Его и называют идентификатором транзакции или XID. Оно сохраняется за транзакцией и всеми данными, которые она изменила, от старта до «заморозки» - переноса транзакции в исторический архив (подробнее заморозка описана ниже в отдельном разделе).
Когда транзакция выполняется над записью DML, она завершает историю старой версии и, возможно, добавляет обновленную копию в виде новой версии записи. Каждая из версий автоматически помечается «сроком действия»: от XID транзакции, которая добавила версию (xmin) и до XID транзакции, которая ее удалила (xmax). Если версия активна (действительна, не удалена), то во втором поле xmax у нее может быть 0 или идентификатор такой удаляющей транзакции, которая уже отменена или еще активна, не з афиксирована.
Сроки действия разных версий одной записи не пересекаются никогда.
Номера транзакций невозможно напрямую сравнить между собой. Например, транзакция № 100 моложе транзакции с номером 3 миллиарда (почему - см. ниже). Поэтому для хранения номеров транзакций определен отдельный служебный тип данных: xid. Значения этого типа нельзя напрямую сравнивать, складывать или вычитать. Стандартная функция age(<идентификатор: xid>) возвращает «возраст» транзакции в единицах счетчика - количество номеров, выданных от ее старта до последнего значения счетчика транзакций на текущий момент. Возраст транзакций имеет тип integer, эти значения можно сравнивать между собой, складывать и вычитать.
Горизонты событий и снимки
При сессии, выполняющей транзакцию, хранится идентификатор самой старой из соседних транзакций, активных на момент старта этой транзакции. Он называется «горизонтом событий» транз акции. Эта граница отделяет в представлении транзакции «древнюю историю» («все умерли до моего появления») от «современной истории» («когда я появился, кто-то из них уже существовал и мог действовать параллельно со мной»). Все активности транзакций старше горизонта с точки зрения транзакции полностью завершены, а их статусы для транзакции безразличны, потому что любая из ее операций одинаково увидит все их результаты.
Дополнительно строится свой согласованный образ базы на каждый из запросов (при уровне изоляции READ COMMITTED) или на первый запрос в транзакции (уровень REPEATABLE READ и выше). Для этого сессия на старте запроса/транзакции запоминает:
- xid старшей из транзакций, активных прямо сейчас -
xmin
(можно понимать как локальный «горизонт» этой операции); - следующий, еще никому не выделенный сейчас xid -
xmax
(этот xid когда-то будет присвоен следующей транзакции в очереди); - список xid всех активных сейчас транзакций -
xip_list
.
В продолжение всей операции (запроса в REPEATABLE READ или транзакции в READ COMMITTED) правила видимости результатов между транзакциями таковы:
- результаты транзакций до xmin - принять;
- активности транзакций от xmin до xmax - просматривая
xip_list
, принять активности зафиксированных транзакций и отбросить активности транзакций, которые не завершены или отменены; - активности самой операции - принять;
- активности более молодых транзакций (xid >= xmax) - отбрасывать, чтобы не разрушить представление о данных, которое было согласовано на момент старта операции.
Для того чтобы отбрасывать эффекты соседних транзакций, не требуется, как в Oracle, по каждому блоку данных читать векторы отмены (undo records
) и формировать в памяти исторический образ блока. Поскольку здесь исторические версии записей хранятся вместе с текущими, достаточно из версий записи выбрать ту, которая действовала на момент старта операции.
Для операции (запроса/транзакции) совокупность (xmin, xmax, xip_list
) называется «снимком» (snapshot). Каждый снимок исчерпывающе определяет согласованное состояние базы, видимое в заданной операции.
Пример просмотра горизонта событий транзакции:
First_db=# BEGIN;
BEGIN
First_db=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
19838
(1 row)
Пример просмотра снимков операций:
$ pgbench -c 10 -j 5 -T 120 First_db &
<..>
$ psql -d First_db
<..>
First_db=# BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
First_db=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
194230
(1 row)
First_db=*# SELECT txid_current_snapshot();
txid_current_snapshot
--------------------------------------------------
200128:200174:200128,200161,200168,200170,200172
(1 row)
First_db=*# SELECT txid_current_snapshot();
txid_current_snapshot
-----------------------------
204776:204782:204776,204780
(1 row)
First_db=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
194230
(1 row)
First_db=*# SELECT txid_current_snapshot();
txid_current_snapshot
-------------------------------------------
247416:247432:247416,247428,247429,247430
(1 row)
First_db=*# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
194230
(1 row)
First_db=*# COMMIT;
COMMIT
First_db=# SELECT backend_xmin FROM pg_stat_activity WHERE pid = pg_backend_pid();
backend_xmin
--------------
260196
(1 row)
Чтобы удовлетворить требования всех возможных снимков, устаревшие версии строк должны храниться в базе как минимум до тех пор, пока они потенциально могут оказаться интересными любой из выполняющих транзакции сессий. Поэтому каждая из длительных тра нзакций
- мешает очистке исторических данных, заставляет удерживать значительное количество устаревших версий записей в таблицах;
- вносит накладные расходы при разрастании массива статусов транзакций и сложности снимков операций, усложняет правила видимости версий записей в операциях (подробности – ниже).
Сервер по умолчанию автоматически поддерживает такое состояние по каждой из таблиц, в котором сохранены все версии строк, потенциально интересные любой из сессий, имеющих возможность увидеть таблицу. Изменить это поведение можно при помощи параметра old_snapshot_threshold
(ниже).
Отметку, которая объединяет горизонты событий всех сессий базы, называют «горизонтом базы». Это старейшее значение по всем горизонтам сессий базы, исключая сессии, которые выполняют VACUUM и декодирование потока логической репликации.
Ниже приведен пример просмотра горизонтов баз, функция age(xid)
возвращает «возраст» транзакции в единицах внутреннего счетчика – количество транзакций от заданного XID до текущего:
First_db=# SELECT pid, datname, usename, state, backend_xmin FROM pg_stat_activity WHERE backend_xmin IS NOT NULL ORDER BY datname, age(backend_xmin) DESC;
pid | datname | usename | state | backend_xmin
-------+----------+----------+--------+--------------
13283 | First_db | postgres | active | 290747
(1 row)
Первая запись по каждой из баз будет содержать ее горизонт: backend_xmin
той из подключенных к базе сессий, которая выполняет самую старую из транзакций.
Правила сохранения устаревших версий записей различаются для очистки разделяемых таблиц (таких таблиц системного каталога, как pg_database) и всех остальных (неразделяемых) таблиц. Именно:
- если
pg_class.relisshared = true
(pg_database и некоторые другие объекты каталога в табличном пространстве pg_global) - определяется общий горизонт событий сервера, старейший из горизонтов баз; - иначе (все остальные таблицы) - актуален горизонт событий по той базе, которая содержит таблицу, поскольку сессии, подключенные к другим базам, эту таблицу не увидят. Если же открытых транзакций нет - определяется последняя завершенная транзакция.
За горизонтом сервера/базы действительные данные одинаково видны в каждой из активных транзакций, а недействительные - не могут быть видны ни одной из активных транзакций.
Недействительные версии, окончание срока действия которых находится за горизонтом событий сервера/базы, дальше будем называть «полностью устаревшими». По коду сервера они называются DEAD
, в отличие от еще удерживаемых RECENTLY_DEAD
.
Только полностью устаревшие записи могут быть удалены без потери данных.