sql >> Databasteknik >  >> RDS >> Database

Läsbara sekundärer på en budget

Tillgänglighetsgrupper, introducerade i SQL Server 2012, representerar en grundläggande förändring i hur vi tänker på både hög tillgänglighet och katastrofåterställning för våra databaser. En av de fantastiska sakerna som är möjliga här är att överföra skrivskyddade operationer till en sekundär replik, så att den primära läs/skrivinstansen inte störs av irriterande saker som slutanvändarrapportering. Att ställa in det här är inte enkelt, men det är mycket enklare och mer underhållbart än tidigare lösningar (räcker upp handen om du gillade att ställa in spegling och ögonblicksbilder, och allt det eviga underhållet som det innebär).

Folk blir väldigt glada när de hör om tillgänglighetsgrupper. Då slår verkligheten till:funktionen kräver Enterprise Edition av SQL Server (i alla fall från och med SQL Server 2014). Enterprise Edition är dyrt, särskilt om du har många kärnor, och särskilt sedan elimineringen av CAL-baserad licensiering (såvida du inte var farfar från 2008 R2, i vilket fall du är begränsad till de första 20 kärnorna). Det kräver också Windows Server Failover Clustering (WSFC), en komplikation inte bara för att demonstrera tekniken på en bärbar dator, utan också kräver Enterprise Edition av Windows, en domänkontrollant och en hel massa konfigurationer för att stödja klustring. Och det finns nya krav kring Software Assurance också; en extra kostnad om du vill att dina standby-instanser ska vara kompatibla.

Vissa kunder kan inte motivera priset. Andra ser värdet, men har helt enkelt inte råd. Så vad ska dessa användare göra?

Din nya hjälte:Loggleverans

Leverans av timmer har funnits i evigheter. Det är enkelt och det bara fungerar. Nästan alltid. Och förutom att kringgå licenskostnaderna och konfigurationshinder som presenteras av Availability Groups, kan den också undvika straffen på 14 bytes som Paul Randal (@PaulRandal) pratade om i veckans SQLskills Insider-nyhetsbrev (13 oktober 2014).

En av utmaningarna som folk har med att använda den skickade kopian av loggen som en läsbar sekundär är dock att du måste sparka ut alla nuvarande användare för att kunna använda nya loggar – så antingen har du användare som blir irriterade för att de upprepade gånger störs från att köra frågor, eller så har du användare som blir irriterade för att deras data är inaktuell. Detta beror på att människor begränsar sig till en enda läsbar sekundär.

Det behöver inte vara så; Jag tror att det finns en graciös lösning här, och även om det kan kräva mycket mer benarbete framför än att till exempel slå på tillgänglighetsgrupper, kommer det säkert att vara ett attraktivt alternativ för vissa.

I grund och botten kan vi sätta upp ett antal sekundärer, där vi loggar fartyg och gör bara en av dem till den "aktiva" sekundära, med hjälp av en round-robin-strategi. Jobbet som skickar loggarna vet vilken som är aktiv för närvarande, så det återställer bara nya loggar till "nästa" server med hjälp av WITH STANDBY alternativ. Rapporteringsapplikationen använder samma information för att vid körning avgöra vilken anslutningssträng som ska vara för nästa rapport som användaren kör. När nästa loggsäkerhetskopiering är klar skiftar allt med ett, och instansen som nu blir den nya läsbara sekundära återställs med WITH STANDBY .

För att hålla modellen okomplicerad, låt oss säga att vi har fyra instanser som fungerar som läsbara sekundärer, och vi tar loggbackuper var 15:e minut. När som helst kommer vi att ha en aktiv sekundär i vänteläge, med data som inte är äldre än 15 minuter gamla, och tre sekundärer i vänteläge som inte betjänar nya frågor (men kan fortfarande returnera resultat för äldre frågor).

Detta fungerar bäst om inga frågor förväntas ta längre än 45 minuter. (Du kan behöva justera dessa cykler beroende på arten av dina skrivskyddade operationer, hur många samtidiga användare som kör längre frågor och om det någonsin är möjligt att störa användare genom att kasta ut alla.)

Det fungerar också bäst om på varandra följande frågor som körs av samma användare kan ändra sin anslutningssträng (detta är logik som måste finnas i applikationen, även om du kan använda synonymer eller vyer beroende på arkitekturen), och innehåller olika data som har ändrats under tiden (precis som om de sökte efter den levande, ständigt föränderliga databasen).

Med alla dessa antaganden i åtanke är här en illustrativ händelseförlopp för de första 75 minuterna av vår implementering:

tid händelser visuellt
12:00 (t0)
  • Säkerhetskopieringslogg t0
  • Kicka ut användare från instans A
  • Återställ logg t0 till instans A (STANDBY)
  • Nya skrivskyddade frågor går till instans A
12:15 (t1)
  • Säkerhetskopieringslogg t1
  • Skaka ut användare från instans B
  • Återställ logg t0 till instans B (NORECOVERY)
  • Återställ logg t1 till instans B (STANDBY)
  • Nya skrivskyddade frågor går till instans B
  • Befintliga skrivskyddade frågor till instans A kan fortsätta köras, men ca 15 minuter efter
12:30 (t2)
  • Säkerhetskopieringslogg t2
  • Kicka ut användare från instans C
  • Återställ loggar t0 -> t1 till instans C (NORECOVERY)
  • Återställ logg t2 till instans C (STANDBY)
  • Nya skrivskyddade frågor går till instans C
  • Befintliga skrivskyddade frågor till instanserna A och B kan fortsätta köras (15–30 minuter efter)
12:45 (t3)
  • Säkerhetskopieringslogg t3
  • Kicka ut användare från instans D
  • Återställ loggar t0 -> t2 till instans D (NORECOVERY)
  • Återställ logg t3 till instans D (STANDBY)
  • Nya skrivskyddade frågor går till instans D
  • Befintliga skrivskyddade frågor till instanserna A, B och C kan fortsätta köras (15–45 minuter efter)
13:00 (t4)
  • Säkerhetskopieringslogg t4
  • Kicka ut användare från instans A
  • Återställ loggar t1 -> t3 till instans A (NORECOVERY)
  • Återställ logg t4 till instans A (STANDBY)
  • Nya skrivskyddade frågor går till instans A
  • Befintliga skrivskyddade frågor till instanserna B, C och D kan fortsätta köras (15–45 minuter efter)
  • Frågor som fortfarande körs på instans A sedan t0 -> ~t1 (45-60 minuter) kommer att avbrytas


Det kan tyckas enkelt nog; att skriva koden för att hantera allt som är lite mer skrämmande. En grov översikt:

  1. På den primära servern (jag kallar den BOSS). ), skapa en databas. Innan du ens tänker på att gå vidare, aktivera Trace Flag 3226 för att förhindra att framgångsrika säkerhetskopieringsmeddelanden skräpar ner SQL Servers fellogg.
  2. BOSS , lägg till en länkad server för varje sekundär (jag kallar dem PEON1 -> PEON4 ).
  3. Någonstans som är tillgänglig för alla servrar, skapa en filresurs för att lagra databas-/loggsäkerhetskopior och se till att tjänstkontona för varje instans har läs-/skrivåtkomst. Dessutom måste varje sekundär instans ha en plats specificerad för standby-filen.
  4. I en separat verktygsdatabas (eller MSDB, om du föredrar), skapa tabeller som innehåller konfigurationsinformation om databasen/databaserna, alla sekundärer och logga säkerhetskopiering och återställningshistorik.
  5. Skapa lagrade procedurer som kommer att säkerhetskopiera databasen och återställa till sekundärerna WITH NORRECOVERY , och använd sedan en logg Med STANDBY , och markera en instans som sekundär för aktuell standby. Dessa procedurer kan också användas för att återinitiera hela loggförsändningsinställningen i händelse av att något går fel.
  6. Skapa ett jobb som körs var 15:e minut för att utföra de uppgifter som beskrivs ovan:
    • säkerhetskopiera loggen
    • bestäm vilken sekundär som eventuella otillämpade loggsäkerhetskopior ska tillämpas på
    • återställ loggarna med lämpliga inställningar
  7. Skapa en lagrad procedur (och/eller en vy?) som talar om för de anropande applikationerna vilken sekundär de ska använda för nya skrivskyddade frågor.
  8. Skapa en rensningsprocedur för att rensa bort loggbackuphistorik för loggar som har tillämpats på alla sekundärer (och kanske också för att flytta eller rensa själva filerna).
  9. Utöka lösningen med robust felhantering och aviseringar.

Steg 1 – skapa en databas

Min primära instans är Standard Edition, som heter .\BOSS . På den instansen skapar jag en enkel databas med en tabell:

ANVÄND [master];GOCREATE DATABASE UserData;GOALTER DATABASE UserData SET RECOVERY FULL;GOUSE UserData;GOCREATE TABLE dbo.LastUpdate(EventTime DATETIME2);INSERT dbo.LastUpdate(EventTime) SELECT SYSDATETIME();

Sedan skapar jag ett SQL Server Agent-jobb som bara uppdaterar den tidsstämpeln varje minut:

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

Det skapar bara den initiala databasen och simulerar aktivitet, vilket gör att vi kan validera hur loggsändningsuppgiften roterar genom var och en av de läsbara sekundärerna. Jag vill uttryckligen säga att poängen med den här övningen inte är att stresstesta stockleverans eller att bevisa hur mycket volym vi kan slå igenom; det är en helt annan övning.

Steg 2 – lägg till länkade servrar

Jag har fyra sekundära Express Edition-instanser som heter .\PEON1 , .\PEON2 , .\PEON3 , och .\PEON4 . Så jag körde den här koden fyra gånger och ändrade @s varje gång:

ANVÄND [master];GODECLARE @s NVARCHAR(128) =N'.\PEON1', -- upprepa för .\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'kollationskompatibel', @optvalue =@t;EXEC [master].dbo.sp_serveroption @server =@s, @optname =N'dataåtkomst', @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;

Steg 3 – validera fildelning(ar)

I mitt fall finns alla 5 instanser på samma server, så jag skapade bara en mapp för varje instans:C:\temp\Peon1\ , C:\temp\Peon2\ , och så vidare. Kom ihåg att om dina sekundära servrar finns på olika servrar, bör platsen vara relativt den servern, men fortfarande vara tillgänglig från den primära (så vanligtvis skulle en UNC-sökväg användas). Du bör validera att varje instans kan skriva till den resursen, och du bör också validera att varje instans kan skriva till den plats som anges för väntelägesfilen (jag använde samma mappar för vänteläge). Du kan validera detta genom att säkerhetskopiera en liten databas från varje instans till var och en av dess angivna platser – fortsätt inte förrän detta fungerar.

Steg 4 – skapa tabeller

Jag bestämde mig för att placera denna data i msdb , men jag har egentligen inga starka känslor för eller emot att skapa en separat databas. Den första tabellen jag behöver är den som innehåller information om databasen/databaserna jag ska logga:

SKAPA TABELL dbo.PMAG_Databaser( DatabaseName SYSNAME, LogBackupFrequency_Minutes SMALLINT INTE NULL DEFAULT (15), BEGRÄNSNING PK_DBS PRIMÄRNYCKEL(Databasnamn));GO INSERT dbo.PMAG_Databases(DatabaseName; 

(Om du är nyfiken på namnschemat står PMAG för "Poor Man's Availability Groups.")

En annan tabell som krävs är en för att hålla information om sekundärerna, inklusive deras individuella mappar och deras aktuella status i loggförsändningssekvensen.

CREATE TABLE dbo.PMAG_Secondaries( DatabaseName SYSNAME, ServerInstance SYSNAME, CommonFolder VARCHAR(512) NOT NULL, DataFolder VARCHAR(512) NOT NULL, LogFolder VARCHAR(512) NOT NULL, Standby512VARCHIT) NOT NULL NULL DEFAULT 0, CONSTRAINT PK_Sec PRIMARY KEY(DatabasName, ServerInstance), CONSTRAINT FK_Sec_DBs FOREIGN KEY(DatabasName) REFERENCES dbo.PMAG_Databases(DatabaseName));

Om du vill säkerhetskopiera från källservern lokalt och få sekundärerna att återställa på distans, eller vice versa, kan du dela CommonFolder i två kolumner (BackupFolder och RestoreFolder ), och gör relevanta ändringar i koden (det blir inte så många).

Eftersom jag kan fylla i den här tabellen åtminstone delvis baserat på informationen i sys.servers – dra fördel av det faktum att data/logg och andra mappar är namngivna efter instansnamnen:

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(namn, 1) + '\MSSQL\DATA\', LogFolder ='C:\Program Files\Microsoft SQL Server\ MSSQL12.PEON' + RIGHT(name, 1) + '\MSSQL\DATA\', StandByLocation ='C:\temp\Peon' + RIGHT(name, 1) + '\' FRÅN sys.servers WHERE name LIKE N' .\PEON[1-4]';

Jag behöver också en tabell för att spåra enskilda loggbackuper (inte bara den sista), eftersom jag i många fall behöver återställa flera loggfiler i en sekvens. Jag kan få den här informationen från msdb.dbo.backupset , men det är mycket mer komplicerat att få saker som platsen – och jag kanske inte har kontroll över andra jobb som kan rensa upp säkerhetskopieringshistoriken.

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_LBEYS FK_LBH_DBs UTLÄNDLIG KEY(Databasnamn) REFERENCER dbo.PMAG_Databases(Databasnamn), CONSTRAINT FK_LBH_Sec UTLÄNDSK KEY(Databasnamn, ServerInstans) REFERENCER dbo.PMAG_Secondaries(Databasnamn, ServerInstance));

Du kanske tycker att det är slösaktigt att lagra en rad för varje sekundär och att lagra platsen för varje säkerhetskopia, men detta är för framtidssäkring – för att hantera fallet där du flyttar CommonFolder för en sekundär.

Och äntligen återställs en historik med loggåterställningar så jag kan när som helst se vilka loggar som har återställts och var, och återställningsjobbet kan vara säker på att bara återställa loggar som inte redan har återställts:

SKAPA TABELL dbo.PMAG_LogRestoreHistory( DatabaseName SYSNAME, ServerInstance SYSNAME, BackupSetID INT, RestoreTime DATETIME, CONSTRAINT PK_LRH PRIMARY KEY(DatabaseName, ServerInstance, BackupSetID), CONSTRAINT FK_LRH_EYDBs FORaIFaCNH_Datum REFERENCHReData. UTLÄNDSK KEY(Databasnamn, ServerInstans) REFERENCER dbo.PMAG_Secondaries(Databasnamn, ServerInstans));

Steg 5 – initiera sekundärer

Vi behöver en lagrad procedur som genererar en säkerhetskopia (och speglar den till alla platser som krävs av olika instanser), och vi kommer också att återställa en logg till varje sekundär för att sätta dem alla i standby. Vid det här laget kommer de alla att vara tillgängliga för skrivskyddade frågor, men endast en kommer att vara "aktuell" standby vid en viss tidpunkt. Detta är den lagrade proceduren som kommer att hantera både fullständiga säkerhetskopior och transaktionsloggar; när en fullständig säkerhetskopia begärs, och @init är inställd på 1, återinitierar den automatiskt loggsändningen.

SKAPA PROCEDUR [dbo].[PMAG_Backup] @dbname SYSNAME, @typ CHAR(3) ='bak', -- eller 'trn' @init BIT =0 -- används endast med 'bak'AS BEGIN SET NOCOUNT ON; -- generera ett filnamnsmönster DECLARE @now DATETIME =SYSDATETIME(); DECLARE @fn NVARCHAR(256) =@dbname + N'_' + CONVERT(CHAR(8), @nu, 112) + RIGHT(REPLICATE('0',6) + CONVERT(VARCHAR(32), DATEDIFF(SECOND) , KONVERTERA(DATUM, @nu), @nu)), 6) + N'.' + @typ; -- generera ett backup-kommando med MIRROR TO för varje distinkt CommonFolder DECLARE @sql NVARCHAR(MAX) =N'BACKUP' + CASE @typ NÄR 'bak' DÅ N' DATABASE ' ELSE N' LOG ' END + QUOTENAME(@dbname) + ' ' + STUFF( (SELECT DISTINCT CHAR(13) + CHAR(10) + N' MIRROR TO DISK =''' + s.CommonFolder + @fn + '''' FRÅN dbo.PMAG_Secondaries AS s WHERE s.DatabaseName =@dbname FÖR XML PATH(''), TYPE).value(N'.[1]',N'nvarchar(max)'),1,9,N'') + N' MED NAMN =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') SEN N', KOMPRESSION;' ANNAT N';' SLUTET; EXEC [master].sys.sp_executesql @sql; OM @type ='bak' OCH @init =1 -- initiera loggsändning BÖRJA EXEC dbo.PMAG_InitializeSecondaries @dbname =@dbname, @fn =@fn; END IF @type ='trn' BEGIN -- registrera det faktum att vi säkerhetskopierade en logg INSERT dbo.PMAG_LogBackupHistory ( DatabaseName, ServerInstance, BackupSetID, Location ) SELECT DatabaseName =@dbname, ServerInstance =s.ServerInstance, BackupSetID =MAX(b .backup_set_id), Location =s.CommonFolder + @fn FRÅN msdb.dbo.backupset AS b CROSS JOIN dbo.PMAG_Secondaries AS s WHERE b.name =@dbname + N'_PMAGLog' OCH s.DatabaseName =@dbname GROUP BY s. ServerInstance, s.CommonFolder + @fn; -- när vi väl har säkerhetskopierat loggar, -- återställ dem i nästa sekundära EXEC dbo.PMAG_RestoreLogs @dbname =@dbname; SLUT

Detta kallar i sin tur två procedurer som du kan anropa separat (men troligen inte kommer att göra det). Först, proceduren som initierar sekundärerna vid första körningen:

ÄNDRINGSPROCEDUR dbo.PMAG_InitializeSecondaries @dbname SYSNAME, @fn VARCHAR(512)AS BÖRJAN STÄLLA IN NOCOUNT ON; -- rensa bort befintlig historik/inställningar (eftersom detta kan vara en återinitiering) DELETE dbo.PMAG_LogBackupHistory WHERE DatabaseName =@dbname; DELETE dbo.PMAG_LogRestoreHistory WHERE DatabaseName =@dbname; UPPDATERA dbo.PMAG_Secondaries SET IsCurrentStandby =0 WHERE DatabaseName =@dbname; DECLARE @sql NVARCHAR(MAX) =N'', @filer NVARCHAR(MAX) =N''; -- behöver känna till de logiska filnamnen - kan vara fler än två SET @sql =N'SELECT @files =(SELECT N'', MOVE N''''''' + name + ''''''' TO N ''''$'' + CASE [typ] WHEN 0 THEN N''df'' WHEN 1 THEN N''lf'' END + ''$'''''' FRÅN ' + QUOTENAME(@dbname) + '.sys.database_files WHERE [skriv] IN (0,1) FÖR XML PATH, TYPE).value(N''.[1]'',N''nvarchar(max)'');'; EXEC master.sys.sp_executesql @sql, N'@filer NVARCHAR(MAX) OUTPUT', @filer =@filer OUTPUT; SET @sql =N''; -- återställ - behöver fysiska sökvägar för data/loggfiler för MED FLYTT -- detta kan uppenbarligen misslyckas om dessa sökvägar+namn redan finns för en annan db SELECT @sql +=N'EXEC ' + QUOTENAME(ServerInstance) + N' .master.sys.sp_executesql N''RESTORE DATABASE ' + QUOTENAME(@dbname) + N' FRÅN DISK =N''''' + CommonFolder + @fn + N'''''' + N' MED ERSÄTTNING, NORÅTERHÄLLNING ' + REPLACE(REPLACE(REPLACE(@files, N'$df$', DataFolder + @dbname + N'.mdf'), N'$lf$', LogFolder + @dbname + N'.ldf'), N '''', N'''''') + N';'';' + CHAR(13) + CHAR(10) FRÅN dbo.PMAG_Secondaries WHERE DatabaseName =@dbname; EXEC [master].sys.sp_executesql @sql; -- säkerhetskopiera en logg för denna databas EXEC dbo.PMAG_Backup @dbname =@dbname, @type ='trn'; -- återställ loggar EXEC dbo.PMAG_RestoreLogs @dbname =@dbname, @PrepareAll =1;END

Och sedan proceduren som återställer loggarna:

SKAPA PROCEDUR dbo.PMAG_RestoreLogs @dbname SYSNAME, @PrepareAll BIT =0AS BEGIN SET NOCOUNT ON; DECLARE @StandbyInstance SYSNAME, @CurrentInstance SYSNAME, @BackupSetID INT, @Location VARCHAR(512), @StandByLocation VARCHAR(512), @sql NVARCHAR(MAX), @rn INT; -- hämta "nästa" standby-instans SELECT @StandbyInstance =MIN(ServerInstance) FROM dbo.PMAG_Secondaries WHERE IsCurrentStandby =0 OCH ServerInstance> (SELECT ServerInstance FROM dbo.PMAG_Secondaries WHERE IsCurrentStandBy =1); OM @StandbyInstance ÄR NULL -- antingen var det sist eller en återinit BEGIN SELECT @StandbyInstance =MIN(ServerInstance) FROM dbo.PMAG_Secondaries; END -- få den instansen upp och in i STANDBY -- för varje inloggning logbackuphistory inte i logrestorehistory:-- återställ, och infoga den i logrestorehistory -- markera den sista som STANDBY -- om @prepareAll är sant, markera alla andra som NORECOVERY -- i det här fallet ska det bara finnas en, men bara i fall DECLARE c CURSOR LOCAL FAST_FORWARD FÖR SELECT bh.BackupSetID, s.ServerInstance, bh.Location, s.StandbyLocation, rn =ROW_NUMBER() OVER (PARTITION BY s. ServerInstance BESTÄLLNING AV bh.BackupSetID DESC) FRÅN dbo.PMAG_LogBackupHistory AS bh INNER JOIN dbo.PMAG_Secondaries AS s ON bh.DatabaseName =s.DatabaseName AND bh.ServerInstance =s.ServerInstance.DatadSInstance WHEREInstance WHEREBasInstance WHERE @PrepareAll WHEN 1 THEN s.ServerInstance ELSE @StandbyInstance END AND NOT EXISTS (VÄLJ 1 FRÅN dbo.PMAG_LogRestoreHistory AS rh WHERE DatabaseName =@dbname AND ServerInstance =s.ServerInstance AND BackupBetID =bh. ackupSetID ) ORDER BY CASE s.ServerInstance WHEN @StandbyInstance THEN 1 ELSE 2 END, bh.BackupSetID; ÖPPEN c; HÄMTA c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn; WHILE @@FETCH_STATUS -1 BEGIN -- sparka ut användare - ställ in till single_user och sedan tillbaka till multi SET @sql =N'EXEC ' + QUOTENAME(@CurrentInstance) + N'.[master].sys.sp_executesql ' + 'N' 'OM FINNS (VÄLJ 1 FRÅN sys.databases WHERE name =N''''' + @dbname + ''''' OCH [state] 1) BÖRJA ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET SINGLE_USER ' + N'MED ROLLBACK OMEDELBART; ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET MULTI_USER; SLUTET;'';'; EXEC [master].sys.sp_executesql @sql; -- återställ loggen (i STANDBY om det är den sista):SET @sql =N'EXEC ' + QUOTENAME(@CurrentInstance) + N'.[master].sys.sp_executesql ' + N'N''RESTORE LOG ' + QUOTENAME(@dbname) + N' FROM DISK =N''''' + @Location + N''''' MED ' + CASE NÄR @rn =1 OCH (@CurrentInstance =@StandbyInstance ELLER @PrepareAll =1) THEN N'STANDBY =N''''' + @StandbyLocation + @dbname + N'.standby''''' ELSE N'NORECOVERY' END + N';'';'; EXEC [master].sys.sp_executesql @sql; -- registrera det faktum att vi har återställt loggar INSERT dbo.PMAG_LogRestoreHistory (DatabaseName, ServerInstance, BackupSetID, RestoreTime) SELECT @dbname, @CurrentInstance, @BackupSetID, SYSDATETIME(); -- markera det nya standbyläget IF @rn =1 OCH @CurrentInstance =@StandbyInstance -- detta är den nya STANDBY BEGIN UPDATE dbo.PMAG_Secondaries SET IsCurrentStandby =CASE ServerInstance WHEN @StandbyInstance THEN 1 ELSE 0 END WHERE DatabaseName =@dbname; AVSLUTA HÄMTA c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn; AVSLUTA STÄNG c; DEALLOCATE c;END

(Jag vet att det är mycket kod och mycket kryptisk dynamisk SQL. Jag försökte vara väldigt liberal med kommentarer; om det är något du har problem med, vänligen meddela mig.)

Så nu, allt du behöver göra för att få systemet igång är att göra två proceduranrop:

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

Nu bör du se varje instans med en standby-kopia av databasen:

Och du kan se vilken som för närvarande ska fungera som skrivskyddad standby:

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

Steg 6 – skapa ett jobb som säkerhetskopierar/återställer loggar

Du kan lägga detta kommando i ett jobb som du schemalägger för var 15:e minut:

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

Detta kommer att flytta den aktiva sekundären var 15:e minut, och dess data kommer att vara 15 minuter färskare än den tidigare aktiva sekundären. Om du har flera databaser på olika scheman kan du skapa flera jobb eller schemalägga jobbet oftare och kontrollera dbo.PMAG_Databaser tabell för varje enskild LogBackupFrequency_Minutes värde för att avgöra om du ska köra säkerhetskopieringen/återställningen för den databasen.

Steg 7 – visa och procedur för att tala om för applikationen vilket standbyläge som är aktivt

SKAPA VY dbo.PMAG_ActiveSecondariesAS SELECT DatabaseName, ServerInstance FRÅN dbo.PMAG_Secondaries WHERE IsCurrentStandby =1;GO SKAPA PROCEDUR dbo.PMAG_GetActiveSecondary @dbname SYSNAMEASBEGIN SET NOCOUNT ON; VÄLJ ServerInstance FRÅN dbo.PMAG_ActiveSecondaries WHERE DatabaseName =@dbname;ENDGO

I mitt fall skapade jag också manuellt en vysammanslutning över alla UserData databaser så att jag kunde jämföra aktualiteten av data på den primära med varje sekundär.

SKAPA VY dbo.PMAG_CompareRecency_UserDataAS MED x(ServerInstance, EventTime) AS (VÄLJ @@SERVERNAME, EventTime FRÅN UserData.dbo.LastUpdate UNION ALLA VÄLJ N'.\PEON1', EventTime FRÅN [.\PEON1].dboData .LastUpdate UNION ALL SELECT N'.\PEON2', EventTime FRÅN [.\PEON2].UserData.dbo.LastUpdate UNION ALL SELECT N'.\PEON3', EventTime FRÅN [.\PEON3].UserData.dbo.LastUpdate UNION ALL VÄLJ N'.\PEON4', EventTime FROM [.\PEON4].UserData.dbo.LastUpdate ) VÄLJ x.ServerInstance, s.IsCurrentStandby, x.EventTime, Age_Minutes =DATEDIFF(MINUTE, x.EventTime, SYSDATETIME()), Age_Seconds =DATEDIFF(SECOND, x.EventTime, SYSDATETIME()) FRÅN x VÄNSTER YTTRE JOIN dbo.PMAG_Secondaries AS s ON s.ServerInstance =x.ServerInstance AND s.DatabaseName =N'UserData';GO

Provresultat från helgen:

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

Steg 8 – rengöringsprocedur

Det är ganska enkelt att rensa upp loggsäkerhetskopieringen och återställningshistoriken.

SKAPA PROCEDUR dbo.PMAG_CleanupHistory @dbname SYSNAME, @DaysOld INT =7AS BÖRJAN STÄLL IN NOCOUNT ON; DECLARE @cutoff INT; -- detta förutsätter att en loggsäkerhetskopiering antingen -- lyckades eller misslyckades på alla sekundärer VÄLJ @cutoff =MAX(BackupSetID) FROM dbo.PMAG_LogBackupHistory AS bh WHERE DatabaseName =@dbname AND BackupTime  

Nu kan du lägga till det som ett steg i det befintliga jobbet, eller så kan du schemalägga det helt separat eller som en del av andra rensningsrutiner.

Jag lämnar rensningen av filsystemet för ett annat inlägg (och förmodligen en separat mekanism helt och hållet, t.ex. PowerShell eller C# – det här är vanligtvis inte den typ av saker du vill att T-SQL ska göra).

Steg 9 – utöka lösningen

Det är sant att det kan finnas bättre felhantering och andra finesser här för att göra den här lösningen mer komplett. Tills vidare kommer jag att lämna det som en övning för läsaren, men jag planerar att titta på uppföljande inlägg för att detaljera förbättringar och finesser av denna lösning.

Variabler och begränsningar

Observera att i mitt fall använde jag Standard Edition som primär och Express Edition för alla sekundärer. Du kan gå ett steg längre på budgetskalan och till och med använda Express Edition som den primära – många tror att Express Edition inte stöder loggsändning, när det i själva verket bara är guiden som inte fanns i versioner av Management Studio Express före SQL Server 2012 Service Pack 1. Som sagt, eftersom Express Edition inte stöder SQL Server Agent, skulle det vara svårt att göra det till en utgivare i det här scenariot – du skulle behöva konfigurera din egen schemaläggare för att anropa de lagrade procedurerna (C#) kommandoradsapp som körs av Windows Task Scheduler, PowerShell-jobb eller SQL Server Agent-jobb på ännu en instans). För att använda Express i båda ändarna måste du också vara säker på att din datafil inte kommer att överstiga 10 GB, och dina frågor kommer att fungera bra med minne, CPU och funktionsbegränsningar i den utgåvan. Jag menar inte på något sätt att Express är idealiskt; Jag använde det bara för att visa att det är möjligt att ha mycket flexibla läsbara sekundärer gratis (eller väldigt nära det).

Dessutom lever dessa separata instanser i mitt scenario alla på samma virtuella dator, men det behöver inte fungera på det sättet alls – du kan sprida instanserna över flera servrar; eller så kan du gå åt andra hållet och återställa till olika kopior av databasen, med olika namn, på samma instans. Dessa konfigurationer skulle kräva minimala ändringar av vad jag har lagt ut ovan. Och hur många databaser du återställer till, och hur ofta, är helt upp till dig – även om det kommer att finnas en praktisk övre gräns (där [genomsnittlig frågetid]> [antal sekundärer] x [logg backup-intervall] ).

Slutligen finns det definitivt vissa begränsningar med detta tillvägagångssätt. En icke uttömmande lista:

  1. Medan du kan fortsätta att ta fullständiga säkerhetskopior enligt ditt eget schema, måste loggsäkerhetskopiorna fungera som din enda mekanism för säkerhetskopiering av logg. Om du behöver lagra loggbackuperna för andra ändamål kommer du inte att kunna säkerhetskopiera loggar separat från den här lösningen, eftersom de kommer att störa loggkedjan. Istället kan du överväga att lägga till ytterligare MIRROR TO 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. MySQL:Transaktioner vs låsningstabeller

  2. Oracle SQL - Identifiera sekventiella värdeintervall

  3. Vikten av underhåll på MSDB

  4. Nyfiken på de senaste Microsoft Access-funktionerna?