sql >> Database >  >> RDS >> Database

Leesbare secundairen met een beperkt budget

Beschikbaarheidsgroepen, geïntroduceerd in SQL Server 2012, vertegenwoordigen een fundamentele verschuiving in de manier waarop we denken over zowel hoge beschikbaarheid als noodherstel voor onze databases. Een van de geweldige dingen die hier mogelijk worden gemaakt, is het offloaden van alleen-lezen bewerkingen naar een secundaire replica, zodat de primaire lees-/schrijfinstantie geen last heeft van vervelende dingen zoals rapportage door eindgebruikers. Dit instellen is niet eenvoudig, maar het is een stuk eenvoudiger en beter te onderhouden dan eerdere oplossingen (steek uw hand op als u mirroring en snapshots wilt instellen, en al het eeuwigdurende onderhoud dat daarmee gepaard gaat).

Mensen worden erg enthousiast als ze horen over Beschikbaarheidsgroepen. Dan slaat de realiteit toe:de functie vereist de Enterprise-editie van SQL Server (in ieder geval vanaf SQL Server 2014). Enterprise Edition is duur, vooral als je veel cores hebt, en vooral sinds de eliminatie van op CAL gebaseerde licenties (tenzij je grootvader was vanaf 2008 R2, in welk geval je beperkt bent tot de eerste 20 cores). Het vereist ook Windows Server Failover Clustering (WSFC), een complicatie niet alleen voor het demonstreren van de technologie op een laptop, maar ook voor de Enterprise Edition van Windows, een domeincontroller en een hele reeks configuraties om clustering te ondersteunen. En er zijn ook nieuwe vereisten rond Software Assurance; een extra kost als u wilt dat uw stand-by-instanties compatibel zijn.

Sommige klanten kunnen de prijs niet rechtvaardigen. Anderen zien de waarde, maar kunnen het simpelweg niet betalen. Dus wat moeten deze gebruikers doen?

Je nieuwe held:verzending loggen

Logboekverzending bestaat al eeuwen. Het is eenvoudig en het werkt gewoon. Bijna altijd. En afgezien van het omzeilen van de licentiekosten en configuratiehindernissen van Beschikbaarheidsgroepen, kan het ook de straf van 14 bytes vermijden waarover Paul Randal (@PaulRandal) sprak in de SQLskills Insider-nieuwsbrief van deze week (13 oktober 2014).

Een van de uitdagingen die mensen hebben met het gebruik van de verzonden kopie van het logboek als een leesbaar secundair, is echter dat je alle huidige gebruikers eruit moet gooien om nieuwe logboeken toe te passen - dus ofwel krijg je gebruikers die geïrriteerd raken omdat ze herhaaldelijk worden verstoord van het uitvoeren van query's, of je hebt gebruikers die geïrriteerd raken omdat hun gegevens oud zijn. Dit komt omdat mensen zich beperken tot een enkele leesbare secundaire.

Het hoeft niet zo te zijn; Ik denk dat hier een gracieuze oplossing is, en hoewel het misschien veel meer beenwerk vooraf vereist dan bijvoorbeeld het inschakelen van Beschikbaarheidsgroepen, zal het voor sommigen zeker een aantrekkelijke optie zijn.

In principe kunnen we een aantal secondaries opzetten, waar we het schip loggen en slechts één van hen de "actieve" secundaire maken, met behulp van een round-robin-benadering. De taak die de logboeken verzendt, weet welke momenteel actief is, dus het herstelt alleen nieuwe logboeken naar de "volgende" server met behulp van de WITH STANDBY optie. De rapportagetoepassing gebruikt dezelfde informatie om tijdens runtime te bepalen wat de verbindingsreeks moet zijn voor het volgende rapport dat de gebruiker uitvoert. Wanneer de volgende logback-up klaar is, verschuift alles met één, en de instantie die nu de nieuwe leesbare secundaire wordt, wordt hersteld met behulp van WITH STANDBY .

Om het model ongecompliceerd te houden, laten we zeggen dat we vier instances hebben die als leesbare secondaries dienen, en dat we elke 15 minuten een logback-up maken. We hebben op elk moment één actieve secundaire modus in de standby-modus, met gegevens die niet ouder zijn dan 15 minuten oud, en drie secondaries in de standby-modus die geen nieuwe zoekopdrachten verwerken (maar mogelijk nog steeds resultaten retourneren voor oudere zoekopdrachten).

Dit werkt het beste als er geen vragen zijn die langer dan 45 minuten duren. (Het is mogelijk dat u deze cycli moet aanpassen, afhankelijk van de aard van uw alleen-lezen bewerkingen, hoeveel gelijktijdige gebruikers langere zoekopdrachten uitvoeren en of het ooit mogelijk is gebruikers te storen door iedereen eruit te schoppen.)

Het werkt ook het beste als opeenvolgende zoekopdrachten die door dezelfde gebruiker worden uitgevoerd, hun verbindingsreeks kunnen wijzigen (dit is logica die in de toepassing moet staan, hoewel u synoniemen of weergaven kunt gebruiken, afhankelijk van de architectuur), en verschillende gegevens bevatten die ondertussen gewijzigd (net alsof ze de live, constant veranderende database doorzoeken).

Met al deze aannames in gedachten, volgt hier een illustratieve reeks gebeurtenissen voor de eerste 75 minuten van onze implementatie:

tijd evenementen visueel
12:00 (t0)
  • Back-uplogboek t0
  • Gebruikers uit instantie A verwijderen
  • Herstel log t0 naar instantie A (STANDBY)
  • Nieuwe alleen-lezen zoekopdrachten gaan naar instantie A
12:15 (t1)
  • Back-uplogboek t1
  • Gebruikers uit instantie B verwijderen
  • Herstel log t0 naar instantie B (NORECOVERY)
  • Herstel log t1 naar instantie B (STANDBY)
  • Nieuwe alleen-lezen zoekopdrachten gaan naar instantie B
  • Bestaande alleen-lezen-query's naar instantie A kunnen worden uitgevoerd, maar ongeveer 15 minuten achter
12:30 (t2)
  • Back-uplogboek t2
  • Gebruikers uit instantie C verwijderen
  • Herstel logs t0 -> t1 naar instantie C (NORECOVERY)
  • Herstel log t2 naar instantie C (STANDBY)
  • Nieuwe alleen-lezen zoekopdrachten gaan naar instantie C
  • Bestaande alleen-lezen-query's naar instanties A en B kunnen doorgaan (15-30 minuten achter)
12:45 (t3)
  • Back-uplogboek t3
  • Gebruikers uit instantie D verwijderen
  • Herstel logs t0 -> t2 naar instantie D (NORECOVERY)
  • Herstel log t3 naar instantie D (STANDBY)
  • Nieuwe alleen-lezen zoekopdrachten gaan naar instantie D
  • Bestaande alleen-lezen-query's naar instanties A, B en C kunnen doorgaan (15-45 minuten achter)
13:00 (t4)
  • Back-uplogboek t4
  • Gebruikers uit instantie A verwijderen
  • Herstel logs t1 -> t3 naar instantie A (NORECOVERY)
  • Herstel log t4 naar instantie A (STANDBY)
  • Nieuwe alleen-lezen zoekopdrachten gaan naar instantie A
  • Bestaande alleen-lezen-query's naar instanties B, C en D kunnen doorgaan (15-45 minuten achter)
  • Query's die nog steeds worden uitgevoerd op instantie A sinds t0 -> ~t1 (45-60 minuten) worden geannuleerd


Dat lijkt misschien eenvoudig genoeg; het schrijven van de code om alles aan te kunnen dat een beetje meer ontmoedigend is. Een ruwe schets:

  1. Op de primaire server (ik noem het BOSS ), maak een database aan. Voordat u er zelfs maar aan denkt om verder te gaan, moet u Trace Flag 3226 inschakelen om te voorkomen dat succesvolle back-upberichten het foutenlogboek van SQL Server vervuilen.
  2. Op BOSS , voeg een gekoppelde server toe voor elke secundaire (ik noem ze PEON1 -> PEON4 ).
  3. Ergens toegankelijk voor alle servers, maak een bestandsshare om database-/logback-ups op te slaan en zorg ervoor dat de serviceaccounts voor elke instantie lees-/schrijftoegang hebben. Ook moet voor elke secundaire instantie een locatie worden opgegeven voor het stand-bybestand.
  4. Maak in een aparte hulpprogrammadatabase (of MSDB, als u dat wilt) tabellen met configuratie-informatie over de database(s), alle secundaire gegevens en log de back-up- en herstelgeschiedenis.
  5. Maak opgeslagen procedures die een back-up van de database maken en terugzetten naar de secundaire bestanden WITH NORECOVERY en pas vervolgens één log toe WITH STANDBY en markeer één instantie als de huidige secundaire secundaire stand-by. Deze procedures kunnen ook worden gebruikt om de verzending van het hele logbestand opnieuw te initialiseren voor het geval er iets misgaat.
  6. Maak een taak die elke 15 minuten wordt uitgevoerd om de hierboven beschreven taken uit te voeren:
    • maak een back-up van het logboek
    • bepaal op welke secundaire back-ups niet-toegepaste logback-ups moeten worden toegepast
    • die logs herstellen met de juiste instellingen
  7. Maak een opgeslagen procedure (en/of een weergave?) die de aanroepende toepassing(en) vertelt welk secundair ze moeten gebruiken voor nieuwe alleen-lezen-query's.
  8. Maak een opschoningsprocedure om de back-upgeschiedenis van logbestanden te wissen voor logbestanden die op alle secundaire bestanden zijn toegepast (en misschien ook om de bestanden zelf te verplaatsen of op te schonen).
  9. Voeg de oplossing aan met robuuste foutafhandeling en meldingen.

Stap 1 – maak een database

Mijn primaire exemplaar is de Standard Edition, genaamd .\BOSS . Op dat moment maak ik een eenvoudige database met één 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();

Vervolgens maak ik een SQL Server Agent-taak die alleen die tijdstempel elke minuut bijwerkt:

UPDATE UserData.dbo.LastUpdate SET EventTime = SYSDATETIME();

Dat creëert gewoon de initiële database en simuleert activiteit, waardoor we kunnen valideren hoe de logverzendingstaak door elk van de leesbare secundairen roteert. Ik wil expliciet stellen dat het doel van deze oefening niet is om de verzending van logbestanden te stressen of om te bewijzen hoeveel volume we kunnen doorprikken; dat is een heel andere oefening.

Stap 2 – voeg gekoppelde servers toe

Ik heb vier secundaire Express Edition-instanties met de naam .\PEON1 , .\PEON2 , .\PEON3 , en .\PEON4 . Dus ik heb deze code vier keer uitgevoerd, en veranderde @s elke keer:

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;

Stap 3 – valideer bestandsshare(s)

In mijn geval bevinden alle 5 instanties zich op dezelfde server, dus ik heb zojuist een map voor elke instantie gemaakt:C:\temp\Peon1\ , C:\temp\Peon2\ , enzovoort. Onthoud dat als uw secundaire servers zich op verschillende servers bevinden, de locatie relatief moet zijn ten opzichte van die server, maar nog steeds toegankelijk moet zijn vanaf de primaire server (meestal wordt er dus een UNC-pad gebruikt). U moet valideren dat elke instantie naar die share kan schrijven, en u moet ook valideren dat elke instantie kan schrijven naar de locatie die is opgegeven voor het stand-bybestand (ik gebruikte dezelfde mappen voor stand-by). U kunt dit valideren door een back-up te maken van een kleine database van elke instantie naar elk van de opgegeven locaties - ga niet verder totdat dit werkt.

Stap 4 – tabellen maken

Ik heb besloten om deze gegevens in msdb te plaatsen , maar ik heb niet echt sterke gevoelens voor of tegen het maken van een aparte database. De eerste tabel die ik nodig heb, is de tabel die informatie bevat over de database(s) die ik ga loggen:

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';

(Als je nieuwsgierig bent naar het naamgevingsschema, PMAG staat voor "Poor Man's Availability Groups.")

Een andere vereiste tabel is een tabel met informatie over de secundairen, inclusief hun individuele mappen en hun huidige status in de verzendvolgorde van het logboek.

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)
);

Als u lokaal een back-up wilt maken van de bronserver en de secundaire bestanden op afstand wilt herstellen, of omgekeerd, kunt u CommonFolder splitsen in twee kolommen (BackupFolder en RestoreFolder ), en breng relevante wijzigingen aan in de code (zoveel zullen er niet zijn).

Aangezien ik deze tabel ten minste gedeeltelijk kan vullen op basis van de informatie in sys.servers – profiteren van het feit dat de data / log en andere mappen zijn vernoemd naar de instantienamen:

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]';

Ik heb ook een tabel nodig om individuele logback-ups bij te houden (niet alleen de laatste), omdat ik in veel gevallen meerdere logbestanden achter elkaar moet herstellen. Ik kan deze informatie krijgen van msdb.dbo.backupset , maar het is veel ingewikkelder om dingen als de locatie te krijgen - en ik heb misschien geen controle over andere taken die de back-upgeschiedenis kunnen opschonen.

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)
);

Je zou kunnen denken dat het verspillend is om een ​​rij op te slaan voor elke secundaire, en om de locatie van elke back-up op te slaan, maar dit is voor toekomstbestendigheid - om het geval af te handelen waarin je de CommonFolder voor elke secundaire map verplaatst.

En tot slot een geschiedenis van logboekherstel, zodat ik op elk moment kan zien welke logboeken zijn hersteld en waar, en de hersteltaak kan er zeker van zijn dat alleen logboeken worden hersteld die nog niet zijn hersteld:

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)
);

Stap 5 – initialiseer secundairen

We hebben een opgeslagen procedure nodig die een back-upbestand genereert (en dit spiegelt naar alle locaties die nodig zijn voor verschillende instanties), en we zullen ook één logboek herstellen naar elke secundaire om ze allemaal in stand-by te zetten. Op dit moment zullen ze allemaal beschikbaar zijn voor alleen-lezen query's, maar slechts één zal tegelijkertijd de "huidige" stand-by zijn. Dit is de opgeslagen procedure die zowel volledige back-ups als transactielogboeken zal verwerken; wanneer een volledige back-up wordt gevraagd, en @init is ingesteld op 1, wordt de verzending van logbestanden automatisch opnieuw geïnitialiseerd.

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

Dit roept op zijn beurt twee procedures op die u afzonderlijk zou kunnen aanroepen (maar hoogstwaarschijnlijk niet). Ten eerste, de procedure die de secondaries initialiseert bij de eerste run:

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

En dan de procedure die de logs zal herstellen:

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

(Ik weet dat het veel code is en veel cryptische dynamische SQL. Ik heb geprobeerd heel liberaal te zijn met opmerkingen; als er een onderdeel is waar je problemen mee hebt, laat het me dan weten.)

Het enige wat u nu hoeft te doen om het systeem in gebruik te nemen, is twee procedureaanroepen te doen:

EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'bak', @init = 1;
EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';

Nu zou u elk exemplaar met een standby-kopie van de database moeten zien:

En u kunt zien welke momenteel als alleen-lezen stand-by moet dienen:

SELECT ServerInstance, IsCurrentStandby
  FROM dbo.PMAG_Secondaries 
  WHERE DatabaseName = N'UserData';

Stap 6 – maak een taak die een back-up maakt van logbestanden en deze herstelt

U kunt deze opdracht in een taak plaatsen die u elke 15 minuten plant:

EXEC dbo.PMAG_Backup @dbname = N'UserData', @type = 'trn';

Hierdoor wordt het actieve secundair elke 15 minuten verplaatst en zijn de gegevens 15 minuten verser dan het vorige actieve secundair. Als u meerdere databases met verschillende schema's hebt, kunt u meerdere taken maken, of de taak vaker plannen en de dbo.PMAG_Databases controleren tabel voor elke individuele LogBackupFrequency_Minutes waarde om te bepalen of u de back-up/herstel voor die database moet uitvoeren.

Stap 7 – weergave en procedure om de applicatie te vertellen welke stand-by actief is

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 mijn geval heb ik ook handmatig een view-unioning gemaakt voor alle UserData databases zodat ik de recentheid van de gegevens op de primaire met elke secundaire kon vergelijken.

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

Voorbeeldresultaten van het weekend:

SELECT [Now] = SYSDATETIME();
 
SELECT ServerInstance, IsCurrentStandby, EventTime, Age_Minutes, Age_Seconds
  FROM dbo.PMAG_CompareRecency_UserData
  ORDER BY Age_Seconds DESC;

Stap 8 – opruimprocedure

Het opschonen van de logback-up en herstelgeschiedenis is vrij eenvoudig.

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

Nu kun je dat toevoegen als een stap in de bestaande taak, of je kunt het volledig apart plannen of als onderdeel van andere opruimroutines.

Ik laat het opschonen van het bestandssysteem over aan een andere post (en waarschijnlijk een apart mechanisme, zoals PowerShell of C# - dit is normaal gesproken niet het soort ding dat je wilt dat T-SQL doet).

Stap 9 – verbeter de oplossing

Het is waar dat er hier een betere foutafhandeling en andere aardigheden kunnen zijn om deze oplossing completer te maken. Voor nu laat ik dat als een oefening voor de lezer, maar ik ben van plan om vervolgposten te bekijken om verbeteringen en verfijningen aan deze oplossing in detail te beschrijven.

Variabelen en beperkingen

Merk op dat ik in mijn geval de standaardeditie als de primaire en de Express-editie voor alle secundairen gebruikte. Je zou een stap verder kunnen gaan op de budgetschaal en zelfs Express Edition als de primaire versie gebruiken - veel mensen denken dat Express Edition geen logverzending ondersteunt, terwijl het in feite slechts de wizard is die niet aanwezig was in versies van Management Studio Express vóór SQL Server 2012 Service Pack 1. Aangezien Express Edition geen SQL Server Agent ondersteunt, zou het in dit scenario moeilijk zijn om er een uitgever van te maken - u zou uw eigen planner moeten configureren om de opgeslagen procedures aan te roepen (C# opdrachtregel-app uitgevoerd door Windows Taakplanner, PowerShell-taken of SQL Server Agent-taken op nog een andere instantie). Om Express aan beide kanten te gebruiken, moet u er ook zeker van zijn dat uw gegevensbestand niet groter zal zijn dan 10 GB, en uw zoekopdrachten zullen prima werken met de geheugen-, CPU- en functiebeperkingen van die editie. Ik suggereer geenszins dat Express ideaal is; Ik heb het alleen gebruikt om aan te tonen dat het mogelijk is om zeer flexibel leesbare secundairen gratis (of heel dichtbij) te hebben.

Ook leven deze afzonderlijke instanties in mijn scenario allemaal op dezelfde VM, maar het hoeft helemaal niet zo te werken - u kunt de instanties over meerdere servers spreiden; of u kunt de andere kant op gaan en herstellen naar verschillende exemplaren van de database, met verschillende namen, op dezelfde instantie. Deze configuraties vereisen minimale wijzigingen in wat ik hierboven heb uiteengezet. En naar hoeveel databases u herstelt en hoe vaak, is geheel aan u – hoewel er een praktische bovengrens zal zijn (waar [average query time] > [number of secondaries] x [log backup interval] ).

Ten slotte zijn er zeker enkele beperkingen aan deze benadering. Een niet-limitatieve lijst:

  1. Hoewel u volledige back-ups kunt blijven maken volgens uw eigen schema, moeten de logback-ups dienen als uw enige back-upmechanisme voor logbestanden. Als u de logback-ups voor andere doeleinden moet opslaan, kunt u geen back-ups maken van logbestanden afzonderlijk van deze oplossing, omdat deze de logketen verstoren. In plaats daarvan kunt u overwegen extra MIRROR TO . toe te voegen 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.


  1. databasebestand kopiëren van /assets naar /data/datamap in bestandsverkenner - Android

  2. Kan ik een binaire tekenreeks opslaan in de CLOB-kolom?

  3. Controleer op mislukte e-mail in SQL Server (T-SQL)

  4. Cloud Vendor Deep-Dive:PostgreSQL op AWS Aurora