sql >> データベース >  >> RDS >> Database

予算内で読み取り可能なセカンダリ

    SQL Server 2012で導入された可用性グループは、データベースの高可用性と障害復旧の両方についての考え方の根本的な変化を表しています。ここで可能になった優れた点の1つは、読み取り専用操作をセカンダリレプリカにオフロードすることです。これにより、プライマリ読み取り/書き込みインスタンスは、エンドユーザーのレポートなどの厄介なことに煩わされることはありません。これを設定するのは簡単ではありませんが、以前のソリューションよりもはるかに簡単で保守が容易です(ミラーリングとスナップショットの設定、およびそれに関連するすべての永続的な保守が好きな場合は、手を挙げてください)。

    可用性グループについて聞くと、人々は非常に興奮します。次に現実が襲います。この機能には、SQLServerのEnterpriseEditionが必要です(とにかく、SQL Server 2014以降)。 Enterprise Editionは、特にコアが多数ある場合、特にCALベースのライセンスが廃止されたため(2008 R2から祖父になっていない限り、最初の20コアに制限されている場合)は高価です。また、Windows Serverフェールオーバークラスタリング(WSFC)も必要です。これは、ラップトップでテクノロジをデモンストレーションするためだけでなく、WindowsのEnterprise Edition、ドメインコントローラ、およびクラスタリングをサポートするための一連の構成も必要とします。また、ソフトウェアアシュアランスに関する新しい要件もあります。スタンバイインスタンスを準拠させたい場合は、追加のコストがかかります。

    一部の顧客は価格を正当化できません。他の人はその価値を見ますが、単にそれを買う余裕はありません。では、これらのユーザーは何をするのでしょうか?

    あなたの新しいヒーロー:ログ配送

    丸太の発送は何年も前からありました。シンプルで機能します。ほとんどいつも。また、可用性グループによって提示されたライセンスコストと構成のハードルを回避するだけでなく、今週のSQLskills Insiderニュースレター(2014年10月13日)でPaul Randal(@PaulRandal)が話していた14バイトのペナルティを回避することもできます。

    ただし、ログに送信されたコピーを読み取り可能なセカンダリとして使用する際の課題の1つは、新しいログを適用するために現在のすべてのユーザーを追い出さなければならないことです。クエリの実行から、またはデータが古くなっているためにユーザーがイライラする場合。これは、人々が自分自身を単一の読み取り可能なセカンダリに制限しているためです。

    そのようにする必要はありません。ここには適切な解決策があると思います。たとえば、可用性グループをオンにするよりも前もって多くの作業が必要になる場合がありますが、それは確かに一部の人にとって魅力的なオプションです。

    基本的に、いくつかのセカンダリを設定できます。ここでは、ラウンドロビンアプローチを使用して、ログシップを行い、そのうちの1つだけを「アクティブな」セカンダリにします。ログを送信するジョブは、現在アクティブなログを認識しているため、 WITH STANDBYを使用して新しいログを「次の」サーバーに復元するだけです。 オプション。レポートアプリケーションは同じ情報を使用して、実行時にユーザーが実行する次のレポートの接続文字列を決定します。次のログバックアップの準備ができると、すべてが1つシフトし、新しい読み取り可能なセカンダリになるインスタンスが WITH STANDBYを使用して復元されます。 。

    モデルを複雑にしないために、読み取り可能なセカンダリとして機能する4つのインスタンスがあり、15分ごとにログバックアップを作成するとします。いつでも、スタンバイモードでアクティブなセカンダリが1つあり、データは15分以内で、スタンバイモードで新しいクエリを処理していないセカンダリが3つあります(ただし、古いクエリの結果が返される場合があります)。

    これは、クエリが45分より長く続くと予想されない場合に最適に機能します。 (読み取り専用操作の性質、より長いクエリを実行している同時ユーザーの数、および全員を追い出すことによってユーザーを混乱させる可能性があるかどうかに応じて、これらのサイクルを調整する必要がある場合があります。)

    また、同じユーザーが連続して実行するクエリで接続文字列を変更でき(アーキテクチャによっては同義語またはビューを使用できますが、これはアプリケーションに必要なロジックです)、次のような異なるデータが含まれている場合にも最適に機能します。その間に変更されました(ライブの絶えず変化するデータベースにクエリを実行しているかのように)。

    これらすべての仮定を念頭に置いて、実装の最初の75分間の一連のイベントを以下に示します。

    time イベント ビジュアル
    12:00 (t0)
    • バックアップログt0
    • ユーザーをインスタンスAから追い出します
    • ログt0をインスタンスAに復元します(スタンバイ)
    • 新しい読み取り専用クエリはインスタンスAに送信されます
    12:15 (t1)
    • バックアップログt1
    • ユーザーをインスタンスBから追い出します
    • ログt0をインスタンスBに復元します(NORECOVERY)
    • ログt1をインスタンスBに復元します(スタンバイ)
    • 新しい読み取り専用クエリはインスタンスBに送信されます
    • インスタンスAへの既存の読み取り専用クエリは実行を継続できますが、最大15分遅れます
    12:30 (t2)
    • バックアップログt2
    • ユーザーをインスタンスCから追い出します
    • ログt0->t1をインスタンスCに復元します(NORECOVERY)
    • ログt2をインスタンスCに復元します(スタンバイ)
    • 新しい読み取り専用クエリはインスタンスCに送信されます
    • インスタンスAおよびBへの既存の読み取り専用クエリは実行を継続できます(15〜30分遅れ)
    12:45 (t3)
    • バックアップログt3
    • ユーザーをインスタンスDから追い出します
    • ログt0->t2をインスタンスDに復元します(NORECOVERY)
    • ログt3をインスタンスDに復元します(スタンバイ)
    • 新しい読み取り専用クエリはインスタンスDに送信されます
    • インスタンスA、B、Cへの既存の読み取り専用クエリは実行を継続できます(15〜45分遅れ)
    13:00 (t4)
    • バックアップログt4
    • ユーザーをインスタンスAから追い出します
    • ログt1->t3をインスタンスAに復元します(NORECOVERY)
    • ログt4をインスタンスAに復元します(スタンバイ)
    • 新しい読み取り専用クエリはインスタンスAに送信されます
    • インスタンスB、C、Dへの既存の読み取り専用クエリは実行を継続できます(15〜45分遅れ)
    • t0->〜t1(45〜60分)以降、インスタンスAで実行中のクエリはキャンセルされます


    それは簡単に思えるかもしれません。それをすべて処理するコードを書くのはもう少し大変です。大まかな概要:

    1. プライマリサーバー上(これを BOSSと呼びます )、データベースを作成します。さらに先に進むことを考える前に、トレースフラグ3226をオンにして、正常なバックアップメッセージがSQLServerのエラーログを散らかさないようにします。
    2. BOSSについて 、セカンダリごとにリンクサーバーを追加します( PEON1 と呼びます) -> PEON4
    3. すべてのサーバーがアクセスできる場所で、データベース/ログのバックアップを保存するファイル共有を作成し、各インスタンスのサービスアカウントに読み取り/書き込みアクセス権があることを確認します。また、各セカンダリインスタンスには、スタンバイファイル用に指定された場所が必要です。
    4. 別のユーティリティデータベース(または必要に応じてMSDB)で、データベース、すべてのセカンダリ、およびログのバックアップと復元の履歴に関する構成情報を保持するテーブルを作成します。
    5. データベースをバックアップし、セカンダリに復元するストアドプロシージャを作成します WITH NORECOVERY 、次に1つのログを適用します WITH STANDBY 、および1つのインスタンスを現在のスタンバイセカンダリとしてマークします。これらの手順を使用して、問題が発生した場合にログ配布設定全体を再初期化することもできます。
    6. 15分ごとに実行されるジョブを作成して、上記のタスクを実行します。
      • ログをバックアップする
      • 適用されていないログバックアップを適用するセカンダリを決定する
      • これらのログを適切な設定で復元します
    7. 新しい読み取り専用クエリに使用するセカンダリを呼び出し元のアプリケーションに通知するストアドプロシージャ(および/またはビュー?)を作成します。
    8. クリーンアップ手順を作成して、すべてのセカンダリに適用されたログのログバックアップ履歴をクリアします(おそらく、ファイル自体を移動またはパージするためにも)。
    9. 堅牢なエラー処理と通知でソリューションを強化します。

    ステップ1-データベースを作成する

    私のプライマリインスタンスは、。\ BOSSという名前のStandardEditionです。 。その場合、1つのテーブルを使用して単純なデータベースを作成します。

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

    次に、そのタイムスタンプを1分ごとに更新するだけのSQLServerエージェントジョブを作成します。

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

    これにより、初期データベースが作成され、アクティビティがシミュレートされ、読み取り可能な各セカンダリでログ配布タスクがどのようにローテーションされるかを検証できます。この演習のポイントは、テストログの配布にストレスをかけたり、パンチスルーできる量を証明したりすることではないことを明確に述べたいと思います。それはまったく別の演習です。

    ステップ2–リンクサーバーを追加する

    。\PEON1という名前の4つのセカンダリExpressEditionインスタンスがあります 、。\ PEON2 。\ PEON3 、および。\ PEON4 。そこで、このコードを4回実行し、 @sを変更しました。 毎回:

    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;

    ステップ3–ファイル共有を検証する

    私の場合、5つのインスタンスすべてが同じサーバー上にあるため、インスタンスごとにフォルダーを作成しました: C:\ temp \ Peon1 \ C:\ temp \ Peon2 \ 、 等々。セカンダリが異なるサーバー上にある場合、場所はそのサーバーからの相対位置である必要がありますが、プライマリからアクセスできる必要があります(したがって、通常はUNCパスが使用されます)。各インスタンスがその共有に書き込めることを検証する必要があります。また、各インスタンスがスタンバイファイルに指定された場所に書き込めることも検証する必要があります(スタンバイに同じフォルダーを使用しました)。これを検証するには、各インスタンスから指定された各場所に小さなデータベースをバックアップします。これが機能するまで続行しないでください。

    ステップ4–テーブルを作成する

    このデータをmsdbに配置することにしました 、しかし、私は別のデータベースを作成することに賛成または反対の強い感情を持っていません。最初に必要なテーブルは、ログ配布するデータベースに関する情報を保持するテーブルです。

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

    (命名スキームに興味がある場合、PMAGは「PoorMan'sAvailabilityGroups」の略です。)

    必要なもう1つのテーブルは、個々のフォルダやログ配布シーケンスでの現在のステータスなど、セカンダリに関する情報を保持するためのテーブルです。

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

    ソースサーバーからローカルにバックアップし、セカンダリをリモートで復元する場合、またはその逆の場合は、 CommonFolderを分割できます。 2つの列に( BackupFolder およびRestoreFolder )、コードに関連する変更を加えます(それほど多くはありません)。

    sys.servers の情報に少なくとも部分的に基づいて、このテーブルにデータを入力できるため –データ/ログおよびその他のフォルダーがインスタンス名にちなんで名付けられているという事実を利用する:

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

    多くの場合、複数のログファイルを順番に復元する必要があるため、(最後のバックアップだけでなく)個々のログバックアップを追跡するためのテーブルも必要です。この情報はmsdb.dbo.backupsetから取得できます 、しかし、場所などを取得するのははるかに複雑です。バックアップ履歴をクリーンアップする可能性のある他のジョブを制御できない可能性があります。

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

    各セカンダリの行を保存し、すべてのバックアップの場所を保存するのは無駄だと思うかもしれませんが、これは将来を保証するためです。つまり、任意のセカンダリのCommonFolderを移動する場合に対処します。

    そして最後に、ログの復元の履歴があるため、いつでも、どのログがどこに復元されたかを確認できます。復元ジョブでは、まだ復元されていないログのみを確実に復元できます。

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

    ステップ5–セカンダリを初期化する

    バックアップファイルを生成する(そして、さまざまなインスタンスが必要とする場所にミラーリングする)ストアドプロシージャが必要です。また、各セカンダリに1つのログを復元して、すべてをスタンバイ状態にします。この時点で、これらはすべて読み取り専用クエリで使用できるようになりますが、一度に「現在の」スタンバイになるのは1つだけです。これは、完全バックアップとトランザクションログバックアップの両方を処理するストアドプロシージャです。完全バックアップが要求されたとき、および @init 1に設定すると、ログ配布が自動的に再初期化されます。

    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

    これにより、別々に呼び出すことができる2つのプロシージャが呼び出されます(ただし、ほとんどの場合、呼び出されません)。まず、最初の実行時にセカンダリを初期化する手順:

    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

    そして、ログを復元する手順:

    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

    (コードが多く、不可解な動的SQLが多いことはわかっています。コメントは非常に寛大にしようとしました。問題が発生している部分がある場合は、お知らせください。)>

    これで、システムを起動して実行するために必要なのは、2つのプロシージャ呼び出しを行うことだけです。

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

    これで、データベースのスタンバイコピーを含む各インスタンスが表示されます。

    そして、どれが現在読み取り専用スタンバイとして機能する必要があるかを確認できます:

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

    ステップ6–ログをバックアップ/復元するジョブを作成する

    このコマンドは、15分ごとにスケジュールするジョブに入れることができます:

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

    これにより、アクティブなセカンダリが15分ごとにシフトし、そのデータは前のアクティブなセカンダリより15分新鮮になります。異なるスケジュールで複数のデータベースがある場合は、複数のジョブを作成するか、ジョブをより頻繁にスケジュールして dbo.PMAG_Databasesを確認できます。 個々のLogBackupFrequency_Minutesのテーブル そのデータベースのバックアップ/復元を実行する必要があるかどうかを判断するための値。

    ステップ7–どのスタンバイがアクティブであるかをアプリケーションに通知するためのビューと手順

    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

    私の場合、すべての UserData全体でビューユニオンを手動で作成しました データベースを使用して、プライマリと各セカンダリのデータの最新性を比較できるようにします。

    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

    週末のサンプル結果:

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

    ステップ8–クリーンアップ手順

    ログのバックアップと復元の履歴をクリーンアップするのは非常に簡単です。

    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

    これで、既存のジョブのステップとして追加することも、完全に個別に、または他のクリーンアップルーチンの一部としてスケジュールすることもできます。

    ファイルシステムのクリーンアップは別の投稿のために残しておきます(おそらく、PowerShellやC#などの別のメカニズムです。これは通常、T-SQLで実行したい種類のことではありません)。

    ステップ9–ソリューションを強化する

    このソリューションをより完全にするために、ここでより良いエラー処理やその他の優れた機能がある可能性があるのは事実です。今のところ、これは読者の演習として残しておきますが、このソリューションの改善と改良について詳しく説明するフォローアップ投稿を確認する予定です。

    変数と制限

    私の場合、プライマリとしてStandard Editionを使用し、すべてのセカンダリにExpressEditionを使用したことに注意してください。予算規模をさらに進めて、Express Editionをプライマリとして使用することもできます。多くの人は、Express Editionはログ配布をサポートしていないと考えていますが、実際には、ManagementStudioのバージョンには存在しなかったウィザードにすぎません。 SQL Server 2012 Service Pack 1より前のExpress。ただし、ExpressEditionはSQLServer Agentをサポートしていないため、このシナリオでパブリッシャーにすることは困難です。ストアドプロシージャを呼び出すように独自のスケジューラーを構成する必要があります(C#さらに別のインスタンスでWindowsタスクスケジューラ、PowerShellジョブ、またはSQL Serverエージェントジョブによって実行されるコマンドラインアプリ)。どちらの側でもExpressを使用するには、データファイルが10 GBを超えないこと、およびクエリがそのエディションのメモリ、CPU、および機能の制限で正常に機能することを確認する必要があります。 Expressが理想的であることを示唆しているわけではありません。私は単に、非常に柔軟で読み取り可能なセカンダリを無料で(またはそれに非常に近い形で)持つことができることを示すために使用しました。

    また、私のシナリオのこれらの個別のインスタンスはすべて同じVM上にありますが、そのように機能する必要はありません。インスタンスを複数のサーバーに分散させることができます。または、別の方法で、同じインスタンス上の異なる名前のデータベースの異なるコピーに復元することもできます。これらの構成では、上記でレイアウトしたものに最小限の変更を加える必要があります。また、復元するデータベースの数と頻度は完全にあなた次第です。ただし、実際的な上限はあります(ここで、[平均クエリ時間]>[セカンダリの数]x[ログバックアップ間隔]

    最後に、このアプローチには確かにいくつかの制限があります。網羅的ではないリスト:

    1. 自分のスケジュールで完全バックアップを引き続き作成できますが、ログバックアップは唯一のログバックアップメカニズムとして機能する必要があります。他の目的でログバックアップを保存する必要がある場合、ログチェーンに干渉するため、このソリューションとは別にログをバックアップすることはできません。代わりに、 MIRROR TOを追加することを検討できます。 ログのコピーを他の場所で使用する必要がある場合は、既存のログバックアップスクリプトへの引数。
    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. どのSqlDbTypeがvarBinary(max)にマップされますか?

    2. postgresqlでグローバル変数を定義することは可能ですか?

    3. 簡単な例でSQLServerHierarchyIDを使用する方法

    4. Hibernateでの同時更新処理