Вокруг резервного копирования 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-файлов.

Это важный момент: проект сразу создавался не под один "счастливый путь", а под типовые эксплуатационные сценарии.

Логика делится на две части:

  1. Подготовка задания;

  2. Выполнение VDI-потока для конкретной базы.

На этапе подготовки утилита:

  • валидирует аргументы;

  • подключается к SQL Server через ODBC;

  • получает список БД, если пользователь не указал конкретную;

  • определяет, какие наборы файлов использовать при restore;

  • для diff подбирает цепочку full + diff по общему timestamp.

Это уже дает важный практический элемент: учтена не только запись backup, но и логика последующего восстановления, а значит проект ориентирован именно на полный цикл РК/ВД.

Backup через VDI

Сценарий выглядит так:

  1. Приложение инициализирует COM через CoInitializeEx.

  2. Создает объект IClientVirtualDeviceSet2 через CoCreateInstance.

  3. Генерирует GUID-базу имен виртуальных устройств.

  4. Формирует VDConfig: число устройств, размер блока, размер передачи, таймаут, глубину I/O.

  5. Вызывает CreateEx, публикуя набор виртуальных устройств для SQL Server.

  6. В отдельном потоке запускает T-SQL команду BACKUP DATABASE или BACKUP LOG ... TO VIRTUAL_DEVICE=....

  7. После получения конфигурации от 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 цикла.