Кооперативная сборка мусора в InterBase и Firebird

KDV, ibase.ru, 11.03.2005, 18.12.2006, 11.11.2007.

Статьи для предварительного чтения:
Известно, что InterBase/Firebird – это версионный сервер. Версии записей создаются при update и delete, живут определенное время (пока нужны транзакциям), и убираются как мусор в определенные моменты. Мусорные версии записей – это те, которые уже не нужны ни одной активной транзакции.

Самый известный момент сборки мусора – это sweep, автоматический (sweep interval > 0) или ручной (gfix -sweep db.gdb).

Sweep, запущенный в автоматическом или ручном режиме, просматривает записи всех таблиц и убирает мусорные (то есть, не нужные ни одной транзакции на данный момент) версии записей.

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

Кооперативная сборка мусора срабатывает при чтении. Под чтением кроме select также подразумеваются и update и delete.

Вообще сборка мусора бывает следующих видов:
  • sweep, автоматический или ручной. Сервер просматривает абсолютно все страницы данных (таблиц) для сборки мусора. Если после сборки мусора удается "подвинуть" вперед Oldest transactionLINIK, то это делается.
  • кооперативная явная. Используется в Classic и во всех версиях до InterBase 6 SuperServer (также оставлена в Yaffil, как Classic так и SuperServer). Мусор убирается пользовательским процессом (или thread-ом) только в тех данных, которые читаются или обновляются.
  • кооперативная фоновая. Появилась в InterBase 6 SuperServer. Сборкой мусора занимается отдельный thread, которому клиентские threads сообщают о наличии мусора на конкретных страницах. Thread фонового сборщика мусора работает с низким приоритетом, поэтому часто он просто не успевает собрать мусор или "застревает" на блокировках (см. статьюLINK). Мусор также собирается только в тех данных, которые читает или обновляет конкретная транзакция.
Примечание. Сборщик мусора всегда оперирует "страницами данных". То есть, мусорные версии убираются для всех записей, которые находятся на странице, считанной сборщиком мусора.
Мусорными версии записей становятся, когда нет транзакций, которые в них нуждаются. То есть, когда идентификатор транзакции версии записи меньше номера транзакции Oldest Snapshot в Header Page. Это означает, что пока есть долгоживущие активные транзакции, версии записей не станут "мусором" и не будут убраны кооперативной сборкой мусора или запуском sweep.

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

Таким образом, существует два случая – один общий, это "нечтение" записей, где есть мусорные версии, и второй – для SuperServer, когда даже обновление записей не приводит к сборке мусора (фоновый сборщик мусора или "не успевает" или "не может").

Давайте рассмотрим примеры систем, в которых может происходить подобное накопление мусорных версий.
 

"Неактуальные данные"

Система "прокачивает" через базу данных данные определенного рода, которые агрегируются и перестают быть нужными через некоторое время. Допустим, в нашей системе в день в таблицу TEST вставляется 100 записей. Для ускорения работы запросы к этой таблице всегда пишутся следующим образом:
select ...
from TEST
where TS = CURRENT_DATE
то есть, любой выполняемый запрос оперирует только "сегодняшними" данными. Кроме того, каждая из "сегодняшних" записей обновляется путем update несколько раз (если обновления не происходит, то никаких версий не будет, соответственно не будет и "мусора").

Таким образом, мы получаем некоторое количество "мусора" (вызванного обновлениями, update), которое (100 записей) завтра абсолютно точно станет ненужно приложениям. Но раз "вчерашние" записи никто не будет читать, следовательно и их версии не будут убраны как мусор (только если не запустить sweep вручную. С системами с sweep interval > 0 здесь тоже может быть проблема, если разница Oldest snapshot – Oldest transaction никогда не достигнет sweep interval, то есть в системе нет "больших" rollback и не бывает обрыва коннектов – то есть "автоматический" sweep не срабатывает).

Собрать "вчерашние" мусорные версии в данной ситуации можно либо запуском sweep, что может быть неприемлемо из-за длительной работы sweep, либо запуском select count(*) from test where ts < current_date.

То же самое относится к удаляемым данным. Если мы удалили данные за "вчера", и сервер не прочтет эти страницы данных, на самом деле данные не удалятся, т. к. удаление – это создание новой версии записи с пометкой, что все версии этой записи должны быть удалены при сборке мусора (кооперативной или sweep).
 

Хранимые агрегаты и динамическое обновление

Самый типичный пример – обновление остатка товара на складе в таблице товаров, или пересчет суммы по заказу при добавлении-удалении-модификации товаров в заказе. Обычно это делается триггером, который производит операцию update.
CREATE TRIGGER UPD_ITEMS FOR ORDERLINE
ACTIVE AFTER INSERT
AS
BEGIN
UPDATE ITEMS I
SET I.INSTOCK=I.INSTOCK - NEW.INORDER
WHERE I.ID = NEW.ITEM_ID;
...
END
 
CREATE TRIGGER INC_ORDERSUM FOR ORDERLINE
ACTIVE AFTER INSERT
AS
DECLARE VARIABLE OS NUMERIC(15,2);
BEGIN
SELECT SUM(I.PRICE * L.INORDER) FROM ITEMS I, ORDERLINE L
WHERE I.I_ID = new.I_ID and L.O_ID = new.O_ID
INTO OS:
UPDATE ORDERS O
SET O.ORDERSUM = :OS
WHERE O.O_ID = new.O_ID;
END
То есть, при вставке, удалении или обновлении записей одной таблицы, происходит обновление конкретной записи другой таблицы. Если все эти обновления одной и той же записи происходят в одной транзакции, то сервер создает только одну версию записи для этой транзакции, независимо от числа update. Но часто выполнить, например, формирование заказа в одной транзакции невозможно – как минимум для того, чтобы сохранить текущее состояние сформированного заказа на случай сбоя сети, компьютера или программы – и поэтому можно сказать, что количество версий записи заказа будет равно количеству вставок, удалений или модификаций позиций заказа (если над записью заказа больше не проводятся никакие операции).

Допустим, среднее число позиций в заказе = 10. Тогда и версий тоже будет 10. Однако, это справедливо только для SuperServer, т. к. выше уже говорилось, что в Classic кооперативная сборка мусора работает и при update. Конечно, и для Classic кооперативная сборка мусора будет работать только в том случае, если нет активных транзакций, которым нужны эти версии.

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

Производительность

Накопление мусора, конечно, влияет на производительность. Влияние может быть либо сильным либо слабым, все зависит как от количества накопленного мусора, так и от действий приложений. Известны редкие случаи, когда приложения удерживают активными очень много транзакций и производят очень много обновлений, и из-за накапливания мусора сервер начинает работать настолько медленно, что приходится его рестартовать (см. статьюLINK). Причем, замедление происходит как от чтения большого количества версий записей, так и от попыток их собрать как мусор. Больше всего влияет на скорость сборки мусора наличие неуникальных индексов (см. статьюLINK) – но эта проблема решена в InterBase 7.1 и Firebird 2.0).
 

Бэкап

Известно, что у gbakLINK есть опция -g, которая при создании резервной копии (бэкапа) позволяет отключить сборку мусора. Под "отключением" имеется в виду следующее:

Никогда и ни при каких условиях никакой "мусор" (см. определение мусорных версий в начале статьи) не попадает в backup. Более того, странно предполагать, что он (мусор) был бы там (в бэкапе) нужен.

gbak является обычной программой, как и любые другие, которые можете написать и вы. Она подсоединяется к базе данных, стартует транзакцию snapshot и затем вычитывает все данные в БД сохраняя их в файл резервной копии. То есть, фактически gbak -b без ключа -g будет вызывать срабатывание кооперативной сборки мусора (опять же, см. начало статьи), причем для всех данных, т. к. будут прочитаны все данные всех таблиц.

При этом, поскольку gbak стартует обычную транзакцию snapshot, он не может выполнить то же самое, что и явная, принудительная сборка мусора – gfix -sweep.

Таким образом, с точки зрения производительности для большинства используемых версий InterBase/Firebird (но не для InterBase 7.1 и выше или Firebird 2.0, где решена проблема долгой сборки мусора в неуникальных индексах) сборка мусора, вызываемая запуском gbak -b является лишней, и может привести к более медленному выполнению gbak.

Например, еще на Borland Developer Conference 2003, при выходе InterBase 7.1, был проведен достаточно простой тест:
  1. создана БД, в ней одна таблица с 3-мя столбцами – id, name, fld
  2. в таблицу занесено 300 тысяч записей, в столбце fld – 3 разных значения, столбец проиндексирован
  3. выполнено удаление всех записей в таблице, commit, соединение с сервером закрыто
  4. выполнен gbak -b
Результаты изображены на рисунке:


То есть, как видим, фоновая сборка мусора в InterBase 6.0 и Firebird 1.5 не совсем фоновая, а больше похожа на кооперативную явную, как в Yaffil. То есть, она однозначно приводит к замедлению работы gbak -b без ключа -g.

Ключ -g, в свою очередь, отключает сборку мусора, не вообще, а для конкретного коннекта gbak. То есть, при чтении данных gbak-ом сервер не будет инициировать сборку мусора. Это является результатом действия давно известного параметра коннекта no_garbage_collect, который может быть использован при работе с компонентами прямого доступа (IBX, FIBPlus) или инструментами (IBExpert), позволяющими указать подобные параметры коннекта к серверу (см. статью о IBXLINK).

В то же время, чтение или обновление данных во всех других коннектах, у которых нет параметра no_garbage_collect, будет приводить к фоновой или кооперативной сборке мусора.

Версии InterBase 7.1 и выше, а также Firebird 2.0 не имеют проблемы медленной сборки мусора – у них сборка мусора в индексах выполняется гораздо быстрее (при условии, что формат БД является "родным" для конкретной версии InterBase/Firebird).

Независимо от этого, gbak -b – это выполнение резервной копии БД, а следовательно это операция, которая должна быть выполнена максимально быстро. Поэтому, более логичной сборка накопившегося мусора (если он действительно накопился из-за не совсем корректной работы приложений с транзакциями. См. статистику в IBAnaltyst) представляется выполнение gbak -b -gLINK, а затем gfix -sweepLINK.

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

Что делать?

Читать статьи "Жизненный цикл транзакций"LINK, "Транзакции"LINK, "Версионность"LINK, регулярно смотреть статистику по БД утилитой IBAnalyst, оптимизировать работу с транзакциями в приложениях. Никакого более конкретного совета, к сожалению, здесь нет и быть не может, потому что сборке мусора может помешать даже забытый на несколько часов открытый коннект в IBExpert...
 

Дополнения

В Firebird 2.0 сборщик мусора переработан, и есть 3 стратегии сборки мусора, которые можно использовать при различных особенностях конкретной системы (биллинг, склад и т. п.) – cooperative (Classic), background (SuperServer), combined (SuperServer). Подробнее см. Release Notes от Firebird 2.0.