Verfügbarkeitsgruppen, die in SQL Server 2012 eingeführt wurden, stellen eine grundlegende Veränderung in unserer Denkweise über Hochverfügbarkeit und Notfallwiederherstellung für unsere Datenbanken dar. Eines der großartigen Dinge, die hier ermöglicht werden, ist das Auslagern von schreibgeschützten Vorgängen auf ein sekundäres Replikat, sodass die primäre Lese-/Schreibinstanz nicht durch lästige Dinge wie Endbenutzerberichte gestört wird. Das Einrichten ist nicht einfach, aber viel einfacher und wartungsfreundlicher als frühere Lösungen (heben Sie die Hand, wenn Ihnen das Einrichten von Spiegelungen und Snapshots und all die damit verbundenen ständigen Wartungsarbeiten gefallen haben).
Die Leute sind sehr aufgeregt, wenn sie von Verfügbarkeitsgruppen hören. Dann schlägt die Realität ein:Das Feature erfordert die Enterprise Edition von SQL Server (zumindest ab SQL Server 2014). Die Enterprise Edition ist teuer, insbesondere wenn Sie viele Kerne haben, und insbesondere seit der Abschaffung der CAL-basierten Lizenzierung (es sei denn, Sie wurden von 2008 R2 übernommen, in diesem Fall sind Sie auf die ersten 20 Kerne beschränkt). Es erfordert auch Windows Server Failover Clustering (WSFC), eine Komplikation, die nicht nur für die Demonstration der Technologie auf einem Laptop erforderlich ist, sondern auch die Enterprise Edition von Windows, einen Domänencontroller und eine ganze Reihe von Konfigurationen zur Unterstützung von Clustering erfordert. Und es gibt auch neue Anforderungen rund um Software Assurance; zusätzliche Kosten, wenn Sie möchten, dass Ihre Standby-Instanzen konform sind.
Manche Kunden können den Preis nicht rechtfertigen. Andere sehen den Wert, können es sich aber einfach nicht leisten. Was sollen diese Benutzer also tun?
Dein neuer Held:Holzversand
Rundholzversand gibt es schon seit Ewigkeiten. Es ist einfach und es funktioniert einfach. Fast immer. Und abgesehen von der Umgehung der Lizenzkosten und Konfigurationshürden durch Verfügbarkeitsgruppen kann es auch die 14-Byte-Strafe vermeiden, über die Paul Randal (@PaulRandal) diese Woche im SQLskills Insider-Newsletter (13. Oktober 2014) gesprochen hat.
Eine der Herausforderungen bei der Verwendung der mit dem Protokoll versendeten Kopie als lesbare Sekundärdatei besteht jedoch darin, dass Sie alle aktuellen Benutzer rausschmeißen müssen, um neue Protokolle anzuwenden – also haben Sie entweder Benutzer, die sich ärgern, weil sie wiederholt unterbrochen werden von der Ausführung von Abfragen, oder Sie haben Benutzer, die sich ärgern, weil ihre Daten veraltet sind. Das liegt daran, dass sich die Leute auf einen einzigen lesbaren Secondary beschränken.
Es muss nicht so sein; Ich denke, es gibt hier eine anmutige Lösung, und obwohl es im Vorfeld viel mehr Beinarbeit erfordern könnte, als beispielsweise das Aktivieren von Verfügbarkeitsgruppen, wird es sicherlich für einige eine attraktive Option sein.
Grundsätzlich können wir eine Reihe von Secondaries einrichten, bei denen wir das Logship durchführen und nur einen von ihnen zum "aktiven" Secondary machen, indem wir einen Round-Robin-Ansatz verwenden. Der Job, der die Logs versendet, weiß, welcher gerade aktiv ist, also stellt er neue Logs nur auf dem "nächsten" Server wieder her, indem er den WITH STANDBY
verwendet Möglichkeit. Die Berichtsanwendung verwendet dieselben Informationen, um zur Laufzeit zu bestimmen, wie die Verbindungszeichenfolge für den nächsten Bericht aussehen soll, den der Benutzer ausführt. Wenn die nächste Log-Sicherung fertig ist, verschiebt sich alles um eins, und die Instanz, die jetzt die neue lesbare sekundäre wird, wird mit WITH STANDBY
wiederhergestellt .
Um das Modell unkompliziert zu halten, nehmen wir an, wir haben vier Instanzen, die als lesbare Sekundärinstanzen dienen, und wir erstellen alle 15 Minuten Protokollsicherungen. Zu jedem Zeitpunkt haben wir einen aktiven sekundären Server im Standby-Modus mit Daten, die nicht älter als 15 Minuten sind, und drei sekundäre Server im Standby-Modus, die keine neuen Abfragen verarbeiten (aber möglicherweise noch Ergebnisse für ältere Abfragen zurückgeben).
Dies funktioniert am besten, wenn keine Abfrage voraussichtlich länger als 45 Minuten dauern wird. (Möglicherweise müssen Sie diese Zyklen abhängig von der Art Ihrer schreibgeschützten Vorgänge anpassen, davon, wie viele gleichzeitige Benutzer längere Abfragen ausführen und ob es jemals möglich ist, Benutzer zu stören, indem Sie alle rausschmeißen.)
Es funktioniert auch am besten, wenn aufeinanderfolgende Abfragen, die von demselben Benutzer ausgeführt werden, ihre Verbindungszeichenfolge ändern können (dies ist eine Logik, die in der Anwendung vorhanden sein muss, obwohl Sie je nach Architektur Synonyme oder Ansichten verwenden könnten) und unterschiedliche Daten enthalten, die vorhanden sind sich in der Zwischenzeit geändert haben (so als würden sie die sich ständig ändernde Live-Datenbank abfragen).
Unter Berücksichtigung all dieser Annahmen finden Sie hier eine beispielhafte Abfolge von Ereignissen für die ersten 75 Minuten unserer Implementierung:
Zeit | Veranstaltungen | visuell |
---|---|---|
12:00 (t0) |
| |
12:15 (t1) |
| |
12:30 (t2) |
| |
12:45 (t3) |
| |
13:00 (t4) |
|
Das mag einfach erscheinen; Den Code zu schreiben, um all das zu handhaben, ist etwas entmutigender. Ein grober Überblick:
- Auf dem primären Server (ich nenne ihn
BOSS
), erstellen Sie eine Datenbank. Bevor Sie auch nur daran denken, weiter zu gehen, schalten Sie das Ablaufverfolgungs-Flag 3226 ein, um zu verhindern, dass Meldungen über erfolgreiche Sicherungen das Fehlerprotokoll von SQL Server verunreinigen. - Auf
BOSS
, fügen Sie einen verknüpften Server für jeden Sekundärserver hinzu (ich nenne ihnPEON1
->PEON4
). - Erstellen Sie an einem für alle Server zugänglichen Ort eine Dateifreigabe zum Speichern von Datenbank-/Protokollsicherungen und stellen Sie sicher, dass die Dienstkonten für jede Instanz Lese-/Schreibzugriff haben. Außerdem muss für jede sekundäre Instanz ein Speicherort für die Standby-Datei angegeben werden.
- Erstellen Sie in einer separaten Dienstprogrammdatenbank (oder MSDB, wenn Sie dies vorziehen) Tabellen, die Konfigurationsinformationen über die Datenbank(en), alle sekundären Datenbanken enthalten und den Sicherungs- und Wiederherstellungsverlauf protokollieren.
- Erstellen Sie gespeicherte Prozeduren, die die Datenbank sichern und auf den Sekundärdatenbanken
WITH NORECOVERY
wiederherstellen , und wenden Sie dann ein ProtokollWITH STANDBY
an , und markieren Sie eine Instanz als aktuelle sekundäre Standby-Instanz. Diese Verfahren können auch verwendet werden, um die gesamte Einrichtung des Protokollversands neu zu initialisieren, falls etwas schief geht. - Erstellen Sie einen Job, der alle 15 Minuten ausgeführt wird, um die oben beschriebenen Aufgaben auszuführen:
- Sichern Sie das Protokoll
- Bestimmen Sie, auf welche Sekundärdatenbank nicht angewendete Protokollsicherungen angewendet werden sollen
- Stellen Sie diese Protokolle mit den entsprechenden Einstellungen wieder her
- Erstellen Sie eine gespeicherte Prozedur (und/oder eine Ansicht?), die den aufrufenden Anwendungen mitteilt, welche sekundären Anwendungen sie für alle neuen schreibgeschützten Abfragen verwenden sollen.
- Erstellen Sie eine Bereinigungsprozedur, um den Protokollsicherungsverlauf für Protokolle zu löschen, die auf alle sekundären Dateien angewendet wurden (und vielleicht auch, um die Dateien selbst zu verschieben oder zu löschen).
- Erweitern Sie die Lösung mit robuster Fehlerbehandlung und Benachrichtigungen.
Schritt 1 – Erstellen Sie eine Datenbank
Meine primäre Instanz ist die Standard Edition mit dem Namen .\BOSS
. Auf dieser Instanz erstelle ich eine einfache Datenbank mit einer Tabelle:
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();
Dann erstelle ich einen SQL Server Agent-Job, der diesen Zeitstempel lediglich jede Minute aktualisiert:
UPDATE UserData.dbo.LastUpdate SET EventTime = SYSDATETIME();
Dadurch wird nur die anfängliche Datenbank erstellt und die Aktivität simuliert, sodass wir überprüfen können, wie die Protokollversandaufgabe durch jede der lesbaren sekundären Datenbanken rotiert. Ich möchte ausdrücklich betonen, dass der Zweck dieser Übung nicht darin besteht, den Protokollversand zu stressen oder zu beweisen, wie viel Volumen wir durchsetzen können; das ist eine ganz andere Übung.
Schritt 2 – Verbindungsserver hinzufügen
Ich habe vier sekundäre Express Edition-Instanzen namens .\PEON1
, .\PEON2
, .\PEON3
, und .\PEON4
. Also habe ich diesen Code viermal ausgeführt und dabei @s
geändert jedes Mal:
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;
Schritt 3 – Dateifreigabe(n) validieren
In meinem Fall befinden sich alle 5 Instanzen auf demselben Server, also habe ich einfach einen Ordner für jede Instanz erstellt:C:\temp\Peon1\
, C:\temp\Peon2\
, und so weiter. Denken Sie daran, dass, wenn sich Ihre sekundären Server auf verschiedenen Servern befinden, der Speicherort relativ zu diesem Server sein sollte, aber dennoch vom primären Server aus zugänglich sein sollte (daher würde normalerweise ein UNC-Pfad verwendet werden). Sie sollten überprüfen, ob jede Instanz in diese Freigabe schreiben kann, und Sie sollten auch überprüfen, ob jede Instanz in den für die Standby-Datei angegebenen Speicherort schreiben kann (ich habe dieselben Ordner für Standby verwendet). Sie können dies überprüfen, indem Sie eine kleine Datenbank von jeder Instanz an jedem der angegebenen Speicherorte sichern – fahren Sie nicht fort, bis dies funktioniert.
Schritt 4 – Tabellen erstellen
Ich habe mich entschieden, diese Daten in msdb
zu platzieren , aber ich habe nicht wirklich starke Gefühle für oder gegen die Erstellung einer separaten Datenbank. Die erste Tabelle, die ich benötige, ist diejenige, die Informationen über die Datenbank(en) enthält, die ich protokollieren werde:
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';
(Wenn Sie neugierig auf das Namensschema sind, PMAG steht für "Poor Man's Availability Groups.")
Eine weitere erforderliche Tabelle enthält Informationen zu den sekundären Servern, einschließlich ihrer einzelnen Ordner und ihres aktuellen Status in der Protokollversandsequenz.
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) );
Wenn Sie vom Quellserver lokal sichern und die sekundären Server remote wiederherstellen möchten oder umgekehrt, können Sie CommonFolder
aufteilen in zwei Spalten (BackupFolder
und RestoreFolder
) und nehmen Sie relevante Änderungen im Code vor (es werden nicht so viele sein).
Da ich diese Tabelle zumindest teilweise basierend auf den Informationen in sys.servers
füllen kann – Ausnutzen der Tatsache, dass die Ordner data / log und andere nach den Instanznamen benannt sind:
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]';
Ich brauche auch eine Tabelle, um einzelne Protokollsicherungen (nicht nur die letzte) zu verfolgen, da ich in vielen Fällen mehrere Protokolldateien nacheinander wiederherstellen muss. Ich kann diese Informationen von msdb.dbo.backupset
erhalten , aber es ist viel komplizierter, Dinge wie den Speicherort zu erhalten – und ich habe möglicherweise keine Kontrolle über andere Jobs, die den Sicherungsverlauf bereinigen können.
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) );
Sie denken vielleicht, dass es verschwenderisch ist, eine Zeile für jede Sekundärdatenbank und den Speicherort jeder Sicherung zu speichern, aber dies dient der Zukunftssicherheit – um den Fall zu handhaben, in dem Sie den CommonFolder für eine Sekundärdatenbank verschieben.
Und schließlich ein Verlauf der Protokollwiederherstellungen, damit ich jederzeit sehen kann, welche Protokolle wo wiederhergestellt wurden, und der Wiederherstellungsjob sicher sein kann, dass nur Protokolle wiederhergestellt werden, die noch nicht wiederhergestellt wurden:
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) );
Schritt 5 – Secondaries initialisieren
Wir benötigen eine gespeicherte Prozedur, die eine Sicherungsdatei generiert (und sie an allen Orten spiegelt, die von verschiedenen Instanzen benötigt werden), und wir werden auch ein Protokoll auf jeder sekundären Datei wiederherstellen, um sie alle in den Standby-Modus zu versetzen. An diesem Punkt stehen sie alle für schreibgeschützte Abfragen zur Verfügung, aber immer nur einer ist der "aktuelle" Standby-Server. Dies ist die gespeicherte Prozedur, die sowohl vollständige als auch Transaktionsprotokollsicherungen verarbeitet. wenn eine vollständige Sicherung angefordert wird, und @init
auf 1 gesetzt ist, wird der Protokollversand automatisch neu initialisiert.
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
Dies wiederum ruft zwei Prozeduren auf, die Sie separat aufrufen könnten (aber höchstwahrscheinlich nicht). Erstens, die Prozedur, die die Secondaries beim ersten Lauf initialisiert:
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
Und dann das Verfahren, das die Protokolle wiederherstellt:
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
(Ich weiß, dass es viel Code und viel kryptisches dynamisches SQL ist. Ich habe versucht, mit Kommentaren sehr großzügig umzugehen; wenn es einen Teil gibt, mit dem Sie Probleme haben, lassen Sie es mich bitte wissen.)
Alles, was Sie jetzt tun müssen, um das System zum Laufen zu bringen, sind zwei Prozeduraufrufe:
EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'bak', @init = 1; EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';
Jetzt sollten Sie jede Instanz mit einer Standby-Kopie der Datenbank sehen:
Und Sie können sehen, welche derzeit als Nur-Lese-Standby dienen soll:
SELECT ServerInstance, IsCurrentStandby FROM dbo.PMAG_Secondaries WHERE DatabaseName = N'UserData';
Schritt 6 – Erstellen Sie einen Job, der Protokolle sichert / wiederherstellt
Sie können diesen Befehl in einen Job einfügen, den Sie alle 15 Minuten einplanen:
EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';
Dadurch wird die aktive Sekundärseite alle 15 Minuten verschoben, und ihre Daten sind 15 Minuten aktueller als die der vorherigen aktiven Sekundärseite. Wenn Sie mehrere Datenbanken mit unterschiedlichen Zeitplänen haben, können Sie mehrere Jobs erstellen oder den Job häufiger planen und die dbo.PMAG_Databases
überprüfen Tabelle für jede einzelne LogBackupFrequency_Minutes
Wert, um zu bestimmen, ob Sie die Sicherung/Wiederherstellung für diese Datenbank ausführen sollten.
Schritt 7 – Ansicht und Verfahren, um der Anwendung mitzuteilen, welches Standby aktiv ist
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
In meinem Fall habe ich auch manuell eine Ansicht erstellt, die alle UserData
vereint Datenbanken, damit ich die Aktualität der Daten auf der Primärseite mit jeder Sekundärseite vergleichen kann.
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
Beispielergebnisse vom Wochenende:
SELECT [Now] = SYSDATETIME(); SELECT ServerInstance, IsCurrentStandby, EventTime, Age_Minutes, Age_Seconds FROM dbo.PMAG_CompareRecency_UserData ORDER BY Age_Seconds DESC;
Schritt 8 – Bereinigungsverfahren
Das Bereinigen des Protokollsicherungs- und Wiederherstellungsverlaufs ist ziemlich einfach.
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
Jetzt können Sie dies als Schritt in den vorhandenen Job einfügen oder es vollständig separat oder als Teil anderer Bereinigungsroutinen planen.
Das Bereinigen des Dateisystems überlasse ich einem anderen Beitrag (und wahrscheinlich einem ganz separaten Mechanismus wie PowerShell oder C# – das ist normalerweise nicht das, was T-SQL tun soll).
Schritt 9 – Erweitern Sie die Lösung
Es stimmt, dass es hier eine bessere Fehlerbehandlung und andere Feinheiten geben könnte, um diese Lösung vollständiger zu machen. Für den Moment überlasse ich das dem Leser als Übung, aber ich plane, mir nachfolgende Posts anzusehen, um Verbesserungen und Verfeinerungen dieser Lösung im Detail zu beschreiben.
Variablen und Einschränkungen
Beachten Sie, dass ich in meinem Fall die Standard Edition als primäre und die Express Edition für alle sekundären verwendet habe. Sie könnten auf der Budgetskala noch einen Schritt weiter gehen und die Express Edition sogar als primäre Version verwenden – viele Leute denken, dass die Express Edition keinen Protokollversand unterstützt, obwohl es sich tatsächlich nur um den Assistenten handelt, der in Versionen von Management Studio nicht vorhanden war Express vor SQL Server 2012 Service Pack 1. Da die Express Edition den SQL Server Agent jedoch nicht unterstützt, wäre es schwierig, sie in diesem Szenario zu einem Herausgeber zu machen – Sie müssten Ihren eigenen Scheduler konfigurieren, um die gespeicherten Prozeduren aufzurufen (C# Befehlszeilen-App, die von Windows Task Scheduler, PowerShell-Jobs oder SQL Server-Agent-Jobs auf einer weiteren Instanz ausgeführt wird). Um Express auf beiden Seiten verwenden zu können, müssen Sie außerdem sicher sein, dass Ihre Datendatei 10 GB nicht überschreitet und Ihre Abfragen mit den Speicher-, CPU- und Funktionsbeschränkungen dieser Edition einwandfrei funktionieren. Ich behaupte keineswegs, dass Express ideal ist; Ich habe es nur verwendet, um zu demonstrieren, dass es möglich ist, sehr flexibel lesbare Secondaries kostenlos (oder sehr nahe daran) zu haben.
Außerdem leben diese separaten Instanzen in meinem Szenario alle auf derselben VM, aber es muss überhaupt nicht so funktionieren – Sie können die Instanzen auf mehrere Server verteilen; oder Sie könnten den anderen Weg gehen und auf verschiedenen Kopien der Datenbank mit unterschiedlichen Namen auf derselben Instanz wiederherstellen. Diese Konfigurationen würden minimale Änderungen an dem, was ich oben dargelegt habe, erfordern. Und wie viele Datenbanken Sie wiederherstellen und wie oft, liegt ganz bei Ihnen – obwohl es eine praktische Obergrenze gibt (wobei [average query time] > [number of secondaries] x [log backup interval]
).
Schließlich gibt es definitiv einige Einschränkungen bei diesem Ansatz. Eine nicht erschöpfende Liste:
- Obwohl Sie weiterhin vollständige Sicherungen nach Ihrem eigenen Zeitplan erstellen können, müssen die Protokollsicherungen als Ihr einziger Protokollsicherungsmechanismus dienen. Wenn Sie die Protokollsicherungen für andere Zwecke speichern müssen, können Sie Protokolle nicht separat von dieser Lösung sichern, da sie die Protokollkette beeinträchtigen. Stattdessen können Sie erwägen, zusätzliches
MIRROR TO
hinzuzufügen 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.