Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Sekunder yang Dapat Dibaca dengan Anggaran

Grup Ketersediaan, yang diperkenalkan di SQL Server 2012, mewakili perubahan mendasar dalam cara kami berpikir tentang ketersediaan tinggi dan pemulihan bencana untuk database kami. Salah satu hal hebat yang dimungkinkan di sini adalah memindahkan operasi baca-saja ke replika sekunder, sehingga instance baca/tulis utama tidak terganggu oleh hal-hal buruk seperti pelaporan pengguna akhir. Menyiapkan ini tidak sederhana, tetapi jauh lebih mudah dan lebih mudah dipelihara daripada solusi sebelumnya (angkat tangan Anda jika Anda suka menyiapkan mirroring dan snapshot, dan semua pemeliharaan terus-menerus yang terlibat dengan itu).

Orang-orang menjadi sangat bersemangat saat mendengar tentang Grup Ketersediaan. Kemudian kenyataan muncul:fitur tersebut memerlukan Edisi Perusahaan dari SQL Server (pada SQL Server 2014, toh). Edisi Perusahaan mahal, terutama jika Anda memiliki banyak inti, dan terutama sejak penghapusan lisensi berbasis CAL (kecuali jika Anda telah menjadi kakek dari 2008 R2, dalam hal ini Anda terbatas pada 20 inti pertama). Ini juga memerlukan Windows Server Failover Clustering (WSFC), komplikasi tidak hanya untuk mendemonstrasikan teknologi pada laptop, tetapi juga membutuhkan Enterprise Edition Windows, pengontrol domain, dan sejumlah besar konfigurasi untuk mendukung pengelompokan. Dan ada persyaratan baru seputar Jaminan Perangkat Lunak juga; biaya tambahan jika Anda ingin instans siaga Anda sesuai.

Beberapa pelanggan tidak dapat membenarkan harga. Orang lain melihat nilainya, tetapi tidak mampu membelinya. Jadi apa yang harus dilakukan para pengguna ini?

Pahlawan Baru Anda:Log Pengiriman

Log pengiriman telah ada selama berabad-abad. Ini sederhana dan itu hanya berfungsi. Hampir selalu. Dan selain melewati biaya lisensi dan rintangan konfigurasi yang disajikan oleh Availability Groups, juga dapat menghindari penalti 14 byte yang dibicarakan oleh Paul Randal (@PaulRandal) dalam buletin SQLskills Insider minggu ini (13 Oktober 2014).

Namun, salah satu tantangan yang dihadapi orang dengan menggunakan salinan yang dikirimkan log sebagai sekunder yang dapat dibaca, adalah Anda harus mengeluarkan semua pengguna saat ini untuk menerapkan log baru – jadi Anda membuat pengguna merasa terganggu karena mereka berulang kali terganggu dari menjalankan kueri, atau Anda membuat pengguna kesal karena data mereka basi. Ini karena orang membatasi diri mereka pada satu sekunder yang dapat dibaca.

Tidak harus seperti itu; Saya pikir ada solusi yang anggun di sini, dan meskipun mungkin memerlukan lebih banyak pekerjaan kaki di depan daripada, katakanlah, mengaktifkan Grup Ketersediaan, itu pasti akan menjadi opsi yang menarik bagi sebagian orang.

Pada dasarnya, kita dapat mengatur sejumlah sekunder, di mana kita akan mencatat kapal dan menjadikan salah satunya sebagai sekunder "aktif", menggunakan pendekatan round-robin. Pekerjaan yang mengirimkan log mengetahui mana yang sedang aktif, sehingga hanya mengembalikan log baru ke server "berikutnya" menggunakan WITH STANDBY pilihan. Aplikasi pelaporan menggunakan informasi yang sama untuk menentukan saat runtime seperti apa string koneksi untuk laporan berikutnya yang dijalankan pengguna. Saat pencadangan log berikutnya sudah siap, semuanya bergeser satu per satu, dan instance yang sekarang akan menjadi sekunder baru yang dapat dibaca akan dipulihkan menggunakan WITH STANDBY .

Agar model tidak rumit, katakanlah kita memiliki empat instance yang berfungsi sebagai sekunder yang dapat dibaca, dan kita mengambil cadangan log setiap 15 menit. Setiap saat, kami akan memiliki satu sekunder aktif dalam mode siaga, dengan data tidak lebih lama dari 15 menit, dan tiga sekunder dalam mode siaga yang tidak melayani kueri baru (tetapi mungkin masih mengembalikan hasil untuk kueri lama).

Ini akan bekerja paling baik jika tidak ada kueri yang diharapkan bertahan lebih dari 45 menit. (Anda mungkin perlu menyesuaikan siklus ini bergantung pada sifat operasi hanya-baca, berapa banyak pengguna serentak yang menjalankan kueri lebih lama, dan apakah mungkin mengganggu pengguna dengan mengeluarkan semua orang.)

Ini juga akan berfungsi paling baik jika kueri berurutan yang dijalankan oleh pengguna yang sama dapat mengubah string koneksi mereka (ini adalah logika yang perlu ada dalam aplikasi, meskipun Anda dapat menggunakan sinonim atau tampilan tergantung pada arsitektur), dan berisi data berbeda yang memiliki berubah sementara itu (seperti jika mereka menanyakan database langsung yang terus berubah).

Dengan mempertimbangkan semua asumsi ini, berikut adalah ilustratif urutan kejadian selama 75 menit pertama implementasi kami:

waktu acara visual
12:00 (t0)
  • Cadangan log t0
  • Mengusir pengguna dari instance A
  • Pulihkan log t0 ke instance A (STANDBY)
  • Kueri hanya-baca baru akan masuk ke instance A
12:15 (t1)
  • Cadangan log t1
  • Mengusir pengguna dari instance B
  • Pulihkan log t0 ke instance B (NORECOVERY)
  • Pulihkan log t1 ke instance B (STANDBY)
  • Kueri hanya-baca baru akan masuk ke instance B
  • Kueri hanya baca yang ada ke instance A dapat terus berjalan, tetapi ~15 menit lebih lambat
12:30 (t2)
  • Cadangan log t2
  • Mengusir pengguna dari instance C
  • Pulihkan log t0 -> t1 ke instance C (NORECOVERY)
  • Pulihkan log t2 ke instance C (STANDBY)
  • Kueri hanya-baca baru akan masuk ke instance C
  • Kueri hanya-baca yang ada untuk instance A &B dapat terus berjalan (15-30 menit di belakang)
12:45 (t3)
  • Cadangan log t3
  • Mengusir pengguna dari instance D
  • Pulihkan log t0 -> t2 ke instance D (NORECOVERY)
  • Pulihkan log t3 ke instance D (STANDBY)
  • Kueri hanya-baca baru akan masuk ke instance D
  • Kueri hanya-baca yang ada untuk instance A, B &C dapat terus berjalan (15-45 menit di belakang)
13:00 (t4)
  • Cadangan log t4
  • Mengusir pengguna dari instance A
  • Pulihkan log t1 -> t3 ke instance A (NORECOVERY)
  • Pulihkan log t4 ke instance A (STANDBY)
  • Kueri hanya-baca baru akan masuk ke instance A
  • Kueri baca-saja yang ada untuk instance B, C &D dapat terus berjalan (15-45 menit di belakang)
  • Kueri yang masih berjalan pada instance A sejak t0 -> ~t1 (45-60 menit) akan dibatalkan


Itu mungkin tampak cukup sederhana; menulis kode untuk menangani semua itu sedikit lebih menakutkan. Garis besar kasar:

  1. Di server utama (saya akan menyebutnya BOSS ), membuat database. Bahkan sebelum berpikir untuk melangkah lebih jauh, aktifkan Trace Flag 3226 untuk mencegah pesan pencadangan yang berhasil mengotori log kesalahan SQL Server.
  2. Di BOSS , tambahkan server tertaut untuk setiap sekunder (saya akan menyebutnya PEON1 -> PEON4 ).
  3. Di suatu tempat yang dapat diakses oleh semua server, buat file yang dibagikan untuk menyimpan cadangan database/log, dan pastikan akun layanan untuk setiap instans memiliki akses baca/tulis. Selain itu, setiap instans sekunder harus memiliki lokasi yang ditentukan untuk file siaga.
  4. Dalam database utilitas terpisah (atau MSDB, jika Anda mau), buat tabel yang akan menyimpan informasi konfigurasi tentang database, semua sekunder, dan log backup dan restore history.
  5. Buat prosedur tersimpan yang akan mencadangkan database dan memulihkan ke WITH NORECOVERY sekunder , lalu terapkan satu log WITH STANDBY , dan tandai satu instans sebagai sekunder siaga saat ini. Prosedur ini juga dapat digunakan untuk menginisialisasi ulang seluruh penyiapan pengiriman log jika terjadi kesalahan.
  6. Buat tugas yang akan berjalan setiap 15 menit, untuk melakukan tugas yang dijelaskan di atas:
    • cadangkan log
    • menentukan sekunder mana yang akan menerapkan pencadangan log yang belum diterapkan
    • pulihkan log tersebut dengan setelan yang sesuai
  7. Buat prosedur tersimpan (dan/atau tampilan?) yang akan memberi tahu aplikasi pemanggil sekunder mana yang harus mereka gunakan untuk kueri hanya-baca baru.
  8. Buat prosedur pembersihan untuk menghapus riwayat pencadangan log untuk log yang telah diterapkan ke semua sekunder (dan mungkin juga untuk memindahkan atau membersihkan file itu sendiri).
  9. Tingkatkan solusi dengan penanganan kesalahan dan pemberitahuan yang kuat.

Langkah 1 – buat database

Instance utama saya adalah Edisi Standar, bernama .\BOSS . Pada contoh itu saya membuat database sederhana dengan satu tabel:

USE [master];
GO
CREATE DATABASE UserData;
GO
ALTER DATABASE UserData SET RECOVERY FULL;
GO
USE UserData;
GO
CREATE TABLE dbo.LastUpdate(EventTime DATETIME2);
INSERT dbo.LastUpdate(EventTime) SELECT SYSDATETIME();

Lalu saya membuat pekerjaan Agen Server SQL yang hanya memperbarui stempel waktu itu setiap menit:

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

Itu hanya membuat database awal dan mensimulasikan aktivitas, memungkinkan kami untuk memvalidasi bagaimana tugas pengiriman log berputar melalui masing-masing sekunder yang dapat dibaca. Saya ingin menyatakan secara eksplisit bahwa inti dari latihan ini bukanlah untuk menekankan pengiriman log uji atau untuk membuktikan berapa banyak volume yang dapat kita tembus; itu adalah latihan yang berbeda sama sekali.

Langkah 2 – tambahkan server tertaut

Saya memiliki empat instans Edisi Ekspres sekunder bernama .\PEON1 , .\PEON2 , .\PEON3 , dan .\PEON4 . Jadi saya menjalankan kode ini empat kali, mengubah @s setiap kali:

USE [master];
GO
DECLARE @s NVARCHAR(128) = N'.\PEON1',  -- repeat for .\PEON2, .\PEON3, .\PEON4
        @t NVARCHAR(128) = N'true';
EXEC [master].dbo.sp_addlinkedserver   @server     = @s, @srvproduct = N'SQL Server';
EXEC [master].dbo.sp_addlinkedsrvlogin @rmtsrvname = @s, @useself = @t;
EXEC [master].dbo.sp_serveroption      @server     = @s, @optname = N'collation compatible', @optvalue = @t;
EXEC [master].dbo.sp_serveroption      @server     = @s, @optname = N'data access',          @optvalue = @t;
EXEC [master].dbo.sp_serveroption      @server     = @s, @optname = N'rpc',                  @optvalue = @t;
EXEC [master].dbo.sp_serveroption      @server     = @s, @optname = N'rpc out',              @optvalue = @t;

Langkah 3 – validasi file yang dibagikan

Dalam kasus saya, semua 5 instance berada di server yang sama, jadi saya baru saja membuat folder untuk setiap instance:C:\temp\Peon1\ , C:\temp\Peon2\ , dan seterusnya. Ingatlah bahwa jika sekunder Anda berada di server yang berbeda, lokasinya harus relatif terhadap server itu, tetapi masih dapat diakses dari server utama (jadi biasanya jalur UNC akan digunakan). Anda harus memvalidasi bahwa setiap instance dapat menulis ke share itu, dan Anda juga harus memvalidasi bahwa setiap instance dapat menulis ke lokasi yang ditentukan untuk file standby (saya menggunakan folder yang sama untuk standby). Anda dapat memvalidasi ini dengan mencadangkan database kecil dari setiap instance ke setiap lokasi yang ditentukan – jangan lanjutkan sampai ini berhasil.

Langkah 4 – buat tabel

Saya memutuskan untuk menempatkan data ini di msdb , tetapi saya tidak memiliki perasaan yang kuat untuk atau menentang pembuatan database terpisah. Tabel pertama yang saya butuhkan adalah tabel yang menyimpan informasi tentang database yang akan saya log pengirimannya:

CREATE TABLE dbo.PMAG_Databases
(
  DatabaseName               SYSNAME,
  LogBackupFrequency_Minutes SMALLINT NOT NULL DEFAULT (15),
  CONSTRAINT PK_DBS PRIMARY KEY(DatabaseName)
);
GO
 
INSERT dbo.PMAG_Databases(DatabaseName) SELECT N'UserData';

(Jika Anda penasaran dengan skema penamaan, PMAG adalah singkatan dari "Kelompok Ketersediaan Orang Miskin.")

Tabel lain yang diperlukan adalah tabel untuk menyimpan informasi tentang sekunder, termasuk folder masing-masing dan statusnya saat ini dalam urutan pengiriman log.

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

Jika Anda ingin mencadangkan dari server sumber secara lokal, dan mengembalikan sekunder dari jarak jauh, atau sebaliknya, Anda dapat membagi CommonFolder menjadi dua kolom (BackupFolder dan RestoreFolder ), dan buat perubahan yang relevan pada kode (tidak akan sebanyak itu).

Karena saya dapat mengisi tabel ini setidaknya sebagian berdasarkan informasi di sys.servers – mengambil keuntungan dari fakta bahwa data / log dan folder lain dinamai dengan nama instance:

INSERT dbo.PMAG_Secondaries
(
  DatabaseName,
  ServerInstance, 
  CommonFolder, 
  DataFolder, 
  LogFolder, 
  StandByLocation
)
SELECT 
  DatabaseName = N'UserData', 
  ServerInstance = name,
  CommonFolder = 'C:\temp\Peon' + RIGHT(name, 1) + '\', 
  DataFolder = 'C:\Program Files\Microsoft SQL Server\MSSQL12.PEON'  
               + RIGHT(name, 1) + '\MSSQL\DATA\',
  LogFolder  = 'C:\Program Files\Microsoft SQL Server\MSSQL12.PEON' 
               + RIGHT(name, 1) + '\MSSQL\DATA\',
  StandByLocation = 'C:\temp\Peon' + RIGHT(name, 1) + '\' 
FROM sys.servers 
WHERE name LIKE N'.\PEON[1-4]';

Saya juga memerlukan tabel untuk melacak cadangan log individual (bukan hanya yang terakhir), karena dalam banyak kasus saya harus memulihkan beberapa file log secara berurutan. Saya bisa mendapatkan informasi ini dari msdb.dbo.backupset , tetapi jauh lebih rumit untuk mendapatkan hal-hal seperti lokasi – dan saya mungkin tidak memiliki kendali atas pekerjaan lain yang mungkin membersihkan riwayat pencadangan.

CREATE TABLE dbo.PMAG_LogBackupHistory
(
  DatabaseName   SYSNAME,
  ServerInstance SYSNAME,
  BackupSetID    INT NOT NULL,
  Location       VARCHAR(2000) NOT NULL,
  BackupTime     DATETIME NOT NULL DEFAULT SYSDATETIME(),
  CONSTRAINT PK_LBH PRIMARY KEY(DatabaseName, ServerInstance, BackupSetID),
  CONSTRAINT FK_LBH_DBs FOREIGN KEY(DatabaseName)
    REFERENCES dbo.PMAG_Databases(DatabaseName),
  CONSTRAINT FK_LBH_Sec FOREIGN KEY(DatabaseName, ServerInstance)
    REFERENCES dbo.PMAG_Secondaries(DatabaseName, ServerInstance)
);

Anda mungkin berpikir menyimpan baris untuk setiap sekunder adalah pemborosan, dan untuk menyimpan lokasi setiap cadangan, tetapi ini untuk pemeriksaan di masa mendatang – untuk menangani kasus di mana Anda memindahkan CommonFolder untuk sekunder mana pun.

Dan akhirnya riwayat pemulihan log sehingga, kapan saja, saya dapat melihat log mana yang telah dipulihkan dan di mana, dan pekerjaan pemulihan dapat dipastikan hanya memulihkan log yang belum dipulihkan:

CREATE TABLE dbo.PMAG_LogRestoreHistory
(
  DatabaseName   SYSNAME,
  ServerInstance SYSNAME,
  BackupSetID    INT,
  RestoreTime    DATETIME,
  CONSTRAINT PK_LRH PRIMARY KEY(DatabaseName, ServerInstance, BackupSetID),
  CONSTRAINT FK_LRH_DBs FOREIGN KEY(DatabaseName)
    REFERENCES dbo.PMAG_Databases(DatabaseName),
  CONSTRAINT FK_LRH_Sec FOREIGN KEY(DatabaseName, ServerInstance)
    REFERENCES dbo.PMAG_Secondaries(DatabaseName, ServerInstance)
);

Langkah 5 – inisialisasi sekunder

Kami memerlukan prosedur tersimpan yang akan menghasilkan file cadangan (dan mencerminkannya ke lokasi mana pun yang diperlukan oleh instance yang berbeda), dan kami juga akan memulihkan satu log ke setiap sekunder untuk menempatkan semuanya dalam keadaan siaga. Pada titik ini semuanya akan tersedia untuk kueri hanya-baca, tetapi hanya satu yang akan menjadi siaga "saat ini" pada satu waktu. Ini adalah prosedur tersimpan yang akan menangani pencadangan log transaksi dan penuh; ketika cadangan lengkap diminta, dan @init disetel ke 1, ini secara otomatis menginisialisasi ulang pengiriman log.

CREATE PROCEDURE [dbo].[PMAG_Backup]
  @dbname SYSNAME,
  @type   CHAR(3) = 'bak', -- or 'trn'
  @init   BIT     = 0 -- only used with 'bak'
AS
BEGIN
  SET NOCOUNT ON;
 
  -- generate a filename pattern
  DECLARE @now DATETIME = SYSDATETIME();
  DECLARE @fn NVARCHAR(256) = @dbname + N'_' + CONVERT(CHAR(8), @now, 112) 
    + RIGHT(REPLICATE('0',6) + CONVERT(VARCHAR(32), DATEDIFF(SECOND, 
      CONVERT(DATE, @now), @now)), 6) + N'.' + @type;
 
  -- generate a backup command with MIRROR TO for each distinct CommonFolder
  DECLARE @sql NVARCHAR(MAX) = N'BACKUP' 
    + CASE @type WHEN 'bak' THEN N' DATABASE ' ELSE N' LOG ' END
    + QUOTENAME(@dbname) + ' 
    ' + STUFF(
        (SELECT DISTINCT CHAR(13) + CHAR(10) + N' MIRROR TO DISK = ''' 
           + s.CommonFolder + @fn + ''''
         FROM dbo.PMAG_Secondaries AS s 
         WHERE s.DatabaseName = @dbname 
         FOR XML PATH(''), TYPE).value(N'.[1]',N'nvarchar(max)'),1,9,N'') + N' 
        WITH NAME = N''' + @dbname + CASE @type 
        WHEN 'bak' THEN N'_PMAGFull' ELSE N'_PMAGLog' END 
        + ''', INIT, FORMAT' + CASE WHEN LEFT(CONVERT(NVARCHAR(128), 
        SERVERPROPERTY(N'Edition')), 3) IN (N'Dev', N'Ent')
        THEN N', COMPRESSION;' ELSE N';' END;
 
  EXEC [master].sys.sp_executesql @sql;
 
  IF @type = 'bak' AND @init = 1  -- initialize log shipping
  BEGIN
    EXEC dbo.PMAG_InitializeSecondaries @dbname = @dbname, @fn = @fn;
  END
 
  IF @type = 'trn'
  BEGIN
    -- record the fact that we backed up a log
    INSERT dbo.PMAG_LogBackupHistory
    (
      DatabaseName, 
      ServerInstance, 
      BackupSetID, 
      Location
    )
    SELECT 
      DatabaseName = @dbname, 
      ServerInstance = s.ServerInstance, 
      BackupSetID = MAX(b.backup_set_id), 
      Location = s.CommonFolder + @fn
    FROM msdb.dbo.backupset AS b
    CROSS JOIN dbo.PMAG_Secondaries AS s
    WHERE b.name = @dbname + N'_PMAGLog'
      AND s.DatabaseName = @dbname
    GROUP BY s.ServerInstance, s.CommonFolder + @fn;
 
    -- once we've backed up logs, 
    -- restore them on the next secondary
    EXEC dbo.PMAG_RestoreLogs @dbname = @dbname;
  END
END

Ini pada gilirannya memanggil dua prosedur yang dapat Anda panggil secara terpisah (tetapi kemungkinan besar tidak). Pertama, prosedur yang akan menginisialisasi sekunder saat pertama dijalankan:

ALTER PROCEDURE dbo.PMAG_InitializeSecondaries
  @dbname SYSNAME,
  @fn     VARCHAR(512)
AS
BEGIN
  SET NOCOUNT ON;
 
  -- clear out existing history/settings (since this may be a re-init)
  DELETE dbo.PMAG_LogBackupHistory  WHERE DatabaseName = @dbname;
  DELETE dbo.PMAG_LogRestoreHistory WHERE DatabaseName = @dbname;
  UPDATE dbo.PMAG_Secondaries SET IsCurrentStandby = 0
    WHERE DatabaseName = @dbname;
 
  DECLARE @sql   NVARCHAR(MAX) = N'',
          @files NVARCHAR(MAX) = N'';
 
  -- need to know the logical file names - may be more than two
  SET @sql = N'SELECT @files = (SELECT N'', MOVE N'''''' + name 
    + '''''' TO N''''$'' + CASE [type] WHEN 0 THEN N''df''
      WHEN 1 THEN N''lf'' END + ''$''''''
    FROM ' + QUOTENAME(@dbname) + '.sys.database_files
    WHERE [type] IN (0,1)
    FOR XML PATH, TYPE).value(N''.[1]'',N''nvarchar(max)'');';
 
  EXEC master.sys.sp_executesql @sql,
    N'@files NVARCHAR(MAX) OUTPUT', 
    @files = @files OUTPUT;
 
  SET @sql = N'';
 
  -- restore - need physical paths of data/log files for WITH MOVE
  -- this can fail, obviously, if those path+names already exist for another db
  SELECT @sql += N'EXEC ' + QUOTENAME(ServerInstance) 
    + N'.master.sys.sp_executesql N''RESTORE DATABASE ' + QUOTENAME(@dbname) 
    + N' FROM DISK = N''''' + CommonFolder + @fn + N'''''' + N' WITH REPLACE, 
      NORECOVERY' + REPLACE(REPLACE(REPLACE(@files, N'$df$', DataFolder 
    + @dbname + N'.mdf'), N'$lf$', LogFolder + @dbname + N'.ldf'), N'''', N'''''') 
    + N';'';' + CHAR(13) + CHAR(10)
  FROM dbo.PMAG_Secondaries
  WHERE DatabaseName = @dbname;
 
  EXEC [master].sys.sp_executesql @sql;
 
  -- backup a log for this database
  EXEC dbo.PMAG_Backup @dbname = @dbname, @type = 'trn';
 
  -- restore logs
  EXEC dbo.PMAG_RestoreLogs @dbname = @dbname, @PrepareAll = 1;
END

Dan kemudian prosedur yang akan mengembalikan log:

CREATE PROCEDURE dbo.PMAG_RestoreLogs
  @dbname     SYSNAME,
  @PrepareAll BIT = 0
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @StandbyInstance SYSNAME,
          @CurrentInstance SYSNAME,
          @BackupSetID     INT, 
          @Location        VARCHAR(512),
          @StandByLocation VARCHAR(512),
          @sql             NVARCHAR(MAX),
          @rn              INT;
 
  -- get the "next" standby instance
  SELECT @StandbyInstance = MIN(ServerInstance)
    FROM dbo.PMAG_Secondaries
    WHERE IsCurrentStandby = 0
      AND ServerInstance > (SELECT ServerInstance
    FROM dbo.PMAG_Secondaries
    WHERE IsCurrentStandBy = 1);
 
  IF @StandbyInstance IS NULL -- either it was last or a re-init
  BEGIN
    SELECT @StandbyInstance = MIN(ServerInstance)
      FROM dbo.PMAG_Secondaries;
  END
 
  -- get that instance up and into STANDBY
  -- for each log in logbackuphistory not in logrestorehistory:
  -- restore, and insert it into logrestorehistory
  -- mark the last one as STANDBY
  -- if @prepareAll is true, mark all others as NORECOVERY
  -- in this case there should be only one, but just in case
 
  DECLARE c CURSOR LOCAL FAST_FORWARD FOR 
    SELECT bh.BackupSetID, s.ServerInstance, bh.Location, s.StandbyLocation,
      rn = ROW_NUMBER() OVER (PARTITION BY s.ServerInstance ORDER BY bh.BackupSetID DESC)
    FROM dbo.PMAG_LogBackupHistory AS bh
    INNER JOIN dbo.PMAG_Secondaries AS s
    ON bh.DatabaseName = s.DatabaseName
    AND bh.ServerInstance = s.ServerInstance
    WHERE s.DatabaseName = @dbname
    AND s.ServerInstance = CASE @PrepareAll 
	WHEN 1 THEN s.ServerInstance ELSE @StandbyInstance END
    AND NOT EXISTS
    (
      SELECT 1 FROM dbo.PMAG_LogRestoreHistory AS rh
        WHERE DatabaseName = @dbname
        AND ServerInstance = s.ServerInstance
        AND BackupSetID = bh.BackupSetID
    )
    ORDER BY CASE s.ServerInstance 
      WHEN @StandbyInstance THEN 1 ELSE 2 END, bh.BackupSetID;
 
  OPEN c;
 
  FETCH c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn;
 
  WHILE @@FETCH_STATUS  -1
  BEGIN
    -- kick users out - set to single_user then back to multi
    SET @sql = N'EXEC ' + QUOTENAME(@CurrentInstance) + N'.[master].sys.sp_executesql '
    + 'N''IF EXISTS (SELECT 1 FROM sys.databases WHERE name = N''''' 
	+ @dbname + ''''' AND [state]  1)
	  BEGIN
	    ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET SINGLE_USER '
      +   N'WITH ROLLBACK IMMEDIATE;
	    ALTER DATABASE ' + QUOTENAME(@dbname) + N' SET MULTI_USER;
	  END;'';';
 
    EXEC [master].sys.sp_executesql @sql;
 
    -- restore the log (in STANDBY if it's the last one):
    SET @sql = N'EXEC ' + QUOTENAME(@CurrentInstance) 
      + N'.[master].sys.sp_executesql ' + N'N''RESTORE LOG ' + QUOTENAME(@dbname) 
      + N' FROM DISK = N''''' + @Location + N''''' WITH ' + CASE WHEN @rn = 1 
        AND (@CurrentInstance = @StandbyInstance OR @PrepareAll = 1) THEN 
        N'STANDBY = N''''' + @StandbyLocation + @dbname + N'.standby''''' ELSE 
        N'NORECOVERY' END + N';'';';
 
    EXEC [master].sys.sp_executesql @sql;
 
    -- record the fact that we've restored logs
    INSERT dbo.PMAG_LogRestoreHistory
      (DatabaseName, ServerInstance, BackupSetID, RestoreTime)
    SELECT @dbname, @CurrentInstance, @BackupSetID, SYSDATETIME();
 
    -- mark the new standby
    IF @rn = 1 AND @CurrentInstance = @StandbyInstance -- this is the new STANDBY
    BEGIN
        UPDATE dbo.PMAG_Secondaries 
          SET IsCurrentStandby = CASE ServerInstance
            WHEN @StandbyInstance THEN 1 ELSE 0 END 
          WHERE DatabaseName = @dbname;
    END
 
    FETCH c INTO @BackupSetID, @CurrentInstance, @Location, @StandbyLocation, @rn;
  END
 
  CLOSE c; DEALLOCATE c;
END

(Saya tahu ini banyak kode, dan banyak SQL dinamis yang samar. Saya mencoba untuk menjadi sangat liberal dengan komentar; jika ada bagian yang Anda kesulitan, beri tahu saya.)

Jadi sekarang, yang harus Anda lakukan untuk mengaktifkan dan menjalankan sistem adalah membuat dua panggilan prosedur:

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

Sekarang Anda akan melihat setiap instance dengan salinan database siaga:

Dan Anda dapat melihat mana yang saat ini berfungsi sebagai siaga hanya-baca:

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

Langkah 6 – buat tugas yang mencadangkan / memulihkan log

Anda dapat memasukkan perintah ini ke dalam pekerjaan yang Anda jadwalkan setiap 15 menit:

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

Ini akan menggeser sekunder aktif setiap 15 menit, dan datanya akan menjadi 15 menit lebih baru dari sekunder aktif sebelumnya. Jika Anda memiliki beberapa database pada jadwal yang berbeda, Anda dapat membuat beberapa pekerjaan, atau menjadwalkan pekerjaan lebih sering dan memeriksa dbo.PMAG_Databases tabel untuk masing-masing LogBackupFrequency_Minutes nilai untuk menentukan apakah Anda harus menjalankan pencadangan/pemulihan untuk database tersebut.

Langkah 7 – tampilan dan prosedur untuk memberi tahu aplikasi standby mana yang aktif

CREATE VIEW dbo.PMAG_ActiveSecondaries
AS
  SELECT DatabaseName, ServerInstance
    FROM dbo.PMAG_Secondaries
    WHERE IsCurrentStandby = 1;
GO
 
CREATE PROCEDURE dbo.PMAG_GetActiveSecondary
  @dbname SYSNAME
AS
BEGIN
  SET NOCOUNT ON;
 
  SELECT ServerInstance
    FROM dbo.PMAG_ActiveSecondaries
    WHERE DatabaseName = @dbname;
END
GO

Dalam kasus saya, saya juga secara manual membuat penyatuan tampilan di semua UserData database sehingga saya dapat membandingkan kemutakhiran data di primer dengan setiap sekunder.

CREATE VIEW dbo.PMAG_CompareRecency_UserData
AS
  WITH x(ServerInstance, EventTime)
  AS
  (
    SELECT @@SERVERNAME, EventTime FROM UserData.dbo.LastUpdate
    UNION ALL SELECT N'.\PEON1', EventTime FROM [.\PEON1].UserData.dbo.LastUpdate
    UNION ALL SELECT N'.\PEON2', EventTime FROM [.\PEON2].UserData.dbo.LastUpdate
    UNION ALL SELECT N'.\PEON3', EventTime FROM [.\PEON3].UserData.dbo.LastUpdate
    UNION ALL SELECT N'.\PEON4', EventTime FROM [.\PEON4].UserData.dbo.LastUpdate
  )
  SELECT x.ServerInstance, s.IsCurrentStandby, x.EventTime,
         Age_Minutes = DATEDIFF(MINUTE, x.EventTime, SYSDATETIME()),
         Age_Seconds = DATEDIFF(SECOND, x.EventTime, SYSDATETIME())
    FROM x LEFT OUTER JOIN dbo.PMAG_Secondaries AS s
      ON s.ServerInstance = x.ServerInstance
      AND s.DatabaseName = N'UserData';
GO

Contoh hasil akhir pekan:

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

Langkah 8 – prosedur pembersihan

Membersihkan riwayat pencadangan dan pemulihan log cukup mudah.

CREATE PROCEDURE dbo.PMAG_CleanupHistory
  @dbname   SYSNAME,
  @DaysOld  INT = 7
AS
BEGIN
  SET NOCOUNT ON;
 
  DECLARE @cutoff INT;
 
  -- this assumes that a log backup either 
  -- succeeded or failed on all secondaries 
  SELECT @cutoff = MAX(BackupSetID)
    FROM dbo.PMAG_LogBackupHistory AS bh
    WHERE DatabaseName = @dbname
    AND BackupTime < DATEADD(DAY, -@DaysOld, SYSDATETIME())
    AND EXISTS
    (
      SELECT 1 
        FROM dbo.PMAG_LogRestoreHistory AS rh
        WHERE BackupSetID = bh.BackupSetID
          AND DatabaseName = @dbname
          AND ServerInstance = bh.ServerInstance
    );
 
  DELETE dbo.PMAG_LogRestoreHistory
    WHERE DatabaseName = @dbname
    AND BackupSetID <= @cutoff;
 
  DELETE dbo.PMAG_LogBackupHistory 
    WHERE DatabaseName = @dbname
    AND BackupSetID <= @cutoff;
END
GO

Sekarang, Anda dapat menambahkannya sebagai langkah dalam pekerjaan yang ada, atau Anda dapat menjadwalkannya sepenuhnya secara terpisah atau sebagai bagian dari rutinitas pembersihan lainnya.

Saya akan meninggalkan pembersihan sistem file untuk posting lain (dan mungkin mekanisme yang terpisah sama sekali, seperti PowerShell atau C# – ini biasanya bukan hal yang Anda ingin T-SQL lakukan).

Langkah 9 – tingkatkan solusi

Memang benar bahwa mungkin ada penanganan kesalahan yang lebih baik dan basa-basi lainnya di sini untuk membuat solusi ini lebih lengkap. Untuk saat ini saya akan meninggalkannya sebagai latihan untuk pembaca, tetapi saya berencana untuk melihat posting tindak lanjut untuk merinci peningkatan dan penyempurnaan solusi ini.

Variabel dan batasan

Perhatikan bahwa dalam kasus saya, saya menggunakan Edisi Standar sebagai yang utama, dan Edisi Ekspres untuk semua sekunder. Anda dapat melangkah lebih jauh dalam skala anggaran dan bahkan menggunakan Edisi Ekspres sebagai yang utama – banyak orang berpikir Edisi Ekspres tidak mendukung pengiriman log, padahal sebenarnya itu hanyalah panduan yang tidak ada di versi Management Studio Express sebelum SQL Server 2012 Service Pack 1. Karena itu, karena Express Edition tidak mendukung SQL Server Agent, akan sulit untuk menjadikannya penerbit dalam skenario ini – Anda harus mengonfigurasi penjadwal Anda sendiri untuk memanggil prosedur tersimpan (C# aplikasi baris perintah yang dijalankan oleh Penjadwal Tugas Windows, pekerjaan PowerShell, atau pekerjaan Agen Server SQL pada contoh lain). Untuk menggunakan Express di kedua sisi, Anda juga harus yakin bahwa file data Anda tidak akan melebihi 10 GB, dan kueri Anda akan berfungsi dengan baik dengan memori, CPU, dan batasan fitur edisi tersebut. Saya sama sekali tidak menyarankan bahwa Express ideal; Saya hanya menggunakannya untuk menunjukkan bahwa adalah mungkin untuk memiliki sekunder yang sangat fleksibel dan dapat dibaca secara gratis (atau sangat dekat dengannya).

Selain itu, instance terpisah ini dalam skenario saya semuanya hidup di VM yang sama, tetapi tidak harus bekerja seperti itu sama sekali – Anda dapat menyebarkan instance ke beberapa server; atau, Anda bisa pergi ke arah lain, dan mengembalikan ke salinan database yang berbeda, dengan nama yang berbeda, pada contoh yang sama. Konfigurasi ini membutuhkan sedikit perubahan pada apa yang telah saya paparkan di atas. Dan berapa banyak basis data yang Anda pulihkan, dan seberapa sering, sepenuhnya terserah Anda – meskipun akan ada batas atas praktis (di mana [average query time] > [number of secondaries] x [log backup interval] ).

Akhirnya, pasti ada beberapa keterbatasan dengan pendekatan ini. Daftar yang tidak lengkap:

  1. Meskipun Anda dapat terus melakukan pencadangan penuh sesuai jadwal Anda sendiri, pencadangan log harus berfungsi sebagai satu-satunya mekanisme pencadangan log Anda. Jika Anda perlu menyimpan cadangan log untuk tujuan lain, Anda tidak akan dapat mencadangkan log secara terpisah dari solusi ini, karena akan mengganggu rantai log. Sebagai gantinya, Anda dapat mempertimbangkan untuk menambahkan MIRROR TO additional tambahan arguments to the existing log backup scripts, if you need to have copies of the logs used elsewhere.
  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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Lebih banyak operasi online tersedia sekarang – atau segera

  2. Cara membuat Replikasi Transaksional

  3. 7 Hal Utama yang Perlu Diingat Tentang Globalisasi Model Data

  4. Wawancara dengan Oren Eini dari RavenDB tentang manajemen basis data, analitik &keamanan

  5. Dasar-dasar ekspresi tabel, Bagian 5 – CTE, pertimbangan logis