Database
 sql >> Datenbank >  >> RDS >> Database

Lesbare Secondaries mit kleinem Budget

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)
  • Sicherungsprotokoll t0
  • Entfernen Sie Benutzer aus Instanz A
  • Protokoll t0 auf Instanz A (STANDBY) wiederherstellen
  • Neue schreibgeschützte Abfragen gehen an Instanz A
12:15 (t1)
  • Sicherungsprotokoll t1
  • Entfernen Sie Benutzer aus Instanz B
  • Protokoll t0 in Instanz B wiederherstellen (NORECOVERY)
  • Protokoll t1 auf Instanz B (STANDBY) wiederherstellen
  • Neue schreibgeschützte Abfragen werden an Instanz B gesendet
  • Vorhandene schreibgeschützte Abfragen an Instanz A können weiter ausgeführt werden, aber mit ~15 Minuten Verzögerung
12:30 (t2)
  • Sicherungsprotokoll t2
  • Entfernen Sie Benutzer aus Instanz C
  • Protokolle t0 -> t1 in Instanz C wiederherstellen (NORECOVERY)
  • Protokoll t2 in Instanz C (STANDBY) wiederherstellen
  • Neue schreibgeschützte Abfragen gehen an Instanz C
  • Vorhandene schreibgeschützte Abfragen an die Instanzen A und B können weiter ausgeführt werden (15–30 Minuten später)
12:45 (t3)
  • Sicherungsprotokoll t3
  • Entfernen Sie Benutzer aus Instanz D
  • Protokolle t0 -> t2 in Instanz D wiederherstellen (NORECOVERY)
  • Protokoll t3 in Instanz D (STANDBY) wiederherstellen
  • Neue schreibgeschützte Abfragen werden an Instanz D gesendet
  • Vorhandene schreibgeschützte Abfragen an die Instanzen A, B und C können weiter ausgeführt werden (15–45 Minuten später)
13:00 (t4)
  • Sicherungsprotokoll t4
  • Entfernen Sie Benutzer aus Instanz A
  • Protokolle t1 -> t3 auf Instanz A wiederherstellen (NORECOVERY)
  • Protokoll t4 auf Instanz A (STANDBY) wiederherstellen
  • Neue schreibgeschützte Abfragen gehen an Instanz A
  • Vorhandene schreibgeschützte Abfragen an die Instanzen B, C und D können weiter ausgeführt werden (15–45 Minuten später)
  • Abfragen, die seit t0 -> ~t1 (45–60 Minuten) noch auf Instanz A ausgeführt werden, werden abgebrochen


Das mag einfach erscheinen; Den Code zu schreiben, um all das zu handhaben, ist etwas entmutigender. Ein grober Überblick:

  1. 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.
  2. Auf BOSS , fügen Sie einen verknüpften Server für jeden Sekundärserver hinzu (ich nenne ihn PEON1 -> PEON4 ).
  3. 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.
  4. 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.
  5. Erstellen Sie gespeicherte Prozeduren, die die Datenbank sichern und auf den Sekundärdatenbanken WITH NORECOVERY wiederherstellen , und wenden Sie dann ein Protokoll WITH 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.
  6. 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
  7. 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.
  8. 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).
  9. 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:

  1. 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.
  2. 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.
  3. 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.