21.10.25

Сборка мусора в FILESTREAM

Автор: Paul Randal, FILESTREAM garbage collection

В предыдущих статьях о FILESTREAM я разбирал структуру каталогов контейнера данных FILESTREAM и то, как сопоставлять каталоги с таблицами и столбцами базы данных. В этой статье я объясню, как и когда работает процесс сборки мусора FILESTREAM, поскольку, похоже, нигде это не задокументировано (даже в статье FILESTREAM Storage in SQL Server 2008, которую я писал для Microsoft — предполагаемая глубина не включала такой низкий уровень деталей). Похоже, существует немало путаницы вокруг того, как выполняются обновления данных FILESTREAM и когда удаляются старые версии файлов FILESTREAM. Я объясню, как всё устроено, а затем покажу на примере.

Основное, что кажется неинтуитивным: частичных обновлений данных FILESTREAM не существует. Если у вас 10 МБ данных, хранящихся в столбце FILESTREAM (и, следовательно, есть 10‑мегабайтный файл FILESTREAM), то обновление даже одного байта приведёт к созданию нового 10‑мегабайтного файла FILESTREAM. Любая технология, которая зависит от актуального состояния базы (например, резервные копии журнала, пересылка журнала, репликация), подхватит весь новый 10‑мегабайтный файл FILESTREAM. Каждый раз, когда данные обновляются, создаётся новый 10‑мегабайтный файл FILESTREAM, который затем попадает в резервные копии, репликацию и т. п. Это может привести к неожиданно большим резервным копиям журнала или возросшему сетевому трафику между узлами репликации.

Осознав, что будут создаваться новые версии файлов FILESTREAM, возникает очевидный вопрос: когда удаляются старые версии? Ответ: это зависит от обстоятельств.

Старые версии удаляются процессом, называемым сборкой мусора (garbage collection) — примерно так же, как работает сборка мусора в памяти для управляемого кода и освобождает экземпляры объектов, на которые больше не ссылаются переменные. Ключевой момент: экземпляр объекта больше никому не нужен; иначе сборка мусора в памяти повредила бы рабочее состояние приложения на управляемом коде. Тот же принцип применим к сборке мусора FILESTREAM — старые версии файлов FILESTREAM нельзя удалять, пока они больше не нужны.

Но что означает «больше не нужны» применительно к файлам FILESTREAM? В некотором смысле это похоже на журнальные записи транзакций. Старая версия файла FILESTREAM больше не нужна, если транзакция, которая её создала, была зафиксирована или откатилась, и при этом нет других задач, которым необходимо её прочитать, например резервной копии журнала (при работе в режимах восстановления FULL или BULK_LOGGED) или логридера репликации. Фактически VLF (виртуальный файл журнала) журнала транзакций, содержащий запись о создании файла данных FILESTREAM, должен перейти в неактивное состояние, прежде чем файл FILESTREAM может быть собран (удалён) сборщиком мусора. Обратите внимание, что я не упоминаю зеркалирование базы данных — в SQL Server 2008 зеркалирование базы данных и FILESTREAM несовместимы.

Как только старый файл FILESTREAM больше не нужен, он становится кандидатом для сборки мусора. Как же процесс сборки мусора узнаёт, какие файлы FILESTREAM физически удалять? Дело в том, что когда файл становится не нужен, создаётся запись в специальной таблице, называемой «надгробной» (tombstone). Процесс сборки мусора сканирует такие таблицы и удаляет только те файлы FILESTREAM, для которых есть запись в tombstone‑таблице. Подробнее о tombstone‑таблицах можно прочитать в этой статье из блога CSS: How It Works: File Stream the Before and After Image of a File.

Когда же фактически запускается процесс сборки мусора? Он не может быть частью резервных копий журнала, поскольку в режиме восстановления SIMPLE резервные копии журнала недоступны. Ответ: он запускается как часть процесса контрольной точки базы данных. Это и рождает путаницу — старый файл FILESTREAM не будет удалён до тех пор, пока он больше не нужен И не выполнится контрольная точка.

Теперь посмотрим на всё это в действии. Я создам базу данных с данными FILESTREAM, а затем поэкспериментирую с транзакциями, резервными копиями журнала и контрольными очками, чтобы показать работу сборки мусора.

CREATE DATABASE [FileStreamTestDB] ON PRIMARY
    (NAME = [FileStreamTestDB_data],
    FILENAME = N'C:\Metro Demos\FileStreamTestDB\FSTestDB_data.mdf'),
FILEGROUP [FileStreamFileGroup] CONTAINS FILESTREAM
    (NAME = [FileStreamTestDBDocuments],
    FILENAME = N'C:\Metro Demos\FileStreamTestDB\Documents')
LOG ON
    (NAME = [FileStreamTestDB_log],
    FILENAME = N'C:\Metro Demos\FileStreamTestDB\FSTestDB_log.ldf');
GO
USE [FileStreamTestDB];
GO
CREATE TABLE [FileStreamTest1] (
    [DocId] UNIQUEIDENTIFIER ROWGUIDCOL NOT NULL UNIQUE,
    [DocName] VARCHAR (25),
    [Document] VARBINARY(MAX) FILESTREAM);
GO

Теперь переведу базу в режим восстановления FULL и сделаю полную резервную копию — это означает, что далее мне нужно выполнять резервные копии журнала, чтобы управлять размером журнала транзакций. А также означает, что файл FILESTREAM нельзя удалить, пока он не попадёт в резервную копию.

ALTER DATABASE [FileStreamTestDB] SET RECOVERY FULL;
GO
BACKUP DATABASE [FileStreamTestDB] TO DISK = N'C:\SQLskills\FSTDB.bak';
GO

Теперь создам данные FILESTREAM.

INSERT INTO [FileStreamTest1] VALUES (
    NEWID (),
    'Paul Randal',
    CAST ('SQLskills.com' AS VARBINARY(MAX)));
GO

Заглянув в созданный контейнер данных FILESTREAM, я вижу там следующий файл.

Напомню из предыдущих статей: имена файлов FILESTREAM соответствуют номеру LSN журнала базы на момент их создания. Теперь обновлю значение в неявной транзакции (без BEGIN TRAN и COMMIT TRAN).

UPDATE [FileStreamTest1]
    SET [Document] = CAST (REPLICATE ('Paul', 2000) AS VARBINARY(MAX))
WHERE [DocName] LIKE '%Randal%';
GO

И теперь у нас такие файлы.

Новый файл — это файл на 8 КБ, а старое значение FILESTREAM — файл на 1 КБ. Если выполнить явную команду CHECKPOINT, ничего не изменится, так как старый файл всё ещё нужен — он ещё не попал в резервную копию. Выполню резервную копию журнала.

BACKUP LOG [FileStreamTestDB] TO DISK = 'C:\SQLskills\FSTB_log.bak';
GO

И все файлы всё ещё на месте. Хотя самый первый файл 1 КБ уже не нужен, контрольной точки ещё не было, поэтому сборка мусора не запускалась. Теперь запущу явную CHECKPOINT — и каталог по‑прежнему содержит оба файла. Что произошло? VLF журнала транзакций, содержащий запись о создании файла FILESTREAM, всё ещё активен, значит, файл всё ещё нужен. Мне нужно сделать ещё одну резервную копию журнала и контрольную точку, прежде чем сработает сборка мусора (это приведёт к циклическому переходу журнала, при условии, что в базе ничего не происходит и нет активных транзакций), и тогда содержимое каталога изменится следующим образом:

Альтернатива заключалась бы в том, чтобы сгенерировать больше журнальных записей, чтобы действо перешло в следующий VLF журнала, затем выполнить ещё одну резервную копию журнала, которая пометит VLF «создания» как неактивный, и после следующей контрольной точки сборка мусора удалит файл. Разумеется, это и будет обычным ходом событий в промышленной базе.

Итак, не удивляйтесь, если вы обновили файл FILESTREAM, затем сделали резервную копию журнала и контрольную точку — и ничего не произошло. Помните: журнал транзакций должен продвинуться настолько, чтобы VLF «создания» тоже стал неактивным. Вы можете убедиться в этом сами, создав явную транзакцию одновременно с обновлением FILESTREAM (в другой, неявной транзакции). Сколько бы раз вы ни выполняли резервную копию журнала и ни запускали контрольную точку, сборка мусора не начнётся, пока явная транзакция не будет зафиксирована или откачена, а затем не будет выполнены ещё одна резервная копия журнала и контрольная точка.

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



Комментариев нет:

Отправить комментарий