RDS SQL Server のメモリプレッシャーは、明確なアラートがトリガーされるずっと前から、パフォーマンスを静かに低下させる可能性があります。CPU スパイクとは異なり、メモリの問題は多くの場合、ディスク I/O の上昇やスロークエリとして最初に現れます。これらの症状は、バッファープールやクエリワークスペースに原因を突き止めるまで、別の問題を示唆しているように見えます。このトピックでは、メモリプレッシャーの存在を確認し、発生している 4 つのプレッシャータイプのうちどれに該当するかを特定し、それぞれに適した修正を適用する方法について説明します。
このトピックに関連する一般的な兆候:
-
エラー 701:クエリを実行するためのメモリが不足しています
-
エラー 802:バッファープールページを割り当てられませんでした
-
クエリ待機統計における
RESOURCE_SEMAPHORE待機 -
明確な原因のないページの予測存続期間 (PLE) の低下
-
データ量が一定であるにもかかわらずディスク I/O が上昇
-
クエリの tempdb へのスピル
SQL Server によるメモリの使用方法
SQL Server は動的メモリ管理を使用します。デフォルトでは、可能な限り多くの利用可能なメモリを要求し、バッファープールを使用してデータページをキャッシュし、プランキャッシュを使用して実行プランを格納します。そして、OS がメモリ不足の状態を通知した場合にのみメモリを解放します。したがって、メモリ使用率が高いことは、適切に利用されているインスタンスでは想定内の正常な状態です。
メモリプレッシャーは、現在のワークロードがメモリでサポートできる範囲を超えたときに発生します。これには 4 つの形式があります。
| プレッシャータイプ | 発生事象 | 主な兆候 |
|---|---|---|
| バッファープールのプレッシャー | データページが再利用される前に削除され、ディスクからの読み取りが強制される | PLE の低下、ディスク I/O の上昇、キャッシュヒット率の低下 |
| クエリワークスペースのプレッシャー | クエリがソートやハッシュ処理に十分なメモリを取得できない | RESOURCE_SEMAPHORE 待機、tempdb へのスピル、クエリ実行の低速化 |
| 非バッファープール (盗用メモリ) のプレッシャー | プランキャッシュ、ロック、接続、共通言語ランタイム (CLR) などの内部コンポーネントが、本来バッファープールに供給されるべきメモリを消費する | Stolen_Server_Memory_Kb の増加、バッファープールの縮小、間接的な PLE の低下 |
| 外部メモリプレッシャー | ホスト OS のメモリが不足し、SQL Server にキャッシュの解放を強制する | バックアップ中や、大規模なデータセットを持つ小規模インスタンスでのパフォーマンス変動 |
RDS は OS メモリを保護するためにパラメーターを自動的に設定しますが、ワークロードに対してメモリが不足しているインスタンスでは、依然として外部プレッシャーが発生する可能性があります。例えば、4 コア、8 GB のインスタンスで 2 TB のデータを管理しており、特に物理バックアップが開始された場合などです。
モニタリングツールの可用性
このトピックの診断手順では、さまざまなツールを使用します。開始する前に、お使いのインスタンスでどのツールが利用可能かを確認してください。
| ツール | 場所 | 可用性 |
|---|---|---|
| [モニタリングとアラート] | インスタンス詳細ページの左側のメニュー | すべてのエディションとバージョン |
| 自律サービス > Performance Insight | 左側のナビゲーションペイン > [自律サービス] > [パフォーマンス最適化] | サポート対象のリージョンとバージョンのみ。クラウドディスクを使用する RDS SQL Server 2008 R2 インスタンスでは利用できません |
| 動的管理ビュー (DMV) クエリ (SQL コンソール) | インスタンスに対して直接実行 | すべてのエディションとバージョン |
メモリプレッシャーの有無の判断
モニタリングとアラートでの主要メトリックの確認
-
RDS インスタンスリストに移動し、リージョンを選択して、対象のインスタンス ID をクリックします。
-
左側のメニューで、[モニタリングとアラート] をクリックします。
-
以下のメトリックを確認します。
| メトリック | 確認事項 |
|---|---|
mem_usage |
90% を超える値は一般的に正常です。下記のインスタンス固有のしきい値を参照してください。 |
Page_life_expectancy |
算出したしきい値を下回る持続的な低下は、バッファープールのプレッシャーを示します。 |
bufferpool_hit_ratio |
Page_Reads の上昇と並行して比率が低下している場合、バッファープールのプレッシャーが確認されます。 |
インスタンス固有の mem_usage のしきい値:
| インスタンスメモリ | アラートのしきい値 |
|---|---|
| 512 GB | 97% |
| 256 GB | 96% |
| 192 GB以下 | 95% |
リンクサーバー、CLR、インメモリ OLTP、または多数の拡張イベント (XEvents) やトレースなど、メモリオーバーヘッドの高い機能が有効になっている場合、実際のメモリ消費量は設定された max server memory の値を大幅に超えることがあります。その場合は、max server memory を低く設定して、OS と管理サービスのためのヘッドルームを確保してください。
インスタンスの健全な PLE しきい値の計算:
従来の 300 秒というしきい値は、大容量メモリのインスタンスには適用できません。代わりに次の式を使用してください。
PLE しきい値 = (バッファープールのメモリ (GB) / 4) × 300
例:
-
16 GB インスタンス (バッファープールは約 12 GB):健全な PLE > [900秒]
-
128 GB インスタンス (バッファープールは約 110 GB):健全な PLE > [8,250秒]
Performance Insight でのメモリ構成の分析
モニタリングとアラートで潜在的な問題が示された場合は、Performance Insight を使用して、メモリが何に使用されているかの内訳を取得します。
-
左側のメニューで、[自律サービス] > [パフォーマンス最適化] を選択し、[Performance Insight] タブを開きます。
-
右上隅の [カスタムメトリック] をクリックします。[メモリ使用量の分類] と [高度なメモリ使用量] のグループからメトリックを追加します。
[メモリ使用量の分類メトリック]
これらのメトリックは、SQL Server Memory Manager パフォーマンスオブジェクトカウンターに対応しています。
| メトリック | 測定対象 | 異常の兆候 |
|---|---|---|
Total_Server_Memory_Kb |
SQL Server が OS からコミットした総メモリ量 | メモリが設定された制限に達したかどうかを追跡します |
Database_Cache_Memory_Kb |
データページキャッシュに使用されるバッファープールのメモリ | Page_Reads の上昇と並行した急激または持続的な低下は、バッファープールのプレッシャーを示します |
Stolen_Server_Memory_Kb |
内部コンポーネント (プランキャッシュ、ロック、接続) によってバッファープールから取得されたメモリ | 総メモリの 40% を一貫して超えている場合、バッファープールが縮小し、PLE が低下する原因となります |
Free_Memory_Kb |
現在割り当てられていないコミット済みメモリ | PLE の低下と同時に 0 付近にとどまる場合、メモリの枯渇が確認されます。0 付近であること自体は正常です |
SQL_Cache_Memory_Kb |
実行プランキャッシュの合計 (盗用メモリの主要コンポーネント) | キャッシュヒット率が低い状態で継続的に増加。パラメーター化されていないアドホッククエリで一般的です。 |
Optimizer_Memory_Kb |
クエリのコンパイル中に使用されるメモリ | 一貫して高い値、または高密度のスパイクは、コンパイルストームを示します |
Lock_Memory_Kb |
ロック構造 (行ロック、ページロック、テーブルロック) のためのメモリ | 通常、大規模な未コミットトランザクションやロックのエスカレーションにより、数百 MB に急増します |
Connection_Memory_Kb |
クライアント接続状態を維持するためのメモリ | 接続リークや接続ストームによる急激な増加 |
単一のメトリックだけで全体像を把握することはできません。これらを総合的に、かつワークロードの文脈で解釈してください。
高度なメモリ使用量メトリック (盗用メモリの内訳)
これらのメトリックは、盗用メモリを内部の割り当てカテゴリ別に分類し、どのコンポーネントがメモリを消費しているかを正確に可視化します。
| メトリック | 追跡対象 | 異常の兆候とアクション |
|---|---|---|
CACHESTORE_SQLCP_KB |
アドホッククエリプランキャッシュ (アドホッククエリ、プリペアドステートメント、サーバーサイドカーソル) | 単一使用プランが多い高い値:optimize for ad hoc workloads を有効にし、パラメーター化を追加します |
CACHESTORE_OBJCP_KB |
ストアドプロシージャ、関数、トリガーのオブジェクトプランキャッシュ | 多すぎるストアドプロシージャやパラメーター スニッフィングによる高い値:オブジェクトを監査し、OPTION(RECOMPILE) を使用するか、SQL Server 2022 でパラメーターに依存するプランの最適化 (PSP) を使用します |
CACHESTORE_PHDR_KB |
SQL テキストの解析および代数化ツリーキャッシュ | 非常に複雑な SQL (例:数千の値を持つ IN 句):ハードコードされた定数リストの代わりにテーブル値パラメーター (TVP) を使用します |
MEMORYCLERK_SOSNODE_KB |
SQL Server スケジューリング構造のための非均一メモリ アクセス (NUMA) ノードメモリ | 小規模な High-availability Edition インスタンスで数週間または数か月にわたるゆっくりとした継続的な増加:SOSNODE メモリ増加の問題をご参照ください |
MEMORYCLERK_SQLCLR_KB |
CLR マネージドコードのためのメモリ | 不適切なリソース破棄を行うカスタム CLR アセンブリによる異常な増加:CLR コードをレビューし、未使用のアセンブリを削除します |
MEMORYCLERK_SQLSTORENG_KB |
tempdb 内の行バージョンストアを含むストレージエンジンメモリ | スナップショット分離または Read Committed スナップショット分離 (RCSI) が有効で、長時間実行される未コミットトランザクションがある場合の増加:長時間のトランザクションを監視して終了させ、sys.dm_tran_version_store_space_usage を確認します |
USERSTORE_SCHEMAMGR_KB |
テーブル定義と一時オブジェクトのスキーマメタデータキャッシュ | tempdb で多くの一時テーブルが急速に作成・破棄される場合に上昇:新しいものを作成するのではなく、一時テーブルを再利用します |
プレッシャータイプの特定と最適化
以下の 3 段階のワークフローは、DBA が通常これらの問題に取り組む方法と一致しています。つまり、サーバーレベルで確認し、次にメモリ構成によって原因を切り分け、そして行動に移します。
ステージ1:プレッシャータイプの確認
まず PLE と待機統計を確認します。これにより、どのシナリオが適用されるか、次にどのセクションを読むべきかが決まります。
| 観測事象 | プレッシャータイプ | 参照先 |
|---|---|---|
| PLE が一貫してしきい値を超えており、大きな変動がない | メモリプレッシャーなし | 代わりに CPU または I/O のトラブルシューティングに集中します |
PLE が頻繁にしきい値を下回り、Page_Reads が上昇している |
バッファープールのプレッシャー | シナリオ1:バッファープールキャッシュの不足 |
PLE は正常に見えるが、Memory Grants Pending > 0 またはクエリが tempdb にスピルしている |
クエリワークスペースのプレッシャー | シナリオ3:クエリメモリ許可の不足 |
PLE が低下し、Stolen_Server_Memory_Kb が増加している |
非バッファープールのプレッシャー | シナリオ2:プランキャッシュの肥大化 |
ステージ2:メモリ構成の分析
バッファープールのプレッシャーが確認されたら、Database_Cache_Memory_Kb と Stolen_Server_Memory_Kb を比較します。
-
盗用メモリが高い (全体の 20% 以上):メモリの大部分がデータページ以外の用途 (プランキャッシュ、接続、CLR、その他のコンポーネント) に使われています。これは割り当ての問題です。スケールアップを検討する前に、誤用されているメモリを解放してください。
-
データベースキャッシュが優勢で、空きメモリが 0 に近い:バッファープールはすでに利用可能なメモリのほとんどを使用していますが、それでもホットデータセットを保持できていません。これは容量の問題です。ホットデータ量を最適化するか、スケールアップしてください。
ステージ3:診断に基づくアクション
プレッシャーのタイプとその原因がわかったら、以下の関連するシナリオの最適化を適用します。その後、同じメトリックを使用して修正が機能したことを確認します。
シナリオ1:バッファープールキャッシュの不足
症状
-
Performance Insight で PLE の低下とともに
Page_Readsが大幅に上昇する -
クエリのデータ量が変わっていないのに、ディスク I/O スループットが増加する
-
PLE がインスタンス固有のしきい値を一貫して下回り続ける
最適化オプション
| オプション | 使用するケース |
|---|---|
| メモリのスケールアップ | ホットデータセットが利用可能なメモリより大幅に大きい場合。I/O レイテンシが主要なボトルネックである場合 |
| インデックスの追加または最適化 | フルテーブルスキャンがホットページを追い出している場合。カバーインデックスを追加すると、スキャンがシークに変換されます |
| コールドデータのアーカイブ | 履歴データがホットデータとバッファープールのスペースを競合している場合 |
| データ圧縮の有効化 | 同じバッファープールメモリに、より多くのデータページが収まります |
| 断片化されたインデックスの再構築 | 断片化されたインデックスはバッファープールのスペースを無駄にします。再構築によりページ密度が向上します |
| 未使用インデックスの削除 | 未使用のインデックスは読み取り中にバッファープールのメモリを消費し、書き込みのオーバーヘッドを増加させます |
シナリオ2:プランキャッシュの肥大化
症状
-
Stolen_Server_Memory_Kbが継続的に増加し、プランキャッシュの内部クォータ制限に近い高い値を維持する -
盗用メモリが増加するにつれて、
Database_Cache_Memory_Kbが下方に圧迫される -
頻繁なキャッシュの削除とプランの再コンパイルにより CPU 使用率が上昇する
-
バッファープールが縮小するため、ディスク I/O が上昇する
-
アプリケーションが、リテラル値が異なるが構造的に同一の SQL ステートメントを多数送信する (例:
WHERE id = 123、WHERE id = 456)
診断手順
ステップ 1:単一使用プランの割合を確認する。
一度しか実行されないプランの割合が高い場合、キャッシュが再利用不可能なプランで埋め尽くされていることを示します。
WITH PlanStats AS
(
SELECT
cp.usecounts,
cp.size_in_bytes / 1024.0 / 1024.0 AS size_mb
FROM sys.dm_exec_cached_plans AS cp
WHERE cp.cacheobjtype = 'Compiled Plan'
)
SELECT
total_plans = COUNT(*),
single_use_plans = SUM(CASE WHEN usecounts = 1 THEN 1 ELSE 0 END),
single_use_ratio_pct =
CAST(
100.0 * SUM(CASE WHEN usecounts = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)
AS DECIMAL(5,2)
),
total_plan_mb = CAST(SUM(size_mb) AS DECIMAL(18,2)),
single_use_plan_mb = CAST(SUM(CASE WHEN usecounts = 1 THEN size_mb ELSE 0 END) AS DECIMAL(18,2)),
single_use_mem_pct =
CAST(
100.0 * SUM(CASE WHEN usecounts = 1 THEN size_mb ELSE 0 END)
/ NULLIF(SUM(size_mb), 0)
AS DECIMAL(5,2)
)
FROM PlanStats;
単一使用プランがプランキャッシュメモリの 50% 以上を占める場合、キャッシュは再利用不可能なプランに浪費されています。
ステップ 2:単一使用プランを生成している SQL ステートメントを特定する。
-- プランキャッシュメモリを最も多く消費している上位20のアドホッククエリ
SELECT TOP 20
cp.usecounts AS [execution_count],
cp.size_in_bytes / 1024 AS [plan_size_kb],
cp.objtype AS [object_type],
st.text AS [sql_text]
FROM sys.dm_exec_cached_plans AS cp
CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) AS st
WHERE cp.objtype = 'Adhoc'
AND cp.usecounts = 1
ORDER BY cp.size_in_bytes DESC;
結果が、同じ構造でリテラル値が異なる多くのステートメントを示している場合、根本原因はパラメーター化されていないクエリです。
最適化オプション
オプション 1:optimize for ad hoc workloads を有効にする (推奨)
このパラメーターは、アドホッククエリの初回実行時に軽量なスタブのみをキャッシュします。完全なプランは、クエリが再度実行された場合にのみ保存されます。これにより、CPU のコンパイルオーバーヘッドに影響を与えることなく、単一使用プランによる盗用メモリを削減できます。
-
RDS インスタンスリストに移動し、インスタンス詳細ページを開きます。
-
左側のメニューで、[パラメータ設定] をクリックします。
-
optimize for ad hoc workloadsを検索して有効にします。
オプション 2:アプリケーションでクエリをパラメーター化する
文字列連結の代わりにパラメーター化クエリを使用するようにアプリケーションコードを変更します。アプリケーションがオブジェクトリレーショナルマッピング (ORM) フレームワークを使用している場合は、自動パラメーター化または第 2 レベルキャッシュが利用可能かどうかを確認してください。
オプション 3:データベースレベルで強制パラメーター化を有効にする
アプリケーションの変更が不可能な場合は、影響を受けるデータベースで強制パラメーター化を有効にします。
-
RDS インスタンスリストに移動し、インスタンス詳細ページを開きます。
-
左側のメニューで、[データベース管理] をクリックします。
-
対象のデータベースの [詳細の表示] をクリックします。
-
[基本情報] セクションで、
parameterizationをFORCEDに設定し、[送信] をクリックします。
強制パラメーター化は、まずステージング環境でテストしてください。複雑な述語を持つ一部のクエリでは、実行プランの変更がパフォーマンスの低下を引き起こす可能性があります。
シナリオ3:クエリメモリ許可の不足
症状
ORDER BY、GROUP BY、DISTINCT、またはハッシュ結合など、ソートやハッシュ処理を伴うクエリは、メモリ許可と呼ばれるメモリのブロックを要求します。この許可が満たされない場合、2 つの障害モードが発生します。
| 障害モード | 観測事象 |
|---|---|
| キューイング (待機) | クエリが SUSPENDED 状態にとどまり、RESOURCE_SEMAPHORE 待機タイプが表示され、Memory Grants Pending カウンターが上昇する |
| ディスクへのスピル | クエリは実行されるが非常に遅く、tempdb の I/O 書き込みが急激に増加する。実行プランの Sort または Hash Match 演算子に黄色の警告アイコンが表示され、「Operator used tempdb to spill data」というメッセージが表示される |
このプレッシャータイプは PLE を低下させません。PLE が正常に見えるにもかかわらず上記の症状が見られる場合、問題はバッファープールではなくクエリワークスペースにあります。
診断手順
現在待機中または大量のメモリ許可を消費しているクエリを見つける:
-- リアルタイムのメモリ許可
SELECT
mg.session_id,
mg.request_time,
mg.grant_time, -- NULLはクエリが許可を待機している (RESOURCE_SEMAPHORE) ことを意味します
(mg.requested_memory_kb / 1024.0) AS requested_mb,
(mg.granted_memory_kb / 1024.0) AS granted_mb,
(mg.required_memory_kb / 1024.0) AS required_mb,
mg.queue_id,
mg.wait_order,
st.text AS sql_text,
qp.query_plan
FROM sys.dm_exec_query_memory_grants AS mg
CROSS APPLY sys.dm_exec_sql_text(mg.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(mg.plan_handle) AS qp
ORDER BY mg.granted_memory_kb DESC;
過去に最大の許可を要求したクエリを見つける:
SELECT TOP 20
qs.execution_count,
(qs.max_grant_kb / 1024.0) AS max_grant_mb,
(qs.total_grant_kb / qs.execution_count / 1024.0) AS avg_grant_mb,
(qs.total_worker_time / qs.execution_count / 1000.0) AS avg_cpu_ms,
qs.last_execution_time,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1) AS query_text,
qp.query_plan
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp
ORDER BY qs.max_grant_kb DESC;
query_plan 列で、Spill キーワードを検索するか、Sort または Hash Match 演算子の警告アイコンを探します。
根本原因と修正
| 根本原因 | 症状 | 修正 |
|---|---|---|
| 古い統計 | オプティマイザが 1 行と見積もるが 100 万行を処理する。小さな許可要求が頻繁なスピルを引き起こす | 特に大規模なデータ変更後は、定期的に統計を更新する |
| 大規模なクエリの同時実行 | 複数のメモリ集約的なレポートが同時に実行される。後続のクエリが RESOURCE_SEMAPHORE でキューに入る |
メモリ集約的なレポートジョブを、オフピーク時に順次実行するようにスケジュールする |
| ソート/ハッシュ操作におけるインデックスの欠落 | カバーインデックスがない複雑な JOIN、GROUP BY、または ORDER BY は、SQL Server に大規模なインメモリハッシュテーブルの構築を強制する |
オプティマイザがインメモリソートを回避できるように、カバーインデックスまたは順序付きインデックスを追加する |
| 広範な行選択 | SELECT * または長いテキスト列がメモリ許可サイズを増大させる (メモリ許可サイズ = 推定行数 × 平均行幅) |
必要な列のみを選択する。大規模な複数結合クエリをより単純なステップに分割する |
SOSNODE メモリ増加の問題
ミラーリングアーキテクチャを持つ RDS SQL Server High-availability Edition インスタンス、特に小規模なインスタンス (2 コア 4 GB または 4 コア 8 GB) では、MEMORYCLERK_SOSNODE_KB が数週間または数か月にわたってゆっくりと継続的に増加する傾向を示すことがあります。この増加はビジネスワークロードのピークとは無関係であり、自然に減少することはありません。
これは、SQL Server における既知の SOSNODE オブジェクトのメモリリークです。Microsoft は、影響を受けるバージョン全体でこの問題を完全には解決していません。リークしたメモリは max server memory にカウントされ、バッファープールを減少させ、最終的に測定可能なメモリボトルネックを引き起こします。プレッシャーが増加すると、エラーログに 701 エラーが表示されることがあります。
緩和策:
-
影響を軽減するために、より新しい SQL Server バージョンにアップグレードします。
-
観測されたリーク率に基づき、オフピーク時に 2~4 か月ごとに計画的な再起動をスケジュールして、蓄積されたリークメモリを解放します。
RDS は SOSNODE の増加を自動的に監視します。メトリックがしきい値に達すると、RDS はプロアクティブな運用保守 (O&M) タスクをトリガーします。コンソールの [イベントセンター] で対応するタスクを表示できます。