В сети можно найти немало информации по организации резервного копирования информационных баз 1С. Не могу и я пройти мимо этой темы. Но немного усложним задачу, добавив к основному процессу создания бэкапа дополнительный функционал: организуем базу вчерашнего дня на другом SQL-сервере (отличном от главного). Т.е на резервном серевере у нас будет полная копия рабочей базы по состоянию на вчерашний день. Такой подход обеспечивает ряд преимуществ:
При этом не требуется выполнять длительного и рутинного восстановления данных из бэкапа - все сделает регламентная процедура резервного копирования.
Итак, действия которые нам необходимо выполнить:
Для организации всего я использую хранимую процедуру, а регламентный запуск ее по расписанию - job агента сервера MSSQL.
Для начала надо прилинковать главный сервер к резервному. Делается это в SQL Managment Studio (Объекты сервера - связанные серверы). Добавляем главный сервер. В нашем примере его имя v8-sql. Это необходимо, чтобы выполнять скрипты запущенные на резервном сервере в адресном пространстве главного сервера. При этом нужно не забыть сконфигурировать прилинкованный сервер ( включить Ole Automation Procedures, rpc, дать разрешение на использование xp_cmdshell). При запуске процедуры вы увидите ошибки, если что-то не так сконфигурировано.
Создаем хранимую процедуру на резервном сервере. Создать ее можно в любой базе, кроме той, куда будет восстанавливаться рабочий бэкап.
Итоговый текст со всеми объявлениями переменной основной процедуры будет приведен в конце статьи. Будем рассматривать по частям.
Определим имя файла с последним бэкапом на главном сервере. На главном сервере обычно настроен автоматический бэкап основной базы с помощью задания агента SQL-сервера (job). Если свежий бэкап присутствует - необязательно снова его создавать. Имя физического файла получим из двух таблиц backupmediafamily и media_set_id в системной базе msdb.
set @last_backup_name = ( select bmf.physical_device_name from ( select top 1 bs.media_set_id from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_start_date desc ) LastBackUp left join [v8-sql].msdb.dbo.backupmediafamily bmf on bmf.media_set_id = LastBackUp.media_set_id )
И проверим существование бэкапа на диске (после выполнения этой процедуры переменная file_exists будет содержать либо 0 либо 1):
exec [v8-sql].master.dbo.xp_fileexist @last_backup_name , @file_exists OUTPUT
Здесь [v8-sql]-это имя прилинкованного главного сервера. В случае, если имя сервера не содержит тире, можно не пользоваться квадратными скобками. Из этой же базы получаем время создания последнего бэкапа:
set @last_backup_date = ISNULL(( select top 1 bs.backup_finish_date from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_finish_date desc ),cast('' as datetime))
Теперь проверим дату и существование файла бэкапа. Если его нет, либо он слишком старый - запустим процедуру бэкапа на главном сервере. Будем пытаться его пересоздать, запустив задание агента. Это дает ряд преимуществ, например имя файла содержит дату.
IF (DATEDIFF(HOUR,@last_backup_date,GETDATE()) > 24) or (@file_exists <> 1) BEGIN -- найдем job с бэкапом по заданному в переменной @PatternOfJobName фрагменту имени set @Job_command = ISNULL(( select sjs.Command from [v8-sql].msdb.dbo.sysjobsteps as sjs inner join [v8-sql].msdb.dbo.sysjobs as sj on sjs.job_id = sj.job_id and sj.name like @PatternOfJobName where sjs.step_id = 1 ),'RAISEERROR (''Нету job-а '''+@PatternOfJobName+',16,1)') select @Job_command -- запустим job exec [v8-sql].master.dbo.sp_executesql @Job_command -- считаем еще раз имя файла с последним бэкапом set @last_backup_name = ( select bmf.physical_device_name from ( select top 1 bs.media_set_id from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_start_date desc ) LastBackUp left join [v8-sql].msdb.dbo.backupmediafamily bmf on bmf.media_set_id = LastBackUp.media_set_id ) -- Проверим еще раз, не появился ли файл бэкапа exec [v8-sql].master.dbo.xp_fileexist @last_backup_name , @file_exists OUTPUT -- не помогло, тогда выпьем яду IF (DATEDIFF(HOUR,@last_backup_date,GETDATE()) > 24) or (@file_exists <> 1) BEGIN RAISERROR ('Не удалось выполнить бэкап базы', 16, 1) END END
Теперь в переменной last_backup_name у нас имя файла последнего бэкапа. Запускаем копирование файла с рабочего сервера на резервный. Здесь следует иметь ввиду, что при копировании файлов более нескольких десятков гигабайт стандартные команды системы (copy) срабатывают некорректно. Потому я использовал не совсем стандартную программу для многопоточного копирования robocopy. Кроме того, перед копированием на резервном сервере надо вычистить папку для приема свежего бэкапа:
-- очистим папку на резервном. сервере для приема свежего бэкапа по шаблону set @Cmd_String = 'del '+@destpath+@SourceDatabaseName+'*.* /Q' exec master.dbo.xp_cmdshell @Cmd_String -- имя текущего(резервного) сервера set @current_server_name = (select top 1 srvname from master.dbo.sysservers where isremote=0) -- имя тек. сервера -- скопируем файл на текущий сервер через админскую $шару --определим имя исходного файла, потому как у robocopy.exe необычный синтаксис set @SourceFileName = SUBSTRING(@last_backup_name,Charindex(@SourceDatabaseName,@last_backup_name),LEN(@last_backup_name)) set @Cmd_String = 'c:windowsrobocopy.exe ' + REPLACE(@last_backup_name,@SourceFileName,' ') + ' '+@current_server_name+''+ REPLACE(@destpath,':','$') + ' ' + @SourceFileName -- заменяем : в пути на админскую шару $ exec [v8-sql].master.dbo.xp_cmdshell @Cmd_String -- Переименуем файл, потому что в robocopy нельзя скопировать файл с новым именем set @Cmd_String = 'REN ' + @destpath + @SourceFileName + ' ' + @DestFileName exec master.dbo.xp_cmdshell @Cmd_String
Файл скопирован. Приступаем к восстановлению бэкапа. Для этого нам надо знать логическое имя резервной базы и имена файлов лога и данных. Достанем его из таблицы sysaltfiles базы master.
Имена файлов:
set @DB_file_name = (select top 1 sf.filename from master.dbo.sysdatabases sd left join master..sysaltfiles sf on sf.dbid = sd.dbid where sf.fileid = 1 and sd.name = @DestDatabaseName) set @Log_file_name = (select top 1 sf.filename from master.dbo.sysdatabases sd left join master..sysaltfiles sf on sf.dbid = sd.dbid where sf.fileid = 2 and sd.name = @DestDatabaseName)
Логические имена:
set @source_ID = (select dbid from [v8-sql].master.dbo.sysdatabases where name = @SourceDatabaseName) set @DB_logical_name = (select top 1 name from [v8-sql].master.dbo.sysaltfiles where dbid = @Source_ID and fileid = 1) set @Log_logical_name = (select top 1 name from [v8-sql].master.dbo.sysaltfiles where dbid = @Source_ID and fileid = 2)
Также перед восстановлением необходимо разорвать все активные коннекты к базе-копии. Добьемся этого переводом ее в монопольный режим.
set @sql = N'ALTER DATABASE '+ @DestDatabaseName + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE' exec sp_executesql @sql
И начнем само восстановление из бэкапа:
restore database @DestDatabaseName from disk = @cmd_srting with replace,move @DB_logical_name to @DB_file_name, move @Log_logical_name to @Log_file_name, stats = 2
Архивируем бэкап с помощью консольного приложения из комплекта WinRAR. Сжатый бэкап помещаем в папку , где хранятся подневные архивы.
set @cmd_srting = '"C:Program FilesWinRARRAR.EXE" a -ep -m2 -md4096 -ilog'+@PackedBackUpPath+'lograr.txt -s '+ @PackedBackUpPath+@arc_name+' '+@destpath+@DestFileName EXEC master..xp_cmdshell @cmd_srting , no_output
Не забываем вернуть базе многопользовательский режим:
set @sql = N'ALTER DATABASE '+ @DestDatabaseName + ' SET MULTI_USER WITH ROLLBACK IMMEDIATE'
Для отслеживания шагов выполнения резервного копирования будем логировать каждый шаг, где может произойти сбой. Всю важную информацию будем выводить также с помощью print. Для логирования я использовал написанную ранее процедуру WriteToLog, хотя можно использовать и другие возможности. Вот ее текст:
CREATE PROCEDURE WriteToLog @line varchar(2000), @filename VARCHAR(200) = 'c:log.txt' AS DECLARE @FSO int, @hr int DECLARE @STREAM INT, @EXF BIT, @LF VARCHAR(200) EXEC @hr = sp_OACreate 'Scripting.FileSystemObject', @FSO OUTPUT EXEC @hr = sp_OAMethod @FSO, 'OpenTextFile' , @STREAM OUT, @filename, 8 , 1 EXEC @hr = sp_OAMethod @STREAM, 'WriteLine' , NULL, @LINE EXEC sp_OADestroy @STREAM print @line+' '+ @filename GO
Файл лога будет при этом иметь следующий вид:
--------------------- Начало ( 03 Mar 2012 23:05:01 ) ----------------
имя файла с последним бэкапом: D:BackUpAutoTD2006_db_201203032120.BAK ,Реальность его существования = 1
дата последнего бэкапа: Mar 3 2012 9:47PM, разница с текущей датой: 2 ч.
(03 Mar 2012 23:05:02) Команда удаления старых бэкапов: del d:FreshBackUptd2006*.* /Q Выполнено
(03 Mar 2012 23:05:02) Начало копирования. Команда копирования: c:windowsrobocopy.exe D:BackUpAuto V8-ARCd$FreshBackUp TD2006_db_201203032120.BAK
(04 Mar 2012 00:15:31) Конец копирования
база-копия уже существует
имена файлов данных и лога: H:MSSQLDataTDDaily.mdf, H:MSSQLDataTDDaily_log.ldf
логическое имя данных и лога базы-источника: TD2006_Data, TD2006_Log
( 04 Mar 2012 00:15:31 ) ALTER DATABASE tddaily SET SINGLE_USER WITH ROLLBACK IMMEDIATE
( 04 Mar 2012 00:15:31 ) Восстанавливаем бэкап из: d:FreshBackUptd2006_03_Mar_2012.BAK
( 04 Mar 2012 00:32:48 ) Окончание рестора
( 04 Mar 2012 00:32:48 ) Командная строка для архиватора: "C:Program FilesWinRARRAR.EXE" a -ep -m2 -md4096 -ilogd:DailyBackUpslograr.txt -s d:DailyBackUpstd2006_03_Mar_2012.rar d:FreshBackUptd2006_03_Mar_2012.BAK
( 04 Mar 2012 02:28:49 ) Окончание архивирования
( 04 Mar 2012 02:28:49 ) ALTER DATABASE tddaily SET MULTI_USER WITH ROLLBACK IMMEDIATE
--------------------- Окончание ( 04 Mar 2012 02:28:49 ) ----------------
Ниже привожу полный текст процедуры:
CREATE PROCEDURE ManageBackUp @SourceDatabaseName varchar(50) = 'td2006', -- Имя исходной базы на v8-sql @DestDatabaseName varchar(50) = 'tddaily', -- Имя базы-назначения на текущем сервере @MastBePacked int = 1, -- Необходимость упаковки бэкапа @destpath varchar(100) = 'd:FreshBackUp', -- папка на текущем сервере, где лежит последний загруженный c v8-sql бэкап @PackedBackUpPath varchar(100) = 'd:DailyBackUps', -- папка на текущем сервере, где складывается запакованный бэкап @PatternOfJobName nvarchar(100) = '%DB Backup%Рабоч%' -- имя job-а на v8-sql, который бэкапит @SourceDatabaseName-базу AS BEGIN declare @last_backup_name varchar(200) declare @arc_name varchar(200) declare @DestFileName varchar(200) declare @cmd_srting varchar(400) declare @last_backup_date datetime declare @Job_command nvarchar(400) declare @Cmd_String as nvarchar(1000) declare @result int declare @DB_logical_name varchar(100) declare @DB_file_name varchar(300) declare @Log_logical_name nvarchar(100) declare @Log_file_name varchar(200) declare @current_server_name varchar(50) declare @LogFname varchar(100) declare @LogLine varchar (2000) declare @SourceFileName varchar(50) --БАЛЯН имя исходного файла set @LogFname = 'd:DailyBackUpsBackUpLog.txt' set @LogLine = ' --------------------- Начало ( ' + convert(varchar(20),getdate(),13) + ' ) ----------------' exec _WriteToLog @LogLine ,@LogFname set @last_backup_name = ( select bmf.physical_device_name from ( select top 1 bs.media_set_id from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_start_date desc ) LastBackUp left join [v8-sql].msdb.dbo.backupmediafamily bmf on bmf.media_set_id = LastBackUp.media_set_id ) -- Проверим, а есть ли файл бэкапа declare @file_exists int exec [v8-sql].master.dbo.xp_fileexist @last_backup_name , @file_exists OUTPUT set @LogLine = ' имя файла с последним бэкапом: ' + @last_backup_name + ' ,Реальность его существования = '+ str(@file_exists) print 'имя файла с последним бэкапом: ' + @last_backup_name + ' ,Реальность его существования = '+ str(@file_exists) exec _WriteToLog @LogLine ,@LogFname -- дата последнего бэкапа set @last_backup_date = ISNULL(( select top 1 bs.backup_finish_date from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_finish_date desc ),cast('' as datetime)) set @LogLine =' дата последнего бэкапа: ' + cast(@last_backup_date as varchar(25))+', разница с текущей датой: '+ str(DATEDIFF(HOUR,@last_backup_date,GETDATE()))+' ч.' print ' дата последнего бэкапа: ' + cast(@last_backup_date as varchar(25))+', разница с текущей датой: '+ str(DATEDIFF(HOUR,@last_backup_date,GETDATE()))+' ч.' exec _WriteToLog @LogLine ,@LogFname --Если бэкапу больше чем день или файла нет, попробуем забэкапить IF (DATEDIFF(HOUR,@last_backup_date,GETDATE()) > 24) or (@file_exists <> 1) BEGIN --грохнем старый бэкап, вдруг места нет на диске set @Cmd_String = 'del '+@last_backup_name+' /Q' set @LogLine =' ('+ convert(varchar(20),getdate(),13) +') Команда удаления старого бэкапа: '+ @Cmd_String print 'Команда удаления старого бэкапа:'+ @Cmd_String exec _WriteToLog @LogLine ,@LogFname exec [v8-sql].master.dbo.xp_cmdshell @Cmd_String -- найдем джоб с бэкапом set @Job_command = ISNULL(( select sjs.Command from [v8-sql].msdb.dbo.sysjobsteps as sjs inner join [v8-sql].msdb.dbo.sysjobs as sj on sjs.job_id = sj.job_id and sj.name like @PatternOfJobName where sjs.step_id = 1 ),'RAISEERROR (''Нету job-а '''+@PatternOfJobName+',16,1)') select @Job_command -- запустим job exec [v8-sql].master.dbo.sp_executesql @Job_command -- опять проверим дату последнего бэкапа set @last_backup_date = ISNULL(( select top 1 bs.backup_finish_date from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_finish_date desc ),cast('' as datetime)) set @LogLine =' ('+ convert(varchar(20),getdate(),13) +') новая дата последнего бэкапа: ' + cast(@last_backup_date as varchar(25))+', разница с текущей датой: '+ str(DATEDIFF(HOUR,@last_backup_date,GETDATE()))+' ч.' print 'новая дата последнего бэкапа: ' + cast(@last_backup_date as varchar(25))+', разница с текущей датой: '+ str(DATEDIFF(HOUR,@last_backup_date,GETDATE()))+' ч.' exec _WriteToLog @LogLine ,@LogFname -- считаем еще раз имя файла с последним бэкапом set @last_backup_name = ( select bmf.physical_device_name from ( select top 1 bs.media_set_id from [v8-sql].msdb.dbo.backupset bs where bs.database_name = @SourceDatabaseName and bs.type='D' order by bs.backup_start_date desc ) LastBackUp left join [v8-sql].msdb.dbo.backupmediafamily bmf on bmf.media_set_id = LastBackUp.media_set_id ) -- Проверим еще раз, не появился ли файл бэкапа exec [v8-sql].master.dbo.xp_fileexist @last_backup_name , @file_exists OUTPUT -- не помогло, тогда выпьем яду IF (DATEDIFF(HOUR,@last_backup_date,GETDATE()) > 24) or (@file_exists <> 1) BEGIN RAISERROR ('Не удалось выполнить бэкап базы', 16, 1) END END -- очистим папку на тек. сервере для приема свежего бэкапа по шаблону set @Cmd_String = 'del '+@destpath+@SourceDatabaseName+'*.* /Q' set @LogLine =' ('+ convert(varchar(20),getdate(),13) +') Команда удаления старых бэкапов: '+ @Cmd_String print 'Команда удаления старых бэкапов: '+ @Cmd_String exec _WriteToLog @LogLine ,@LogFname print @LogLine exec master.dbo.xp_cmdshell @Cmd_String exec _WriteToLog 'Выполнено' ,@LogFname -- скопируем файл на текущий сервер set @current_server_name = (select top 1 srvname from master.dbo.sysservers where isremote=0) -- имя тек. сервера set @DestFileName = @SourceDatabaseName + '_' + replace(convert(char,@last_backup_date,106),' ','_')+'.BAK' -- имя файла без пути, соджержит время бэкапа set @arc_name = @SourceDatabaseName + '_' + replace(convert(char,@last_backup_date,106),' ','_')+'.rar' -- имя будущего архива без пути, соджержит время бэкапа set @SourceFileName = SUBSTRING(@last_backup_name,Charindex(@SourceDatabaseName,@last_backup_name),LEN(@last_backup_name)) --определим имя исходного файла, потому как у robocopy.exe дурацкий синтаксис set @Cmd_String = 'c:windowsrobocopy.exe ' + REPLACE(@last_backup_name,@SourceFileName,' ') + ' \'+@current_server_name+''+ REPLACE(@destpath,':','$') + ' ' + @SourceFileName -- заменяем : в пути на админскую шару $ set @LogLine =' ('+ convert(varchar(20),getdate(),13) +') Начало копирования. Команда копирования: '+ @Cmd_String exec _WriteToLog @LogLine ,@LogFname print 'Команда копирования: '+ @Cmd_String exec [v8-sql].master.dbo.xp_cmdshell @Cmd_String set @LogLine =' ('+ convert(varchar(20),getdate(),13) +') Конец копирования ' exec _WriteToLog @LogLine ,@LogFname -- переименуем файл потому, что он скопировался с исходным именем set @Cmd_String = 'REN ' + @destpath + @SourceFileName + ' ' + @DestFileName exec master.dbo.xp_cmdshell @Cmd_String -- Проверим существаование базы-копии set @result = (select count(1) from master.dbo.sysdatabases where name = @DestDatabaseName) if @result = 0 begin -- если нет назначим имена файлов базы и лога -- получим имя файла с базой master set @Cmd_String = (select top 1 filename from master.dbo.sysaltfiles where dbid = 1 and fileid = 1) -- положим базу-копию в папке базы master set @DB_file_name = REPLACE(@Cmd_String,'master.mdf',RTRIM(@DestDatabaseName) + '.mdf') set @Log_file_name = REPLACE(@Cmd_String,'master.mdf',RTRIM(@DestDatabaseName) + '_log.ldf') print 'базы-копии нет' exec _WriteToLog ' базы-копии нет' ,@LogFname end else begin -- если база есть, определим имена ее файлов set @DB_file_name = (select top 1 sf.filename from master.dbo.sysdatabases sd left join master..sysaltfiles sf on sf.dbid = sd.dbid where sf.fileid = 1 and sd.name = @DestDatabaseName) set @Log_file_name = (select top 1 sf.filename from master.dbo.sysdatabases sd left join master..sysaltfiles sf on sf.dbid = sd.dbid where sf.fileid = 2 and sd.name = @DestDatabaseName) print 'база-копия уже существует' exec _WriteToLog ' база-копия уже существует' ,@LogFname end -- на всякий случай порежем пробелы set @DB_file_name = RTRIM(@DB_file_name) set @Log_file_name = RTRIM(@Log_file_name) print 'имена файлов данных и лога: '+@DB_file_name+', '+ @Log_file_name set @LogLine =' имена файлов данных и лога: '+@DB_file_name+', '+ @Log_file_name exec _WriteToLog @LogLine ,@LogFname -- определим логическое имя базы и лога базы-источника declare @source_ID int set @source_ID = (select dbid from [v8-sql].master.dbo.sysdatabases where name = @SourceDatabaseName) set @DB_logical_name = (select top 1 name from [v8-sql].master.dbo.sysaltfiles where dbid = @Source_ID and fileid = 1) set @Log_logical_name = (select top 1 name from [v8-sql].master.dbo.sysaltfiles where dbid = @Source_ID and fileid = 2) -- на всякий случай порежем пробелы set @DB_logical_name = RTRIM(@DB_logical_name) set @Log_logical_name = RTRIM(@Log_logical_name) print 'логическое имя данных и лога базы-источника: '+@DB_logical_name+', '+ @Log_logical_name set @LogLine =' логическое имя данных и лога базы-источника: '+@DB_logical_name+', '+ @Log_logical_name exec _WriteToLog @LogLine ,@LogFname declare @t datetime set @t=getdate() declare @sql nvarchar(150) set @sql = N'ALTER DATABASE '+ @DestDatabaseName + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE' print @sql set @LogLine =' ( '+ convert(varchar(20),getdate(),13) +' ) '+@sql exec _WriteToLog @LogLine ,@LogFname exec sp_executesql @sql set @cmd_srting = @destpath+@DestFileName print 'Восстанавливаем бэкап из: ' + @cmd_srting set @LogLine =' ( '+ convert(varchar(20),getdate(),13) +' ) Восстанавливаем бэкап из: ' + @cmd_srting exec _WriteToLog @LogLine ,@LogFname restore database @DestDatabaseName from disk = @cmd_srting with replace,move @DB_logical_name to @DB_file_name, move @Log_logical_name to @Log_file_name, stats = 2 set @LogLine =' ( '+ convert(varchar(20),getdate(),13) +' ) Окончание рестора ' exec _WriteToLog @LogLine ,@LogFname if @MastBePacked = 1 begin set @cmd_srting = '"C:Program FilesWinRARRAR.EXE" a -ep -m2 -md4096 -ilog'+@PackedBackUpPath+'lograr.txt -s '+ @PackedBackUpPath+@arc_name+' '+@destpath+@DestFileName set @LogLine =' ( '+ convert(varchar(20),getdate(),13) +' ) Командная строка для архиватора: ' + @cmd_srting exec _WriteToLog @LogLine ,@LogFname print 'Командная строка для архиватора: ' + @cmd_srting EXEC master..xp_cmdshell @cmd_srting , no_output set @LogLine =' ( '+ convert(varchar(20),getdate(),13) +' ) Окончание архивирования ' exec _WriteToLog @LogLine ,@LogFname end -- вернем многопользовательский режим set @sql = N'ALTER DATABASE '+ @DestDatabaseName + ' SET MULTI_USER WITH ROLLBACK IMMEDIATE' print @sql set @LogLine ='