Вкратце опишу как организовал клонирование БД (создание нескольких экземпляров БД из одного бэкапа) на текущем проекте. Способ позволяет сэкономить время и место на жестком диске.
Ситуация: есть толстая БД (скажем, сотня Гб). Хочется иметь эту базу со всеми данными отдельно для каждого разработчика и не тратить на это терабайтный диск. Далее приведено решение для MSSQL под windows с использованием powershell.
Наткнулся на утилиту SQL Clone от Redgate. На сайте есть описание как она работает. Самая суть в использовании такой штуки: differencing virtual hard disk. На русский это переводится как «разностный виртуальный диск» — диск на котором хранится только разница относительно «родительского» диска.
Подробности под катом
Схема работы выглядит следующим образом:
Создание родительского диска описано не будет, т.к. это можно сделать вручную через графический интерфейс disk management. Ну или загуглить команды и дополнить скрипты, приведенные в статье.
Примечание раз:
В windows 10 и windows server 2016 есть powershell commandlet New-VHD. Для тех же, кто использует предыдущие версии сервера есть утилита diskpart. Автоматизировать работу с ней не совсем удобно, т.к. на вход она принимает файл с командами для выполнения.
Примечание два:
Т.к. файлы БД размещаются на differencing disk, то производительность такого решения далеко не на высоте. Получается несколько уровней косвенности: запись идет в базу, которая лежит на виртуальном диске, который хранит разницу, в доме который построил Джек. Конкретных цифр по производительности у меня нет (т.к. на нашем тестовом контуре это далеко не первый вопрос в любом случае). Буду признателен, если кто-то замерит на сколько просаживается скорость записи/чтения.
Примечание три:
Т.к. скрипты не предполагались для широкого использования и приводятся исключительно для примера, наблюдается повышенная криворукость и жесткая привязка к MSSQL.
Т.к. запускается скрипт на машине разработчика, а все действия выполняются на машине с sql сервером, предполагается, что настроен powershell remoting. Все команды выполняются в открытой сессии.
Get-StoredCredential — это commandlet для сохранения credential на локальной машине (устанавливается отдельно). В принципе можно обойтись и без него, поэтому он и завернут в try/catch.
Вот этот кусок «ls function:[d-z]: -n» — это просто какая-то магия для получения списка букв дисков. Как работает — без понятия, скопировал со stackoverflow.
В приведенном коде самая большая трудность — получить полученный виртуальный диск и посадить его на конкретную букву. Еще предварительно ему надо сделать «online».
Ахтунг раз:
Если ребутнуть сервер, то замучаешься объяснять sql серверу, что этих баз нет, и надо их заново подключить.
Ахтунг два:
Автор конечно проверил команды на своем тестовом контуре, но гарантировать ничего (тем более их работоспособность) не намерен. At yout own risk.
Итого:
Запуск дополнительной тестовой БД занимает пару минут и 40Мб на диске. Таким образом гораздо удобнее организовать каждому разработчику свой экземпляр БД.
Дополнительно:
Тот же самый скрипт можно использовать для поднятия БД под интеграционные тесты.
Надеюсь, окажется кому-то полезным.
Ситуация: есть толстая БД (скажем, сотня Гб). Хочется иметь эту базу со всеми данными отдельно для каждого разработчика и не тратить на это терабайтный диск. Далее приведено решение для MSSQL под windows с использованием powershell.
Наткнулся на утилиту SQL Clone от Redgate. На сайте есть описание как она работает. Самая суть в использовании такой штуки: differencing virtual hard disk. На русский это переводится как «разностный виртуальный диск» — диск на котором хранится только разница относительно «родительского» диска.
Подробности под катом
Схема работы выглядит следующим образом:
- Создаем и подключам обычный виртуальный диск (он потом станет родительским).
- Создаем один экземпляр БД, от которого будут делаться клоны. Вычищаем продовские данные, подготавливаем базу полностью к работе в тестовой среде. Файлы БД размещаем на виртуальном диске.
- Отключаем БД от сервера. Отключаем виртуальный диск.
- Создаем differencing disk. Подключаем к системе. Подключаем БД с этого диска к sql серверу.
- Повторяем пункт 4 до достижения гармоничного числа БД.
Создание родительского диска описано не будет, т.к. это можно сделать вручную через графический интерфейс disk management. Ну или загуглить команды и дополнить скрипты, приведенные в статье.
Примечание раз:
В windows 10 и windows server 2016 есть powershell commandlet New-VHD. Для тех же, кто использует предыдущие версии сервера есть утилита diskpart. Автоматизировать работу с ней не совсем удобно, т.к. на вход она принимает файл с командами для выполнения.
Примечание два:
Т.к. файлы БД размещаются на differencing disk, то производительность такого решения далеко не на высоте. Получается несколько уровней косвенности: запись идет в базу, которая лежит на виртуальном диске, который хранит разницу, в доме который построил Джек. Конкретных цифр по производительности у меня нет (т.к. на нашем тестовом контуре это далеко не первый вопрос в любом случае). Буду признателен, если кто-то замерит на сколько просаживается скорость записи/чтения.
Примечание три:
Т.к. скрипты не предполагались для широкого использования и приводятся исключительно для примера, наблюдается повышенная криворукость и жесткая привязка к MSSQL.
Проинициализируем кое-какие переменные:
$server = "server";
$db_file_name = "db_file_name";
$root_path = "path to folder with disks";
$cred = try { Get-StoredCredential -Target "$server\Administrator"; } catch { Get-Credential -Message "server windows user" -UserName "$server\Administrator" }
$db_cred = $(try { Get-StoredCredential -Target "$server\sa"; } catch { Get-Credential -Message "sql server user" -UserName "sa" }).GetNetworkCredential();
$session = New-PSSession -ComputerName $server -Credential $cred;
Т.к. запускается скрипт на машине разработчика, а все действия выполняются на машине с sql сервером, предполагается, что настроен powershell remoting. Все команды выполняются в открытой сессии.
Get-StoredCredential — это commandlet для сохранения credential на локальной машине (устанавливается отдельно). В принципе можно обойтись и без него, поэтому он и завернут в try/catch.
Далее код выполнения скрипта diskpart:
function run_script([string]$script, [bool]$suppress_output = $false)
{
$result = Invoke-Command -Session $session -ArgumentList $script -ScriptBlock {
param($script)
$script.Split("`r`n") | % { Write-Host $_.Trim() };
Out-File -FilePath "tmp" -InputObject $script -Encoding ascii
return diskpart /s "tmp"
}
if($suppress_output)
{
return $result;
}
else
{
$result | ? { !$_.Contains("Microsoft") -and $_ -ne "" } | Write-Host
}
}
Sql команды я выполняю через SQLCMD:
function run_sql([string]$sql)
{
Write-Host $sql
SQLCMD -S $server -d master -U $($db_cred.UserName) -P $($db_cred.Password) -Q $sql
}
Создание differencing disk:
run_script "create vdisk file=`"$root_path\$name.vhdx`" parent=`"$root_path\parent_disk.vhdx`""
Далее подключение диска и БД:
$disk_letter = Invoke-Command -Session $session -ScriptBlock {
ls function:[d-z]: -n | ?{ !(test-path $_) } | select -Last 1;
}
$volumes = run_script "list volume" $true
$disks = run_script "list disk" $true
$script = "
sel vdisk file=`"$current_path\$db_name.vhdx`"
attach vdisk";
run_script $script;
$disks_after = run_script "list disk" $true
$new_disk = $($disks_after | ? { $_ -notin $disks } )
Write-Host $new_disk
$new_disk -match "\d+"
$diskId = $Matches[0]
$script = "
select disk $diskId
online disk";
run_script $script
$volumes_after = run_script "list volume" $true
# get added disk
$new_volume = $($volumes_after | ? { $_ -notin $volumes } )
Write-Host $new_volume
$new_volume -match "\d+"
$volumeId = $Matches[0]
$script = "
select volume $volumeId
assign letter=$disk_letter";
run_script $script
run_script "list volume";
run_script "list vdisk";
$atach_script = "CREATE DATABASE $db_name ON (FILENAME = '$disk_letter\$db_file_name.mdf'),(FILENAME = '$disk_letter\$db_file_name.ldf') FOR ATTACH";
run_sql "$atach_script"
Вот этот кусок «ls function:[d-z]: -n» — это просто какая-то магия для получения списка букв дисков. Как работает — без понятия, скопировал со stackoverflow.
В приведенном коде самая большая трудность — получить полученный виртуальный диск и посадить его на конкретную букву. Еще предварительно ему надо сделать «online».
Отключение диска несколько проще:
run_sql "
ALTER DATABASE $name
SET OFFLINE WITH ROLLBACK IMMEDIATE
GO
sp_detach_db $name";
$script = "select vdisk file=`"$root_path\$name.vhdx`"
detach vdisk ";
run_script $script
Собираем все в кучу:
param(
[ValidateSet("detach_all", "attach_all_available", "create_new", "attach_db", "detach_db", "remove_file")][Parameter(mandatory=$true)][string] $mode,
[string] $name
)
function run_sql([string]$sql)
{
Write-Host $sql
SQLCMD -S $server -d master -U $($db_cred.UserName) -P $($db_cred.Password) -Q $sql
}
function run_script([string]$script, [bool]$suppress_output = $false)
{
$result = Invoke-Command -Session $session -ArgumentList $script -ScriptBlock {
param($script)
$script.Split("`r`n") | % { Write-Host $_.Trim() };
Out-File -FilePath "tmp" -InputObject $script -Encoding ascii
return diskpart /s "tmp"
}
if($suppress_output)
{
return $result;
}
else
{
$result | ? { !$_.Contains("Microsoft") -and $_ -ne "" } | Write-Host
}
}
function attach_disk([string]$db_name, [string]$current_path)
{
$disk_letter = Invoke-Command -Session $session -ScriptBlock {
ls function:[d-z]: -n | ?{ !(test-path $_) } | select -Last 1;
}
$volumes = run_script "list volume" $true
$disks = run_script "list disk" $true
$script = "
sel vdisk file=`"$current_path\$db_name.vhdx`"
attach vdisk";
run_script $script;
$disks_after = run_script "list disk" $true
$new_disk = $($disks_after | ? { $_ -notin $disks } )
Write-Host $new_disk
$new_disk -match "\d+"
$diskId = $Matches[0]
$script = "
select disk $diskId
online disk";
run_script $script
$volumes_after = run_script "list volume" $true
# get added disk
$new_volume = $($volumes_after | ? { $_ -notin $volumes } )
Write-Host $new_volume
$new_volume -match "\d+"
$volumeId = $Matches[0]
$script = "
select volume $volumeId
assign letter=$disk_letter";
run_script $script
run_script "list volume";
run_script "list vdisk";
$atach_script = "CREATE DATABASE $db_name ON (FILENAME = '$disk_letter\$db_file_name.mdf'),(FILENAME = '$disk_letter\$db_file_name.ldf') FOR ATTACH";
run_sql "$atach_script"
}
$server = "server";
$db_file_name = "db_file_name";
$cred = try { Get-StoredCredential -Target "$server\Administrator"; } catch { Get-Credential -Message "server windows user" -UserName "$server\Administrator" }
$db_cred = $(try { Get-StoredCredential -Target "$server\sa"; } catch { Get-Credential -Message "sql server user" -UserName "sa" }).GetNetworkCredential();
$session = New-PSSession -ComputerName $server -Credential $cred;
$root_path = "path to folder with disks";
$files = Invoke-Command -Session $session -ArgumentList $root_path -ScriptBlock {
param($root_path)
Get-ChildItem -Filter "*.vhdx" -Path $root_path
}
switch ($mode) {
"detach_all" {
$files `
| % { Write-Host $("*"*40) `r`n $_.FullName `r`n; $_ } `
| % { "
ALTER DATABASE $($_.Name.Replace('.vhdx', ''))
SET OFFLINE WITH ROLLBACK IMMEDIATE
GO
sp_detach_db $($_.Name.Replace('.vhdx', ''))" } `
| % { run_sql "$_" }
$files `
| % { Write-Host $("*"*40) `r`n $_.FullName `r`n; $_ } `
| % { run_script "select vdisk file=`"$($_.FullName)`"
detach vdisk " }
break;
}
"attach_all_available" {
$files | % { $_.Name.Replace('.vhdx', '') } | ? { $_ -ne "parent_disk" } | % { attach_disk $_ $root_path }
break;
}
"attach_db" {
attach_disk $name $root_path
break;
}
"detach_db" {
run_sql "
ALTER DATABASE $name
SET OFFLINE WITH ROLLBACK IMMEDIATE
GO
sp_detach_db $name";
$script = "select vdisk file=`"$root_path\$name.vhdx`"
detach vdisk ";
run_script $script
break;
}
"create_new" {
$script = "create vdisk file=`"$root_path\$name.vhdx`" parent=`"$root_path\parent_disk.vhdx`""
run_script $script
attach_disk $name $root_path;
break;
}
"remove_file" {
Invoke-Command -Session $session -ArgumentList $name,$root_path -ScriptBlock {
param($name, $root_path)
Remove-Item -Path "$root_path\$name.vhdx"
}
}
}
Remove-PSSession $session
Ахтунг раз:
Если ребутнуть сервер, то замучаешься объяснять sql серверу, что этих баз нет, и надо их заново подключить.
Ахтунг два:
Автор конечно проверил команды на своем тестовом контуре, но гарантировать ничего (тем более их работоспособность) не намерен. At yout own risk.
Итого:
Запуск дополнительной тестовой БД занимает пару минут и 40Мб на диске. Таким образом гораздо удобнее организовать каждому разработчику свой экземпляр БД.
Дополнительно:
Тот же самый скрипт можно использовать для поднятия БД под интеграционные тесты.
Надеюсь, окажется кому-то полезным.