Вокруг резервного копирования Microsoft SQL Server обычно обсуждают либо штатные BACKUP DATABASE ... TO DISK, либо интеграцию с большими корпоративными системами защиты данных. Между этими двумя мирами есть важный слой: VDI (Virtual Device Interface). Именно через него внешнее приложение может встроиться в процесс резервного копирования и восстановления так, чтобы SQL Server писал не в обычный .bak по своему усмотрению, а в управляемый приложением поток данных.
В этой статье разберем небольшой, но вполне рабочий проект на C++, который реализует РК и ВД для MS SQL Server через VDI в ПО «Береста».
Утилита поддерживает:
полный, дифференциальный и логический backup;
restore одной базы или всех найденных;
striped backup/restore в несколько потоков;
Windows-аутентификацию и SQL-аутентификацию;
работу с SQL Server 2008-2022.
Почему VDI?
Если задача ограничивается локальным резервным копированием на диск, VDI не нужен: достаточно стандартных T-SQL команд. Но как только появляется внешняя система резервного копирования, картина меняется.
СРК обычно хочет сама управлять:
жизненным циклом задания;
маршрутом потока данных;
параллелизмом;
политиками хранения;
журналированием и обработкой ошибок.
И здесь VDI становится мостом между SQL Server и внешним приложением. SQL Server продолжает выполнять привычные BACKUP и RESTORE, но вместо физического файла работает с виртуальными устройствами. А уже клиент VDI читает или записывает данные туда, куда считает нужным: в локальные файлы, сетевое хранилище, object storage, дедуп-слой или собственный медиасервер.
В рассматриваемом проекте в качестве целевого носителя используются обычные файлы .bak, но архитектурно это именно VDI-клиент.
Утилита
В проекте есть простой CLI-интерфейс:
mssql_vdi backup|restore [опции]
Параметры описаны в Config. Из коробки поддерживаются:
режимы backup и restore;
типы full, diff, log;
число полос -- stripes;
глубина буферизации -- buffers;
размеры -- maxtransfer и -- blocksize;
компрессия SQL Server;
выбор одной базы или списка баз;
восстановление из каталога с наборами backup-файлов.
Это важный момент: проект сразу создавался не под один "счастливый путь", а под типовые эксплуатационные сценарии.
Логика делится на две части:
Подготовка задания;
Выполнение VDI-потока для конкретной базы.
На этапе подготовки утилита:
валидирует аргументы;
подключается к SQL Server через ODBC;
получает список БД, если пользователь не указал конкретную;
определяет, какие наборы файлов использовать при restore;
для diff подбирает цепочку full + diff по общему timestamp.
Это уже дает важный практический элемент: учтена не только запись backup, но и логика последующего восстановления, а значит проект ориентирован именно на полный цикл РК/ВД.
Backup через VDI
Сценарий выглядит так:
Приложение инициализирует COM через CoInitializeEx.
Создает объект IClientVirtualDeviceSet2 через CoCreateInstance.
Генерирует GUID-базу имен виртуальных устройств.
Формирует VDConfig: число устройств, размер блока, размер передачи, таймаут, глубину I/O.
Вызывает CreateEx, публикуя набор виртуальных устройств для SQL Server.
В отдельном потоке запускает T-SQL команду BACKUP DATABASE или BACKUP LOG ... TO VIRTUAL_DEVICE=....
После получения конфигурации от SQL Server поднимает рабочие потоки передачи данных, по одному на каждый stripe.
Это и есть ключевая идея VDI: SQL Server и внешнее приложение работают синхронно, но не в одном потоке исполнения. SQL Server отдает команды виртуальному устройству, а клиент подтверждает их выполнением через CompleteCommand.
Внутри transferThreadProc происходит наиболее важная часть:
открывается конкретное виртуальное устройство через OpenDevice;
открывается целевой файл;
в цикле вызывается GetCommand;
команды VDC_Write записываются в файл при backup;
команды VDC_Read читаются из файла при restore;
VDC_Flush завершает буферизацию;
после каждой операции вызывается CompleteCommand.
Если смотреть на это архитектурно, то здесь SQL Server воспринимается как producer/consumer блочных данных, а приложение выступает транспортным адаптером между SQL-движком и носителем.
Striped backup в коде
Для реальной СРК поддержка одного потока быстро становится узким местом. Именно поэтому в проекте есть параметр --stripes, а в логике backup/restore создается несколько виртуальных устройств и несколько файлов:
db_full_20260412_101530_s0.bak
db_full_20260412_101530_s1.bak
db_full_20260412_101530_s2.bak
...
Такой подход дает сразу несколько преимуществ:
можно распараллелить запись и чтение;
проще масштабировать;
ближе модель интеграции с промышленными СРК, где потоков почти всегда больше одного;
легче переносить транспорт с локального диска на внешний storage backend.
Для каждого stripe создается отдельный поток _beginthreadex, которому передается свой deviceName и свой filePath. Это достаточно простой, но правильный способ показать параллельный VDI pipeline без лишнего усложнения.
Дополнительно в конфиге вынесены параметры, которые реально влияют на производительность:
blockSize;
maxTransferSize;
buffersPerStripe;
vdiTimeoutSec.
То есть проект уже позволяет не только сделать backup, но и экспериментировать с профилем I/O.
Формирование команд SQL Server
Подключение выполняется по ODBC. Что особенно полезно, код не зашит в один-единственный драйвер, а пробует несколько вариантов:
ODBC Driver 18 for SQL Server;
ODBC Driver 17 for SQL Server;
старый SQL Server.
Для проекта это хорошее решение: оно снижает число ложных отказов на стендах с разным окружением.
Сама SQL-команда собирается программно.
Для backup:
BACKUP DATABASE [db] TO VIRTUAL_DEVICE='...';
или BACKUP LOG [db] TO VIRTUAL_DEVICE='...'.
Дополнительно могут добавляться:
COMPRESSION;
DIFFERENTIAL;
NO_TRUNCATE для аварийного log backup;
BLOCKSIZE=....
Для restore используется:
RESTORE DATABASE [db] FROM VIRTUAL_DEVICE='...' WITH REPLACE;
далее RECOVERY или NORECOVERY.
Отдельно отметим правильную деталь: перед restore для обычной базы выполняется
ALTER DATABASE [db] SET SINGLE_USER WITH ROLLBACK IMMEDIATE
Это нужно, чтобы активные подключения не мешали восстановлению. После успешного RECOVERY база возвращается в MULTI_USER.
Учитываем differential и log backup
Проект не ограничивается full backup.
для differential backup ищется последний full для той же базы;
timestamp включается в имя файла и связывает цепочку full + diff;
при restore differential сначала выполняется full WITH NORECOVERY, потом diff WITH RECOVERY;
log backup разрешается только для баз в моделях восстановления FULL или BULK_LOGGED;
master пропускается для differential backup, потому что SQL Server это не поддерживает;
master можно пропускать при restore, поскольку для него нужны отдельные условия запуска SQL Server в single-user mode.
Это хороший пример того, как даже простая утилита должна учитывать поведение самого SQL Server, а не только собственную логику передачи данных.
Как устроен поиск наборов для восстановления
Для restore одного факта наличия файлов недостаточно. Нужно понять, какие файлы относятся к одному набору.
Именно поэтому в проекте используется соглашение об именовании:
{db}_{type}_{timestamp}_s{n}.bak
Например:
Sales_full_20260412_101530_s0.bak
Sales_diff_20260412_101530_s0.bak
Функции сканируют каталог и строят наборы восстановления:
для full restore выбирается последний набор по timestamp;
для diff restore берутся только такие наборы, у которых одновременно существуют и full, и diff с одинаковой меткой времени;
число stripe определяется по максимальному найденному индексу sN.
Это очень практичный выбор. Нет отдельной метабазы, нет внешнего каталога backup-сессий, но уже есть воспроизводимая логика, достаточная для автоматического restore.
Итог
Проект наглядно демонстрирует, как может выглядеть интеграция MS SQL Server с внешней системой резервного копирования через VDI.
С одной стороны, утилита делает понятные вещи и делает их достаточно прямолинейно. С другой стороны, в ней уже есть те элементы, без которых сложно обсуждать реальное РК и ВД:
параллельные потоки;
full/diff/log сценарии;
цепочки восстановления;
управление состоянием базы при restore;
работа через виртуальные устройства, а не через обычные файлы SQL Server.
Этот проект закрывает самый важный вопрос: как построить поток между SQL Server и внешним приложением через VDI и довести его до работающего backup/restore цикла.
