SQLServerがACIDトランザクションプロパティの保証を提供する前に、おそらく何度も聞いたことがあるでしょう。この記事では、もちろん耐久性を表すD部分に焦点を当てます。具体的には、この記事では、トランザクションの耐久性を強化するSQL Serverロギングアーキテクチャの側面、つまりログバッファーのフラッシュに焦点を当てています。ログバッファが提供する機能、SQL Serverがログバッファをディスクにフラッシュするように強制する条件、トランザクションパフォーマンスを最適化するためにできること、および遅延耐久性や不揮発性ストレージクラスメモリなどの最近追加された関連テクノロジについて説明します。
ログバッファのフラッシュ
ACIDトランザクションプロパティのD部分は、耐久性を表します。論理レベルでは、アプリケーションがSQL Serverにトランザクションをコミットする命令を送信すると(明示的に、または自動コミットトランザクションを使用して)、SQL Serverは通常、トランザクションの永続性を保証できる場合にのみ、呼び出し元に制御を返します。つまり、呼び出し元がトランザクションをコミットした後に制御を取り戻すと、サーバーに電源障害が発生した後でも、トランザクションの変更によってデータベースに反映されたと信頼できます。サーバーが正常に再起動し、データベースファイルが破損していない限り、すべてのトランザクション変更が適用されていることがわかります。
SQL Serverがトランザクションの耐久性を強化する方法の一部は、トランザクションのすべての変更がディスク上のデータベースのトランザクションログに書き込まれるようにすることです。 呼び出し元に制御を戻す前。トランザクションのコミットが確認された後に電源障害が発生した場合、これらの変更はすべて、少なくともディスク上のトランザクションログに書き込まれていることがわかります。これは、関連するデータページがデータキャッシュ(バッファプール)でのみ変更され、ディスク上のデータファイルにまだフラッシュされていない場合でも当てはまります。 SQL Serverを再起動すると、回復プロセスのやり直しフェーズで、SQL Serverはログに記録された情報を使用して、最後のチェックポイントの後に適用され、データファイルに反映されていない変更を再生します。使用しているリカバリモデルと、最後のチェックポイントの後に一括操作が適用されたかどうかによって、話はもう少しありますが、ここでの説明では、変更を強化する部分に焦点を当てるだけで十分です。トランザクションログ。
SQL Serverのロギングアーキテクチャで注意が必要なのは、ログの書き込みがシーケンシャルであるということです。 SQL Serverがディスクへのログ書き込みを軽減するために何らかのログバッファを使用していなかった場合、書き込みが集中するシステム(特に、大量の小さなトランザクションを伴うシステム)は、ログ書き込みに関連するひどいパフォーマンスのボトルネックにすぐに遭遇します。
ディスクへの頻繁な順次ログ書き込みによるパフォーマンスへの悪影響を軽減するために、SQLServerはメモリ内のログバッファを使用します。ログの書き込みは最初にログバッファに対して行われ、特定の条件により、SQLServerはログバッファをディスクにフラッシュまたは強化します。強化されたユニット(別名ログブロック)は、最小セクターサイズ(512バイト)から最大60KBの範囲になります。ログバッファフラッシュをトリガーする条件は次のとおりです(今のところ角括弧内に表示されている部分は無視してください):
- SQL Serverは、[tempdb以外のデータベース内の]データを変更する[完全に耐久性のある]トランザクションのコミット要求を受け取ります
- ログバッファがいっぱいになり、60KBの容量に達しました
- SQL Serverは、たとえばチェックポイントプロセス中に、ダーティデータページを強化する必要があり、それらのページへの変更を表すログレコードはまだ強化されていません(ログ先行書き込み 、または略してWAL)
- プロシージャsys.sp_flush_logを実行して、ログバッファのフラッシュを手動で要求します
- SQL Serverは、[tempdb以外のデータベースに]新しいシーケンスキャッシュ関連の回復値を書き込みます
今のところ角括弧内の情報を無視すると、最初の4つの条件はかなり明確になるはずです。最後の1つはまだ明確ではないかもしれませんが、この記事の後半で詳しく説明します。
SQLServerがログバッファフラッシュを処理するI/O操作の完了を待機する時間は、WRITELOG待機タイプに反映されます。
では、なぜこの情報はそれほど興味深いのでしょうか。また、それをどのように処理するのでしょうか。ログバッファのフラッシュをトリガーする条件を理解すると、特定のワークロードで関連するボトルネックが発生する理由を理解するのに役立ちます。また、場合によっては、そのようなボトルネックを削減または排除するために実行できるアクションがあります。 1つの大きなトランザクションと多くの小さなトランザクション、完全な耐久性と遅延した耐久性のあるトランザクション、ユーザーデータベースとtempdb、シーケンスオブジェクトのキャッシュなど、いくつかの例を取り上げます。
1つの大きなトランザクションと多くの小さなトランザクション
前述のように、ログバッファのフラッシュをトリガーする条件の1つは、トランザクションの耐久性を保証するためにトランザクションをコミットするときです。つまり、OLTPワークロードのように、多くの小さなトランザクションを伴うワークロードでは、ログ書き込み関連のボトルネックが発生する可能性があります。
多くの場合、そうではありませんが、単一のセッションで多数の小さな変更を送信する場合、作業を最適化する簡単で効果的な方法は、複数の小さなトランザクションではなく、単一の大きなトランザクションで変更を適用することです。
次の簡略化された例を検討してください(ここからPerformanceV3をダウンロードしてください):
SET NOCOUNT ON; USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled; -- default DROP TABLE IF EXISTS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i += 1; END;
このコードは、ユーザーデータベースのデータを変更する1,000,000の小さなトランザクションを実行します。この作業により、少なくとも1,000,000のログバッファーフラッシュがトリガーされます。ログバッファがいっぱいになるため、追加のものをいくつか取得する可能性があります。次のテストテンプレートを使用して、ログバッファーのフラッシュの数をカウントし、作業の完了にかかった時間を測定できます。
-- Test template -- ... Preparation goes here ... -- Count log flushes and measure time DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats before SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Log Flushes/sec' AND instance_name = @db ); SET @starttime = SYSDATETIME(); -- ... Actual work goes here ... -- Stats after SET @duration = DATEDIFF(second, @starttime, SYSDATETIME()); SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Log Flushes/sec' AND instance_name = @db ) - @logflushes; SELECT @duration AS durationinseconds, @logflushes AS logflushes;
パフォーマンスカウンターの名前はLogFlushes/ secですが、実際にはこれまでのログバッファーフラッシュの数を累積し続けます。したがって、コードは、作業後のカウントから作業前のカウントを差し引いて、作業によって生成されたログフラッシュのカウントを計算します。このコードは、作業が完了するまでにかかった時間を秒単位で測定します。ここではこれを行いませんが、必要に応じて、fn_dblogの作業前と作業後の状態を照会することで、ログレコードの数と作業によってログに書き込まれるサイズを同様に把握できます。機能。
上記の例では、テストテンプレートの準備セクションに配置する必要のある部分は次のとおりです。
-- Preparation SET NOCOUNT ON; USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled; DROP TABLE IF EXISTS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname = N'PerformanceV3'; DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;を宣言します
そして、実際の作業セクションに配置する必要がある部分は次のとおりです。
-- Actual work DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i += 1; END;
全体として、次のコードを取得します:
-- Example test with many small fully durable transactions in user database -- ... Preparation goes here ... -- Preparation SET NOCOUNT ON; USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Disabled; DROP TABLE IF EXISTS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname = N'PerformanceV3'; DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats before SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Log Flushes/sec' AND instance_name = @db ); SET @starttime = SYSDATETIME(); -- ... Actual work goes here ... -- Actual work DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i += 1; END; -- Stats after SET @duration = DATEDIFF(second, @starttime, SYSDATETIME()); SET @logflushes = ( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Log Flushes/sec' AND instance_name = @db ) - @logflushes; SELECT @duration AS durationinseconds, @logflushes AS logflushes;
このコードは私のシステムで完了するのに193秒かかり、1,000,036のログバッファーフラッシュをトリガーしました。これは非常に遅いですが、ログフラッシュの数が多いために説明できます。
通常のOLTPワークロードでは、さまざまなセッションがさまざまな小さなトランザクションで小さな変更を同時に送信するため、1つの大きなトランザクションで多くの小さな変更をカプセル化するオプションが実際にあるわけではありません。ただし、すべての小さな変更が同じセッションから送信されるという状況の場合、作業を最適化する簡単な方法は、それを単一のトランザクションにカプセル化することです。これにより、2つの主なメリットが得られます。 1つは、作業によって書き込まれるログレコードが少なくなることです。 1,000,000の小さなトランザクションでは、各トランザクションは実際に3つのログレコードを書き込みます。1つはトランザクションの開始用、1つは変更用、もう1つはトランザクションのコミット用です。つまり、1つの大きなトランザクションとして実行された場合の1,000,000を少し超えるのに対し、約3,000,0000のトランザクションログレコードが表示されます。しかし、さらに重要なことは、1つの大きなトランザクションでは、ほとんどのログフラッシュは、ログバッファーがいっぱいになったときにのみトリガーされ、さらに、トランザクションの最後にコミットしたときにもう1つのログフラッシュがトリガーされます。パフォーマンスの違いは非常に重要です。 1つの大きなトランザクションで作業をテストするには、テストテンプレートの実際の作業部分で次のコードを使用します。
-- Actual work BEGIN TRAN; DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i += 1; END; COMMIT TRAN;
私のシステムでは、この作業は7秒で完了し、1,758回のログフラッシュがトリガーされました。 2つのオプションの比較は次のとおりです。
#transactions log flushes duration in seconds -------------- ------------ -------------------- 1000000 1000036 193 1 1758 7
ただし、通常のOLTPワークロードでは、異なるセッションから送信された多くの小さなトランザクションを、同じセッションから送信された1つの大きなトランザクションに置き換えるオプションは実際にはありません。
完全に耐久性のあるトランザクションと遅延した耐久性のあるトランザクション
SQL Server 2014以降では、遅延耐久性と呼ばれる機能を使用できます。これにより、通常の完全な耐久性の保証を犠牲にすることで、異なるセッションによって送信された場合でも、多くの小さなトランザクションでワークロードのパフォーマンスを向上させることができます。遅延した永続トランザクションをコミットする場合、SQL Serverは、ログバッファーのフラッシュをトリガーせずに、コミットログレコードがログバッファーに書き込まれるとすぐにコミットを確認します。ログバッファは、いっぱいになったときなど、前述の他の条件のためにフラッシュされますが、遅延した永続トランザクションがコミットされたときはフラッシュされません。
この機能を使用する前に、それが自分に適しているかどうかを慎重に検討する必要があります。パフォーマンスの観点から、その影響は、小さなトランザクションが多数あるワークロードでのみ重要です。ワークロードが主に大規模なトランザクションに関係している場合、パフォーマンス上の利点はおそらく見られません。さらに重要なのは、データ損失の可能性を認識する必要があるということです。アプリケーションが遅延した永続的なトランザクションをコミットするとします。コミットレコードはログバッファに書き込まれ、すぐに確認されます(制御は呼び出し元に戻されます)。ログバッファがフラッシュされる前にSQLServerで電源障害が発生した場合、再起動後、アプリケーションはトランザクションがコミットされたと見なしても、回復プロセスによってトランザクションによって行われたすべての変更が取り消されます。
では、いつこの機能を使用しても大丈夫ですか?明らかなケースの1つは、SentryOneのMelissa Connorsのこの例のように、データの損失が問題にならない場合です。もう1つは、再起動後に、データベースに反映されなかった変更を特定する手段があり、それらを再現できる場合です。状況がこれら2つのカテゴリのいずれにも該当しない場合は、誘惑にかかわらずこの機能を使用しないでください。
遅延した永続トランザクションを処理するには、DELAYED_DURABILITYというデータベースオプションを設定する必要があります。このオプションは、次の3つの値のいずれかに設定できます。
- 無効 (デフォルト):データベース内のすべてのトランザクションは完全に耐久性があるため、コミットするたびにログバッファーのフラッシュがトリガーされます
- 強制 :データベース内のすべてのトランザクションは永続的に遅延するため、コミットによってログバッファフラッシュがトリガーされることはありません
- 許可 :特に明記されていない限り、トランザクションは完全に耐久性があり、それらをコミットするとログバッファのフラッシュがトリガーされます。ただし、COMMIT TRANステートメントまたは(ネイティブにコンパイルされたプロシージャの)アトミックブロックのいずれかでオプションDELAYED_DURABILITY =ONを使用すると、その特定のトランザクションは永続的に遅延するため、コミットしてもログバッファフラッシュはトリガーされません。
テストとして、テストテンプレートの準備セクションで次のコードを使用します(データベースオプションが[強制]に設定されていることに注意してください):
-- Preparation SET NOCOUNT ON; USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY = Forced; DROP TABLE IF EXISTS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname = N'PerformanceV3';
そして、実際の作業セクションで次のコードを使用します(注意、1,000,000の小さなトランザクション):
-- Actual work DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i += 1; END;
または、データベースレベルで許可モードを使用してから、COMMIT TRANコマンドでWITH(DELAYED_DURABILITY =ON)を追加することもできます。
私のシステムでは、作業が完了するまでに22秒かかり、95,407回のログフラッシュがトリガーされました。より多くのログレコードが生成されるため、1つの大きなトランザクション(7秒)として作業を実行するよりも長くなります(トランザクションごとに、トランザクションの開始用、変更用、トランザクションのコミット用に1つずつ覚えておいてください)。ただし、ログフラッシュの数が1,000,000を超えて100,000未満に減少したため、1,000,000の完全に耐久性のあるトランザクションを使用して作業を完了するのにかかった193秒よりもはるかに高速です。さらに、耐久性が遅れているため、1つの大きなトランザクションを使用するオプションがない別のセッションからトランザクションが送信された場合でも、パフォーマンスが向上します。
大きなトランザクションとして作業を行うときに遅延耐久性を使用してもメリットがないことを示すには、最後のテストの準備部分で同じコードを保持し、実際の作業部分で次のコードを使用します。
-- Actual work BEGIN TRAN; DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i += 1; END; COMMIT TRAN;
実行時間は8秒(完全に耐久性のある1つの大きなトランザクションの場合は7秒)、ログフラッシュは1,759回(1,758回の場合)でした。数値は基本的に同じですが、永続的なトランザクションが遅れると、データが失われるリスクがあります。
4つのテストすべてのパフォーマンス数値の概要は次のとおりです。
durability #transactions log flushes duration in seconds ------------------- -------------- ------------ -------------------- full 1000000 1000036 193 full 1 1758 7 delayed 1000000 95407 22 delayed 1 1759 8
ストレージクラスメモリ
遅延耐久性機能は、高頻度と低遅延を必要とする多数の小さな更新トランザクションを含むOLTPスタイルのワークロードのパフォーマンスを大幅に向上させることができます。問題は、データが失われるリスクがあることです。データの損失を許容できないが、遅延した耐久性のようなパフォーマンスの向上が必要な場合はどうなりますか。ログバッファは、コミットごとにフラッシュされるのではなく、いっぱいになったときにフラッシュされます。私たちはみんなケーキを食べて食べるのが好きですよね?
これは、SQL Server 2016 SP1以降で、ストレージクラスメモリ(別名NVDIMM-N不揮発性ストレージ)を使用して実現できます。このハードウェアは基本的にメモリモジュールであり、メモリグレードのパフォーマンスを提供しますが、そこにある情報は保持されるため、電源が切れても失われることはありません。 SQL Server 2016 SP1の追加により、ログバッファーをそのようなハードウェア上で永続化されたものとして構成できます。これを行うには、SCMをWindowsのボリュームとして設定し、ダイレクトアクセスモード(DAX)ボリュームとしてフォーマットします。次に、通常のALTER DATABASE
パフォーマンスの数値など、この機能の詳細については、KevinFarleeによるWindowsServer 2016 / SQL Server2016SP1のストレージクラスメモリを使用したトランザクションコミットレイテンシの高速化を参照してください。
不思議なことに、SQL Server 2019は、永続化されたログキャッシュシナリオだけでなく、ストレージクラスメモリのサポートを強化します。このようなハードウェアへのデータファイル、ログファイル、およびインメモリOLTPチェックポイントファイルの配置をサポートします。あなたがする必要があるのは、それをOSレベルのボリュームとして公開し、DAXドライブとしてフォーマットすることです。 SQL Server 2019はこのテクノロジーを自動的に認識し、エンライテンドで動作します モード、デバイスに直接アクセスし、OSのストレージスタックをバイパスします。未来へようこそ!
ユーザーデータベースとtempdb
もちろん、tempdbデータベースは、SQLServerを再起動するたびにモデルデータベースの新しいコピーとして最初から作成されます。そのため、一時テーブル、テーブル変数、ユーザーテーブルのいずれに書き込む場合でも、tempdbに書き込んだデータを復元する必要はありません。再起動するとすべて消えます。これを知っていると、SQLServerはロギング関連の要件の多くを緩和できます。たとえば、遅延耐久性オプションを有効にするかどうかに関係なく、コミットイベントはログバッファフラッシュをトリガーしません。さらに、SQL Serverはトランザクションのロールバック、または必要に応じて作業の取り消しをサポートするのに十分な情報しか必要としないため、ログに記録する必要のある情報の量は削減されますが、トランザクションのロールフォワードや作業のやり直しは必要ありません。その結果、tempdb内のオブジェクトへの変更を表すトランザクションログレコードは、同じ変更がユーザーデータベース内のオブジェクトに適用された場合と比較して小さくなる傾向があります。
これを実証するために、PerformanceV3で以前に実行したのと同じテストを実行しますが、今回はtempdbでのみ実行します。データベースオプションDELAYED_DURABILITYがDisabled(デフォルト)に設定されている場合の多くの小さなトランザクションのテストから始めます。テストテンプレートの準備セクションで次のコードを使用します。
-- Preparation SET NOCOUNT ON; USE tempdb; ALTER DATABASE tempdb SET DELAYED_DURABILITY = Disabled; DROP TABLE IF EXISTS dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname = N'tempdb';
実際の作業セクションで次のコードを使用します。
-- Actual work DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i += 1; END;
この作業により、5,095のログフラッシュが生成され、完了するまでに19秒かかりました。これは、完全な耐久性を備えたユーザーデータベースでの100万回を超えるログフラッシュと193秒と比較されます。これは、ログレコードのサイズが縮小されているため、ユーザーデータベースの耐久性の遅延(95,407ログフラッシュと22秒)よりもさらに優れています。
1つの大きなトランザクションをテストするには、準備セクションを変更せずに、実際の作業セクションで次のコードを使用します。
-- Actual work BEGIN TRAN; DECLARE @i AS INT = 1; WHILE @i <= 1000000 BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i += 1; END; COMMIT TRAN;
1,228回のログフラッシュと9秒の実行時間を取得しました。これは、ユーザーデータベースの1,758回のログフラッシュと7秒の実行時間と比較されます。実行時間は似ており、ユーザーデータベースでは少し速くなりますが、テスト間のわずかな違いである可能性があります。 tempdbのログレコードのサイズが縮小されるため、ユーザーデータベースと比較してログフラッシュが少なくなります。
DELAYED_DURABILITYオプションをForcedに設定してテストを実行することもできますが、前述のように、コミットイベントはtempdbのログフラッシュをトリガーしないため、これはtempdbに影響を与えません。
ユーザーデータベースとtempdbの両方でのすべてのテストのパフォーマンス測定値は次のとおりです。
database durability #transactions log flushes duration in seconds -------------- ------------------- -------------- ------------ -------------------- PerformanceV3 full 1000000 1000036 193 PerformanceV3 full 1 1758 7 PerformanceV3 delayed 1000000 95407 22 PerformanceV3 delayed 1 1759 8 tempdb full 1000000 5095 19 tempdb full 1 1228 9 tempdb delayed 1000000 5091 18 tempdb delayed 1 1226 9
シーケンスオブジェクトのキャッシュ
おそらく、ログバッファのフラッシュをトリガーする驚くべきケースは、シーケンスオブジェクトのキャッシュオプションに関連しています。例として、次のシーケンス定義を考えてみましょう。
CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- the default cache size is 50;
新しいシーケンス値が必要になるたびに、次のようにNEXTVALUEFOR関数を使用します。
SELECT NEXT VALUE FOR dbo.Seq1;
CACHEプロパティはパフォーマンス機能です。これがないと、新しいシーケンス値が要求されるたびに、SQLServerは回復のために現在の値をディスクに書き込む必要がありました。実際、これはNOCACHEモードを使用したときに得られる動作です。代わりに、オプションがゼロより大きい値に設定されている場合、SQL Serverは、キャッシュサイズの要求数ごとに1回だけ回復値をディスクに書き込みます。 SQL Serverは、シーケンスタイプとしてサイズ設定された2つのメンバーをメモリに保持し、1つは現在の値を保持し、もう1つはリカバリ値の次のディスク書き込みが必要になるまでに残っている値の数を保持します。電源障害が発生した場合、再起動時に、SQLServerは現在のシーケンス値をリカバリ値に設定します。
これはおそらく例を使って説明する方がはるかに簡単です。 CACHEオプションを50(デフォルト)に設定した上記のシーケンス定義を検討してください。上記のSELECTステートメントを実行して、新しいシーケンス値を初めて要求します。 SQL Serverは、前述のメンバーを次の値に設定します。
On disk recovery value: 50, In-memory current value: 1, In-memory values left: 49, You get: 1
49以上のリクエストはディスクにアクセスせず、メモリメンバーを更新するだけです。合計50件のリクエストの後、メンバーは次の値に設定されます。
On disk recovery value: 50, In-memory current value: 50, In-memory values left: 0, You get: 50
新しいシーケンス値を再度要求すると、リカバリ値100のディスク書き込みがトリガーされます。その後、メンバーは次の値に設定されます。
On disk recovery value: 100, In-memory current value: 51, In-memory values left: 49, You get: 51
この時点でシステムに電源障害が発生した場合、再起動後、現在のシーケンス値は100(ディスクから回復された値)に設定されます。シーケンス値の次の要求は101を生成します(回復値150をディスクに書き込みます)。 52から100の範囲のすべての値が失われました。電源障害の場合のように、SQL Serverプロセスのクリーンでない終了が原因で失う可能性が最も高いのは、キャッシュサイズと同じ数の値です。トレードオフは明らかです。キャッシュサイズが大きいほど、リカバリ値のディスク書き込みが少なくなるため、パフォーマンスが向上します。同時に、停電の場合に2つのシーケンス値の間に生成される可能性のあるギャップが大きくなります。
これはすべて非常に簡単で、おそらくあなたはそれがどのように機能するかをよく知っています。驚くべきことは、SQL Serverが新しいリカバリ値をディスクに書き込むたびに(この例では50リクエストごとに)、ログバッファも強化されることです。これは、ID列プロパティには当てはまりません。SQLServerは、シーケンスオブジェクトの場合と同じように、IDに対して内部的に同じキャッシュ機能を使用しますが、サイズを制御することはできません。デフォルトでオンになっており、サイズはBIGINTとNUMERICの場合は10000、INTの場合は1000、SMALLINTの場合は100、TINYINTの場合は10です。必要に応じて、トレースフラグ272またはIDENTITY_CACHEスコープ設定オプション(2017+)を使用してオフにすることができます。 IDキャッシュ関連のリカバリ値をディスクに書き込むときにSQLServerがログバッファをフラッシュする必要がない理由は、テーブルに行を挿入するときにのみ新しいID値を作成できるためです。電源障害が発生した場合、コミットされなかったトランザクションによってテーブルに挿入された行は、システムの再起動時にデータベース回復プロセスの一部としてテーブルから引き出されます。そのため、再起動後にSQL Serverが、コミットしなかったトランザクションで作成されたものと同じID値を生成した場合でも、行がテーブルから引き出されたため、重複する可能性はありません。トランザクションがコミットされた場合、これによりログフラッシュがトリガーされ、キャッシュ関連のリカバリ値の書き込みも持続します。したがって、Microsoftは、リカバリ値のIDキャッシュ関連のディスク書き込みが行われるたびにログバッファをフラッシュする必要があるとは感じていませんでした。
シーケンスオブジェクトでは、状況が異なります。アプリケーションは新しいシーケンス値を要求できますが、データベースに保存することはできません。コミットしなかったトランザクションで新しいシーケンス値を作成した後に電源障害が発生した場合、再起動後、SQLServerがアプリケーションにその値に依存しないように指示する方法はありません。したがって、再起動後に以前に生成されたシーケンス値と等しい新しいシーケンス値が作成されないようにするために、SQL Serverは、新しいシーケンスキャッシュ関連の回復値がディスクに書き込まれるたびにログフラッシュを強制します。このルールの1つの例外は、シーケンスオブジェクトがtempdbで作成される場合です。もちろん、システムの再起動後にtempdbが新たに作成されるため、このようなログフラッシュは必要ありません。
頻繁なログフラッシュのパフォーマンスへの悪影響は、非常に小さいシーケンスキャッシュサイズを使用する場合、および1つのトランザクションで大量のシーケンス値を生成する場合、たとえば、テーブルに多数の行を挿入する場合に特に顕著です。シーケンスがないと、このようなトランザクションは、ほとんどの場合、ログバッファーがいっぱいになったときにログバッファーを強化し、さらにトランザクションがコミットしたときにもう一度強化します。ただし、このシーケンスでは、リカバリ値のディスク書き込みが行われるたびにログがフラッシュされます。そのため、キャッシュなしモードは言うまでもなく、小さいキャッシュサイズの使用は避けたいと考えています。
これを実証するために、テストテンプレートの準備セクションで次のコードを使用します。
-- Preparation SET NOCOUNT ON; USE PerformanceV3; -- try PerformanceV3, tempdb ALTER DATABASE PerformanceV3 -- try PerformanceV3, tempdb SET DELAYED_DURABILITY = Disabled; -- try Disabled, Forced DROP TABLE IF EXISTS dbo.T1; DROP SEQUENCE IF EXISTS dbo.Seq1; CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- try NO CACHE, CACHE 50, CACHE 10000 DECLARE @db AS sysname = N'PerformanceV3'; -- try PerformanceV3, tempdb
そして、実際の作業セクションの次のコード:
-- Actual work SELECT -- n -- to test without seq NEXT VALUE FOR dbo.Seq1 AS n -- to test sequence INTO dbo.T1 FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;
このコードは、1つのトランザクションを使用して、SELECT INTOステートメントを使用して1,000,000行をテーブルに書き込み、挿入された行の数と同じ数のシーケンス値を生成します。
コメントで指示されているように、PerformanceV3とtempdbの両方で、NO CACHE、CACHE 50、およびCACHE 10000を使用してテストを実行し、完全に耐久性のあるトランザクションと遅延した耐久性のあるトランザクションの両方を試してください。
システムで得られたパフォーマンスの数値は次のとおりです。
database durability cache log flushes duration in seconds -------------- ------------------- --------- ------------ -------------------- PerformanceV3 full NO CACHE 1000047 171 PerformanceV3 full 50 20008 4 PerformanceV3 full 10000 339 < 1 tempdb full NO CACHE 96 4 tempdb full 50 74 1 tempdb full 10000 8 < 1 PerformanceV3 delayed NO CACHE 1000045 166 PerformanceV3 delayed 50 20011 4 PerformanceV3 delayed 10000 334 < 1 tempdb delayed NO CACHE 91 4 tempdb delayed 50 74 1 tempdb delayed 10000 8 < 1
注目すべき興味深いことがたくさんあります。
NO CACHEを使用すると、生成されたすべての単一シーケンス値のログフラッシュを取得します。したがって、これを避けることを強くお勧めします。
シーケンスキャッシュサイズが小さい場合でも、大量のログフラッシュが発生します。おそらく、状況はNO CACHEの場合ほど悪くはありませんが、デフォルトのキャッシュサイズが50の場合、ワークロードが完了するまでに4秒かかったのに対し、サイズが10,000の場合は1秒未満でした。私は個人的に10,000を好みの値として使用しています。
In tempdb you don’t get log flushes when a sequence cache-related recovery value is written to disk, but the recovery value is still written to disk every cache-sized number of requests. That’s perhaps surprising since such a value would never need to be recovered. Therefore, even when using a sequence object in tempdb, I’d still recommend using a large cache size.
Also notice that delayed durability doesn’t prevent the need for log flushes every time the sequence cache-related recovery value is written to disk.
結論
This article focused on log buffer flushes. Understanding this aspect of SQL Server’s logging architecture is important especially in order to be able to optimize OLTP-style workloads that require high frequency and low latency. Workloads using In-Memory OLTP included, of course. You have more options with newer features like delayed durability and persisted log buffer with storage class memory. Make sure you’re very careful with the former, though, since it does incur potential for data loss unlike the latter.
Be careful not to use the sequence object with a small cache size, not to speak of the NO CACHE mode. I find the default size 50 too small and prefer to use 10,000. I’ve heard people expressing concerns that with a cache size 10000, after multiple power failures they might lose all the values in the type. However, even with a four-byte INT type, using only the positive range, 10,000 fits 214,748 times. If your system experience that many power failures, you have a completely different problem to worry about. Therefore, I feel very comfortable with a cache size of 10,000.