Grup Ketersediaan, yang diperkenalkan di SQL Server 2012, mewakili perubahan mendasar dalam cara kami berpikir tentang ketersediaan tinggi dan pemulihan bencana untuk database kami. Salah satu hal hebat yang dimungkinkan di sini adalah memindahkan operasi baca-saja ke replika sekunder, sehingga instance baca/tulis utama tidak terganggu oleh hal-hal buruk seperti pelaporan pengguna akhir. Menyiapkan ini tidak sederhana, tetapi jauh lebih mudah dan lebih mudah dipelihara daripada solusi sebelumnya (angkat tangan Anda jika Anda suka menyiapkan mirroring dan snapshot, dan semua pemeliharaan terus-menerus yang terlibat dengan itu).
Orang-orang menjadi sangat bersemangat saat mendengar tentang Grup Ketersediaan. Kemudian kenyataan muncul:fitur tersebut memerlukan Edisi Perusahaan dari SQL Server (pada SQL Server 2014, toh). Edisi Perusahaan mahal, terutama jika Anda memiliki banyak inti, dan terutama sejak penghapusan lisensi berbasis CAL (kecuali jika Anda telah menjadi kakek dari 2008 R2, dalam hal ini Anda terbatas pada 20 inti pertama). Ini juga memerlukan Windows Server Failover Clustering (WSFC), komplikasi tidak hanya untuk mendemonstrasikan teknologi pada laptop, tetapi juga membutuhkan Enterprise Edition Windows, pengontrol domain, dan sejumlah besar konfigurasi untuk mendukung pengelompokan. Dan ada persyaratan baru seputar Jaminan Perangkat Lunak juga; biaya tambahan jika Anda ingin instans siaga Anda sesuai.
Beberapa pelanggan tidak dapat membenarkan harga. Orang lain melihat nilainya, tetapi tidak mampu membelinya. Jadi apa yang harus dilakukan para pengguna ini?
Pahlawan Baru Anda:Log Pengiriman
Log pengiriman telah ada selama berabad-abad. Ini sederhana dan itu hanya berfungsi. Hampir selalu. Dan selain melewati biaya lisensi dan rintangan konfigurasi yang disajikan oleh Availability Groups, juga dapat menghindari penalti 14 byte yang dibicarakan oleh Paul Randal (@PaulRandal) dalam buletin SQLskills Insider minggu ini (13 Oktober 2014).
Namun, salah satu tantangan yang dihadapi orang dengan menggunakan salinan yang dikirimkan log sebagai sekunder yang dapat dibaca, adalah Anda harus mengeluarkan semua pengguna saat ini untuk menerapkan log baru – jadi Anda membuat pengguna merasa terganggu karena mereka berulang kali terganggu dari menjalankan kueri, atau Anda membuat pengguna kesal karena data mereka basi. Ini karena orang membatasi diri mereka pada satu sekunder yang dapat dibaca.
Tidak harus seperti itu; Saya pikir ada solusi yang anggun di sini, dan meskipun mungkin memerlukan lebih banyak pekerjaan kaki di depan daripada, katakanlah, mengaktifkan Grup Ketersediaan, itu pasti akan menjadi opsi yang menarik bagi sebagian orang.
Pada dasarnya, kita dapat mengatur sejumlah sekunder, di mana kita akan mencatat kapal dan menjadikan salah satunya sebagai sekunder "aktif", menggunakan pendekatan round-robin. Pekerjaan yang mengirimkan log mengetahui mana yang sedang aktif, sehingga hanya mengembalikan log baru ke server "berikutnya" menggunakan WITH STANDBY
pilihan. Aplikasi pelaporan menggunakan informasi yang sama untuk menentukan saat runtime seperti apa string koneksi untuk laporan berikutnya yang dijalankan pengguna. Saat pencadangan log berikutnya sudah siap, semuanya bergeser satu per satu, dan instance yang sekarang akan menjadi sekunder baru yang dapat dibaca akan dipulihkan menggunakan WITH STANDBY
.
Agar model tidak rumit, katakanlah kita memiliki empat instance yang berfungsi sebagai sekunder yang dapat dibaca, dan kita mengambil cadangan log setiap 15 menit. Setiap saat, kami akan memiliki satu sekunder aktif dalam mode siaga, dengan data tidak lebih lama dari 15 menit, dan tiga sekunder dalam mode siaga yang tidak melayani kueri baru (tetapi mungkin masih mengembalikan hasil untuk kueri lama).
Ini akan bekerja paling baik jika tidak ada kueri yang diharapkan bertahan lebih dari 45 menit. (Anda mungkin perlu menyesuaikan siklus ini bergantung pada sifat operasi hanya-baca, berapa banyak pengguna serentak yang menjalankan kueri lebih lama, dan apakah mungkin mengganggu pengguna dengan mengeluarkan semua orang.)
Ini juga akan berfungsi paling baik jika kueri berurutan yang dijalankan oleh pengguna yang sama dapat mengubah string koneksi mereka (ini adalah logika yang perlu ada dalam aplikasi, meskipun Anda dapat menggunakan sinonim atau tampilan tergantung pada arsitektur), dan berisi data berbeda yang memiliki berubah sementara itu (seperti jika mereka menanyakan database langsung yang terus berubah).
Dengan mempertimbangkan semua asumsi ini, berikut adalah ilustratif urutan kejadian selama 75 menit pertama implementasi kami:
waktu | acara | visual |
---|---|---|
12:00 (t0) |
| |
12:15 (t1) |
| |
12:30 (t2) |
| |
12:45 (t3) |
| |
13:00 (t4) |
|
Itu mungkin tampak cukup sederhana; menulis kode untuk menangani semua itu sedikit lebih menakutkan. Garis besar kasar:
- Di server utama (saya akan menyebutnya
BOSS
), membuat database. Bahkan sebelum berpikir untuk melangkah lebih jauh, aktifkan Trace Flag 3226 untuk mencegah pesan pencadangan yang berhasil mengotori log kesalahan SQL Server. - Di
BOSS
, tambahkan server tertaut untuk setiap sekunder (saya akan menyebutnyaPEON1
->PEON4
). - Di suatu tempat yang dapat diakses oleh semua server, buat file yang dibagikan untuk menyimpan cadangan database/log, dan pastikan akun layanan untuk setiap instans memiliki akses baca/tulis. Selain itu, setiap instans sekunder harus memiliki lokasi yang ditentukan untuk file siaga.
- Dalam database utilitas terpisah (atau MSDB, jika Anda mau), buat tabel yang akan menyimpan informasi konfigurasi tentang database, semua sekunder, dan log backup dan restore history.
- Buat prosedur tersimpan yang akan mencadangkan database dan memulihkan ke
WITH NORECOVERY
sekunder , lalu terapkan satu logWITH STANDBY
, dan tandai satu instans sebagai sekunder siaga saat ini. Prosedur ini juga dapat digunakan untuk menginisialisasi ulang seluruh penyiapan pengiriman log jika terjadi kesalahan. - Buat tugas yang akan berjalan setiap 15 menit, untuk melakukan tugas yang dijelaskan di atas:
- cadangkan log
- menentukan sekunder mana yang akan menerapkan pencadangan log yang belum diterapkan
- pulihkan log tersebut dengan setelan yang sesuai
- Buat prosedur tersimpan (dan/atau tampilan?) yang akan memberi tahu aplikasi pemanggil sekunder mana yang harus mereka gunakan untuk kueri hanya-baca baru.
- Buat prosedur pembersihan untuk menghapus riwayat pencadangan log untuk log yang telah diterapkan ke semua sekunder (dan mungkin juga untuk memindahkan atau membersihkan file itu sendiri).
- Tingkatkan solusi dengan penanganan kesalahan dan pemberitahuan yang kuat.
Langkah 1 – buat database
Instance utama saya adalah Edisi Standar, bernama .\BOSS
. Pada contoh itu saya membuat database sederhana dengan satu tabel:
USE [master]; GO CREATE DATABASE UserData; GO ALTER DATABASE UserData SET RECOVERY FULL; GO USE UserData; GO CREATE TABLE dbo.LastUpdate(EventTime DATETIME2); INSERT dbo.LastUpdate(EventTime) SELECT SYSDATETIME();
Lalu saya membuat pekerjaan Agen Server SQL yang hanya memperbarui stempel waktu itu setiap menit:
UPDATE UserData.dbo.LastUpdate SET EventTime = SYSDATETIME();
Itu hanya membuat database awal dan mensimulasikan aktivitas, memungkinkan kami untuk memvalidasi bagaimana tugas pengiriman log berputar melalui masing-masing sekunder yang dapat dibaca. Saya ingin menyatakan secara eksplisit bahwa inti dari latihan ini bukanlah untuk menekankan pengiriman log uji atau untuk membuktikan berapa banyak volume yang dapat kita tembus; itu adalah latihan yang berbeda sama sekali.
Langkah 2 – tambahkan server tertaut
Saya memiliki empat instans Edisi Ekspres sekunder bernama .\PEON1
, .\PEON2
, .\PEON3
, dan .\PEON4
. Jadi saya menjalankan kode ini empat kali, mengubah @s
setiap kali:
USE [master]; GO DECLARE @s NVARCHAR(128) = N'.\PEON1', -- repeat for .\PEON2, .\PEON3, .\PEON4 @t NVARCHAR(128) = N'true'; EXEC [master].dbo.sp_addlinkedserver @server = @s, @srvproduct = N'SQL Server'; EXEC [master].dbo.sp_addlinkedsrvlogin @rmtsrvname = @s, @useself = @t; EXEC [master].dbo.sp_serveroption @server = @s, @optname = N'collation compatible', @optvalue = @t; EXEC [master].dbo.sp_serveroption @server = @s, @optname = N'data access', @optvalue = @t; EXEC [master].dbo.sp_serveroption @server = @s, @optname = N'rpc', @optvalue = @t; EXEC [master].dbo.sp_serveroption @server = @s, @optname = N'rpc out', @optvalue = @t;
Langkah 3 – validasi file yang dibagikan
Dalam kasus saya, semua 5 instance berada di server yang sama, jadi saya baru saja membuat folder untuk setiap instance:C:\temp\Peon1\
, C:\temp\Peon2\
, dan seterusnya. Ingatlah bahwa jika sekunder Anda berada di server yang berbeda, lokasinya harus relatif terhadap server itu, tetapi masih dapat diakses dari server utama (jadi biasanya jalur UNC akan digunakan). Anda harus memvalidasi bahwa setiap instance dapat menulis ke share itu, dan Anda juga harus memvalidasi bahwa setiap instance dapat menulis ke lokasi yang ditentukan untuk file standby (saya menggunakan folder yang sama untuk standby). Anda dapat memvalidasi ini dengan mencadangkan database kecil dari setiap instance ke setiap lokasi yang ditentukan – jangan lanjutkan sampai ini berhasil.
Langkah 4 – buat tabel
Saya memutuskan untuk menempatkan data ini di msdb
, tetapi saya tidak memiliki perasaan yang kuat untuk atau menentang pembuatan database terpisah. Tabel pertama yang saya butuhkan adalah tabel yang menyimpan informasi tentang database yang akan saya log pengirimannya:
CREATE TABLE dbo.PMAG_Databases ( DatabaseName SYSNAME, LogBackupFrequency_Minutes SMALLINT NOT NULL DEFAULT (15), CONSTRAINT PK_DBS PRIMARY KEY(DatabaseName) ); GO INSERT dbo.PMAG_Databases(DatabaseName) SELECT N'UserData';
(Jika Anda penasaran dengan skema penamaan, PMAG adalah singkatan dari "Kelompok Ketersediaan Orang Miskin.")
Tabel lain yang diperlukan adalah tabel untuk menyimpan informasi tentang sekunder, termasuk folder masing-masing dan statusnya saat ini dalam urutan pengiriman log.
CREATE TABLE dbo.PMAG_Secondaries ( DatabaseName SYSNAME, ServerInstance SYSNAME, CommonFolder VARCHAR(512) NOT NULL, DataFolder VARCHAR(512) NOT NULL, LogFolder VARCHAR(512) NOT NULL, StandByLocation VARCHAR(512) NOT NULL, IsCurrentStandby BIT NOT NULL DEFAULT 0, CONSTRAINT PK_Sec PRIMARY KEY(DatabaseName, ServerInstance), CONSTRAINT FK_Sec_DBs FOREIGN KEY(DatabaseName) REFERENCES dbo.PMAG_Databases(DatabaseName) );
Jika Anda ingin mencadangkan dari server sumber secara lokal, dan mengembalikan sekunder dari jarak jauh, atau sebaliknya, Anda dapat membagi CommonFolder
menjadi dua kolom (BackupFolder
dan RestoreFolder
), dan buat perubahan yang relevan pada kode (tidak akan sebanyak itu).
Karena saya dapat mengisi tabel ini setidaknya sebagian berdasarkan informasi di sys.servers
– mengambil keuntungan dari fakta bahwa data / log dan folder lain dinamai dengan nama instance:
INSERT dbo.PMAG_Secondaries ( DatabaseName, ServerInstance, CommonFolder, DataFolder, LogFolder, StandByLocation ) SELECT DatabaseName = N'UserData', ServerInstance = name, CommonFolder = 'C:\temp\Peon' + RIGHT(name, 1) + '\', DataFolder = 'C:\Program Files\Microsoft SQL Server\MSSQL12.PEON' + RIGHT(name, 1) + '\MSSQL\DATA\', LogFolder = 'C:\Program Files\Microsoft SQL Server\MSSQL12.PEON' + RIGHT(name, 1) + '\MSSQL\DATA\', StandByLocation = 'C:\temp\Peon' + RIGHT(name, 1) + '\' FROM sys.servers WHERE name LIKE N'.\PEON[1-4]';
Saya juga memerlukan tabel untuk melacak cadangan log individual (bukan hanya yang terakhir), karena dalam banyak kasus saya harus memulihkan beberapa file log secara berurutan. Saya bisa mendapatkan informasi ini dari msdb.dbo.backupset
, tetapi jauh lebih rumit untuk mendapatkan hal-hal seperti lokasi – dan saya mungkin tidak memiliki kendali atas pekerjaan lain yang mungkin membersihkan riwayat pencadangan.
CREATE TABLE dbo.PMAG_LogBackupHistory ( DatabaseName SYSNAME, ServerInstance SYSNAME, BackupSetID INT NOT NULL, Location VARCHAR(2000) NOT NULL, BackupTime DATETIME NOT NULL DEFAULT SYSDATETIME(), CONSTRAINT PK_LBH PRIMARY KEY(DatabaseName, ServerInstance, BackupSetID), CONSTRAINT FK_LBH_DBs FOREIGN KEY(DatabaseName) REFERENCES dbo.PMAG_Databases(DatabaseName), CONSTRAINT FK_LBH_Sec FOREIGN KEY(DatabaseName, ServerInstance) REFERENCES dbo.PMAG_Secondaries(DatabaseName, ServerInstance) );
Anda mungkin berpikir menyimpan baris untuk setiap sekunder adalah pemborosan, dan untuk menyimpan lokasi setiap cadangan, tetapi ini untuk pemeriksaan di masa mendatang – untuk menangani kasus di mana Anda memindahkan CommonFolder untuk sekunder mana pun.
Dan akhirnya riwayat pemulihan log sehingga, kapan saja, saya dapat melihat log mana yang telah dipulihkan dan di mana, dan pekerjaan pemulihan dapat dipastikan hanya memulihkan log yang belum dipulihkan:
CREATE TABLE dbo.PMAG_LogRestoreHistory ( DatabaseName SYSNAME, ServerInstance SYSNAME, BackupSetID INT, RestoreTime DATETIME, CONSTRAINT PK_LRH PRIMARY KEY(DatabaseName, ServerInstance, BackupSetID), CONSTRAINT FK_LRH_DBs FOREIGN KEY(DatabaseName) REFERENCES dbo.PMAG_Databases(DatabaseName), CONSTRAINT FK_LRH_Sec FOREIGN KEY(DatabaseName, ServerInstance) REFERENCES dbo.PMAG_Secondaries(DatabaseName, ServerInstance) );
Langkah 5 – inisialisasi sekunder
Kami memerlukan prosedur tersimpan yang akan menghasilkan file cadangan (dan mencerminkannya ke lokasi mana pun yang diperlukan oleh instance yang berbeda), dan kami juga akan memulihkan satu log ke setiap sekunder untuk menempatkan semuanya dalam keadaan siaga. Pada titik ini semuanya akan tersedia untuk kueri hanya-baca, tetapi hanya satu yang akan menjadi siaga "saat ini" pada satu waktu. Ini adalah prosedur tersimpan yang akan menangani pencadangan log transaksi dan penuh; ketika cadangan lengkap diminta, dan @init
disetel ke 1, ini secara otomatis menginisialisasi ulang pengiriman log.
CREATE PROCEDURE [dbo].[PMAG_Backup] @dbname SYSNAME, @type CHAR(3) = 'bak', -- or 'trn' @init BIT = 0 -- only used with 'bak' AS BEGIN SET NOCOUNT ON; -- generate a filename pattern DECLARE @now DATETIME = SYSDATETIME(); DECLARE @fn NVARCHAR(256) = @dbname + N'_' + CONVERT(CHAR(8), @now, 112) + RIGHT(REPLICATE('0',6) + CONVERT(VARCHAR(32), DATEDIFF(SECOND, CONVERT(DATE, @now), @now)), 6) + N'.' + @type; -- generate a backup command with MIRROR TO for each distinct CommonFolder DECLARE @sql NVARCHAR(MAX) = N'BACKUP' + CASE @type WHEN 'bak' THEN N' DATABASE ' ELSE N' LOG ' END + QUOTENAME(@dbname) + ' ' + STUFF( (SELECT DISTINCT CHAR(13) + CHAR(10) + N' MIRROR TO DISK = ''' + s.CommonFolder + @fn + '''' FROM dbo.PMAG_Secondaries AS s WHERE s.DatabaseName = @dbname FOR XML PATH(''), TYPE).value(N'.[1]',N'nvarchar(max)'),1,9,N'') + N' WITH NAME = N''' + @dbname + CASE @type WHEN 'bak' THEN N'_PMAGFull' ELSE N'_PMAGLog' END + ''', INIT, FORMAT' + CASE WHEN LEFT(CONVERT(NVARCHAR(128), SERVERPROPERTY(N'Edition')), 3) IN (N'Dev', N'Ent') THEN N', COMPRESSION;' ELSE N';' END; EXEC [master].sys.sp_executesql @sql; IF @type = 'bak' AND @init = 1 -- initialize log shipping BEGIN EXEC dbo.PMAG_InitializeSecondaries @dbname = @dbname, @fn = @fn; END IF @type = 'trn' BEGIN -- record the fact that we backed up a log INSERT dbo.PMAG_LogBackupHistory ( DatabaseName, ServerInstance, BackupSetID, Location ) SELECT DatabaseName = @dbname, ServerInstance = s.ServerInstance, BackupSetID = MAX(b.backup_set_id), Location = s.CommonFolder + @fn FROM msdb.dbo.backupset AS b CROSS JOIN dbo.PMAG_Secondaries AS s WHERE b.name = @dbname + N'_PMAGLog' AND s.DatabaseName = @dbname GROUP BY s.ServerInstance, s.CommonFolder + @fn; -- once we've backed up logs, -- restore them on the next secondary EXEC dbo.PMAG_RestoreLogs @dbname = @dbname; END END
Ini pada gilirannya memanggil dua prosedur yang dapat Anda panggil secara terpisah (tetapi kemungkinan besar tidak). Pertama, prosedur yang akan menginisialisasi sekunder saat pertama dijalankan:
ALTER PROCEDURE dbo.PMAG_InitializeSecondaries @dbname SYSNAME, @fn VARCHAR(512) AS BEGIN SET NOCOUNT ON; -- clear out existing history/settings (since this may be a re-init) DELETE dbo.PMAG_LogBackupHistory WHERE DatabaseName = @dbname; DELETE dbo.PMAG_LogRestoreHistory WHERE DatabaseName = @dbname; UPDATE dbo.PMAG_Secondaries SET IsCurrentStandby = 0 WHERE DatabaseName = @dbname; DECLARE @sql NVARCHAR(MAX) = N'', @files NVARCHAR(MAX) = N''; -- need to know the logical file names - may be more than two SET @sql = N'SELECT @files = (SELECT N'', MOVE N'''''' + name + '''''' TO N''''$'' + CASE [type] WHEN 0 THEN N''df'' WHEN 1 THEN N''lf'' END + ''$'''''' FROM ' + QUOTENAME(@dbname) + '.sys.database_files WHERE [type] IN (0,1) FOR XML PATH, TYPE).value(N''.[1]'',N''nvarchar(max)'');'; EXEC master.sys.sp_executesql @sql, N'@files NVARCHAR(MAX) OUTPUT', @files = @files OUTPUT; SET @sql = N''; -- restore - need physical paths of data/log files for WITH MOVE -- this can fail, obviously, if those path+names already exist for another db SELECT @sql += N'EXEC ' + QUOTENAME(ServerInstance) + N'.master.sys.sp_executesql N''RESTORE DATABASE ' + QUOTENAME(@dbname) + N' FROM DISK = N''''' + CommonFolder + @fn + N'''''' + N' WITH REPLACE, NORECOVERY' + REPLACE(REPLACE(REPLACE(@files, N'$df$', DataFolder + @dbname + N'.mdf'), N'$lf$', LogFolder + @dbname + N'.ldf'), N'''', N'''''') + N';'';' + CHAR(13) + CHAR(10) FROM dbo.PMAG_Secondaries WHERE DatabaseName = @dbname; EXEC [master].sys.sp_executesql @sql; -- backup a log for this database EXEC dbo.PMAG_Backup @dbname = @dbname, @type = 'trn'; -- restore logs EXEC dbo.PMAG_RestoreLogs @dbname = @dbname, @PrepareAll = 1; END
Dan kemudian prosedur yang akan mengembalikan log:
CREATE PROCEDURE dbo.PMAG_RestoreLogs @dbname SYSNAME, @PrepareAll BIT = 0 AS BEGIN SET NOCOUNT ON; DECLARE @StandbyInstance SYSNAME, @CurrentInstance SYSNAME, @BackupSetID INT, @Location VARCHAR(512), @StandByLocation VARCHAR(512), @sql NVARCHAR(MAX), @rn INT; -- get the "next" standby instance SELECT @StandbyInstance = MIN(ServerInstance) FROM dbo.PMAG_Secondaries WHERE IsCurrentStandby = 0 AND ServerInstance > (SELECT ServerInstance FROM dbo.PMAG_Secondaries WHERE IsCurrentStandBy = 1); IF @StandbyInstance IS NULL -- either it was last or a re-init BEGIN SELECT @StandbyInstance = MIN(ServerInstance) FROM dbo.PMAG_Secondaries; END -- get that instance up and into STANDBY -- for each log in logbackuphistory not in logrestorehistory: -- restore, and insert it into logrestorehistory -- mark the last one as STANDBY -- if @prepareAll is true, mark all others as NORECOVERY -- in this case there should be only one, but just in case DECLARE c CURSOR LOCAL FAST_FORWARD FOR SELECT bh.BackupSetID, s.ServerInstance, bh.Location, s.StandbyLocation, rn = ROW_NUMBER() OVER (PARTITION BY s.ServerInstance ORDER BY bh.BackupSetID DESC) FROM dbo.PMAG_LogBackupHistory AS bh INNER JOIN dbo.PMAG_Secondaries AS s ON bh.DatabaseName = s.DatabaseName AND bh.ServerInstance = s.ServerInstance WHERE s.DatabaseName = @dbname AND s.ServerInstance = CASE @PrepareAll WHEN 1 THEN s.ServerInstance ELSE @StandbyInstance END AND NOT EXISTS ( SELECT 1 FROM dbo.PMAG_LogRestoreHistory AS rh WHERE DatabaseName = @dbname AND ServerInstance = s.ServerInstance AND BackupSetID = bh.BackupSetID ) ORDER BY CASE s.ServerInstance WHEN @StandbyInstance THEN 1 ELSE 2 END, bh.BackupSetID; OPEN c; FETCH c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn; WHILE @@FETCH_STATUS -1 BEGIN -- kick users out - set to single_user then back to multi SET @sql = N'EXEC ' + QUOTENAME(@CurrentInstance) + N'.[master].sys.sp_executesql ' + 'N''IF EXISTS (SELECT 1 FROM sys.databases WHERE name = N''''' + @dbname + ''''' AND [state] 1) BEGIN ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET SINGLE_USER ' + N'WITH ROLLBACK IMMEDIATE; ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET MULTI_USER; END;'';'; EXEC [master].sys.sp_executesql @sql; -- restore the log (in STANDBY if it's the last one): SET @sql = N'EXEC ' + QUOTENAME(@CurrentInstance) + N'.[master].sys.sp_executesql ' + N'N''RESTORE LOG ' + QUOTENAME(@dbname) + N' FROM DISK = N''''' + @Location + N''''' WITH ' + CASE WHEN @rn = 1 AND (@CurrentInstance = @StandbyInstance OR @PrepareAll = 1) THEN N'STANDBY = N''''' + @StandbyLocation + @dbname + N'.standby''''' ELSE N'NORECOVERY' END + N';'';'; EXEC [master].sys.sp_executesql @sql; -- record the fact that we've restored logs INSERT dbo.PMAG_LogRestoreHistory (DatabaseName, ServerInstance, BackupSetID, RestoreTime) SELECT @dbname, @CurrentInstance, @BackupSetID, SYSDATETIME(); -- mark the new standby IF @rn = 1 AND @CurrentInstance = @StandbyInstance -- this is the new STANDBY BEGIN UPDATE dbo.PMAG_Secondaries SET IsCurrentStandby = CASE ServerInstance WHEN @StandbyInstance THEN 1 ELSE 0 END WHERE DatabaseName = @dbname; END FETCH c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn; END CLOSE c; DEALLOCATE c; END
(Saya tahu ini banyak kode, dan banyak SQL dinamis yang samar. Saya mencoba untuk menjadi sangat liberal dengan komentar; jika ada bagian yang Anda kesulitan, beri tahu saya.)
Jadi sekarang, yang harus Anda lakukan untuk mengaktifkan dan menjalankan sistem adalah membuat dua panggilan prosedur:
EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'bak', @init = 1; EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';
Sekarang Anda akan melihat setiap instance dengan salinan database siaga:
Dan Anda dapat melihat mana yang saat ini berfungsi sebagai siaga hanya-baca:
SELECT ServerInstance, IsCurrentStandby FROM dbo.PMAG_Secondaries WHERE DatabaseName = N'UserData';
Langkah 6 – buat tugas yang mencadangkan / memulihkan log
Anda dapat memasukkan perintah ini ke dalam pekerjaan yang Anda jadwalkan setiap 15 menit:
EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';
Ini akan menggeser sekunder aktif setiap 15 menit, dan datanya akan menjadi 15 menit lebih baru dari sekunder aktif sebelumnya. Jika Anda memiliki beberapa database pada jadwal yang berbeda, Anda dapat membuat beberapa pekerjaan, atau menjadwalkan pekerjaan lebih sering dan memeriksa dbo.PMAG_Databases
tabel untuk masing-masing LogBackupFrequency_Minutes
nilai untuk menentukan apakah Anda harus menjalankan pencadangan/pemulihan untuk database tersebut.
Langkah 7 – tampilan dan prosedur untuk memberi tahu aplikasi standby mana yang aktif
CREATE VIEW dbo.PMAG_ActiveSecondaries AS SELECT DatabaseName, ServerInstance FROM dbo.PMAG_Secondaries WHERE IsCurrentStandby = 1; GO CREATE PROCEDURE dbo.PMAG_GetActiveSecondary @dbname SYSNAME AS BEGIN SET NOCOUNT ON; SELECT ServerInstance FROM dbo.PMAG_ActiveSecondaries WHERE DatabaseName = @dbname; END GO
Dalam kasus saya, saya juga secara manual membuat penyatuan tampilan di semua UserData
database sehingga saya dapat membandingkan kemutakhiran data di primer dengan setiap sekunder.
CREATE VIEW dbo.PMAG_CompareRecency_UserData AS WITH x(ServerInstance, EventTime) AS ( SELECT @@SERVERNAME, EventTime FROM UserData.dbo.LastUpdate UNION ALL SELECT N'.\PEON1', EventTime FROM [.\PEON1].UserData.dbo.LastUpdate UNION ALL SELECT N'.\PEON2', EventTime FROM [.\PEON2].UserData.dbo.LastUpdate UNION ALL SELECT N'.\PEON3', EventTime FROM [.\PEON3].UserData.dbo.LastUpdate UNION ALL SELECT N'.\PEON4', EventTime FROM [.\PEON4].UserData.dbo.LastUpdate ) SELECT x.ServerInstance, s.IsCurrentStandby, x.EventTime, Age_Minutes = DATEDIFF(MINUTE, x.EventTime, SYSDATETIME()), Age_Seconds = DATEDIFF(SECOND, x.EventTime, SYSDATETIME()) FROM x LEFT OUTER JOIN dbo.PMAG_Secondaries AS s ON s.ServerInstance = x.ServerInstance AND s.DatabaseName = N'UserData'; GO
Contoh hasil akhir pekan:
SELECT [Now] = SYSDATETIME(); SELECT ServerInstance, IsCurrentStandby, EventTime, Age_Minutes, Age_Seconds FROM dbo.PMAG_CompareRecency_UserData ORDER BY Age_Seconds DESC;
Langkah 8 – prosedur pembersihan
Membersihkan riwayat pencadangan dan pemulihan log cukup mudah.
CREATE PROCEDURE dbo.PMAG_CleanupHistory @dbname SYSNAME, @DaysOld INT = 7 AS BEGIN SET NOCOUNT ON; DECLARE @cutoff INT; -- this assumes that a log backup either -- succeeded or failed on all secondaries SELECT @cutoff = MAX(BackupSetID) FROM dbo.PMAG_LogBackupHistory AS bh WHERE DatabaseName = @dbname AND BackupTime < DATEADD(DAY, -@DaysOld, SYSDATETIME()) AND EXISTS ( SELECT 1 FROM dbo.PMAG_LogRestoreHistory AS rh WHERE BackupSetID = bh.BackupSetID AND DatabaseName = @dbname AND ServerInstance = bh.ServerInstance ); DELETE dbo.PMAG_LogRestoreHistory WHERE DatabaseName = @dbname AND BackupSetID <= @cutoff; DELETE dbo.PMAG_LogBackupHistory WHERE DatabaseName = @dbname AND BackupSetID <= @cutoff; END GO
Sekarang, Anda dapat menambahkannya sebagai langkah dalam pekerjaan yang ada, atau Anda dapat menjadwalkannya sepenuhnya secara terpisah atau sebagai bagian dari rutinitas pembersihan lainnya.
Saya akan meninggalkan pembersihan sistem file untuk posting lain (dan mungkin mekanisme yang terpisah sama sekali, seperti PowerShell atau C# – ini biasanya bukan hal yang Anda ingin T-SQL lakukan).
Langkah 9 – tingkatkan solusi
Memang benar bahwa mungkin ada penanganan kesalahan yang lebih baik dan basa-basi lainnya di sini untuk membuat solusi ini lebih lengkap. Untuk saat ini saya akan meninggalkannya sebagai latihan untuk pembaca, tetapi saya berencana untuk melihat posting tindak lanjut untuk merinci peningkatan dan penyempurnaan solusi ini.
Variabel dan batasan
Perhatikan bahwa dalam kasus saya, saya menggunakan Edisi Standar sebagai yang utama, dan Edisi Ekspres untuk semua sekunder. Anda dapat melangkah lebih jauh dalam skala anggaran dan bahkan menggunakan Edisi Ekspres sebagai yang utama – banyak orang berpikir Edisi Ekspres tidak mendukung pengiriman log, padahal sebenarnya itu hanyalah panduan yang tidak ada di versi Management Studio Express sebelum SQL Server 2012 Service Pack 1. Karena itu, karena Express Edition tidak mendukung SQL Server Agent, akan sulit untuk menjadikannya penerbit dalam skenario ini – Anda harus mengonfigurasi penjadwal Anda sendiri untuk memanggil prosedur tersimpan (C# aplikasi baris perintah yang dijalankan oleh Penjadwal Tugas Windows, pekerjaan PowerShell, atau pekerjaan Agen Server SQL pada contoh lain). Untuk menggunakan Express di kedua sisi, Anda juga harus yakin bahwa file data Anda tidak akan melebihi 10 GB, dan kueri Anda akan berfungsi dengan baik dengan memori, CPU, dan batasan fitur edisi tersebut. Saya sama sekali tidak menyarankan bahwa Express ideal; Saya hanya menggunakannya untuk menunjukkan bahwa adalah mungkin untuk memiliki sekunder yang sangat fleksibel dan dapat dibaca secara gratis (atau sangat dekat dengannya).
Selain itu, instance terpisah ini dalam skenario saya semuanya hidup di VM yang sama, tetapi tidak harus bekerja seperti itu sama sekali – Anda dapat menyebarkan instance ke beberapa server; atau, Anda bisa pergi ke arah lain, dan mengembalikan ke salinan database yang berbeda, dengan nama yang berbeda, pada contoh yang sama. Konfigurasi ini membutuhkan sedikit perubahan pada apa yang telah saya paparkan di atas. Dan berapa banyak basis data yang Anda pulihkan, dan seberapa sering, sepenuhnya terserah Anda – meskipun akan ada batas atas praktis (di mana [average query time] > [number of secondaries] x [log backup interval]
).
Akhirnya, pasti ada beberapa keterbatasan dengan pendekatan ini. Daftar yang tidak lengkap:
- Meskipun Anda dapat terus melakukan pencadangan penuh sesuai jadwal Anda sendiri, pencadangan log harus berfungsi sebagai satu-satunya mekanisme pencadangan log Anda. Jika Anda perlu menyimpan cadangan log untuk tujuan lain, Anda tidak akan dapat mencadangkan log secara terpisah dari solusi ini, karena akan mengganggu rantai log. Sebagai gantinya, Anda dapat mempertimbangkan untuk menambahkan
MIRROR TO
additional tambahan arguments to the existing log backup scripts, if you need to have copies of the logs used elsewhere. - While "Poor Man's Availability Groups" may seem like a clever name, it can also be a bit misleading. This solution certainly lacks many of the HA/DR features of Availability Groups, including failover, automatic page repair, and support in the UI, Extended Events and DMVs. This was only meant to provide the ability for non-Enterprise customers to have an infrastructure that supports multiple readable secondaries.
- I tested this on a very isolated VM system with no concurrency. This is not a complete solution and there are likely dozens of ways this code could be made tighter; as a first step, and to focus on the scaffolding and to show you what's possible, I did not build in bulletproof resiliency. You will need to test it at your scale and with your workload to discover your breaking points, and you will also potentially need to deal with transactions over linked servers (always fun) and automating the re-initialization in the event of a disaster.
The "Insurance Policy"
Log shipping also offers a distinct advantage over many other solutions, including Availability Groups, mirroring and replication:a delayed "insurance policy" as I like to call it. At my previous job, I did this with full backups, but you could easily use log shipping to accomplish the same thing:I simply delayed the restores to one of the secondary instances by 24 hours. This way, I was protected from any client "shooting themselves in the foot" going back to yesterday, and I could get to their data easily on the delayed copy, because it was 24 hours behind. (I implemented this the first time a customer ran a delete without a where clause, then called us in a panic, at which point we had to restore their database to a point in time before the delete – which was both tedious and time consuming.) You could easily adapt this solution to treat one of these instances not as a read-only secondary but rather as an insurance policy. More on that perhaps in another post.