案件の概要
SQL Server 2012の時と違うのは、急激なメモリアロケーションだけで、解放処理が行われていないことです。Standby Cacheが溜まっていることから、前回と同じようにNUMAに関する問題の可能性が高いのですが、SQL Server 2012では、メモリマネージャが大きく変わっているので、これは異なる問題です。この問題を調査するには現象を再現させて、詳細を調査するしかないと思いました。
バッファプール
幸いにも、前回の問題を開発部門へ報告する時に再現環境を構築していたので、この環境を流用してSQL Server 2008でも再現を試みることにしました。数台のアプリケーションサーバーと高スペックなデータベースサーバーを用意し、お客様からアプリケーションを借りて再現環境を構築していたのです。
Standby Cacheをいっぱいにして、アプリケーションを実行するとあっさりと再現しました。SQL Serverは、暴走したかのように急激なメモリアロケーションを行います。パフォーマンスカウンターのBuffer Manager\Free pagesカウンターがぐんぐん上がっていきます。
Standby Cacheが溜まっていない時は、アプリケーションのラッシュをかけてもSQL Serverが確保するメモリはmax server memoryに設定していた64GBには及ばず、12GB位で安定します。しかし、現象が再現すると一気にSQL Serverは64GBまで確保してしまうのです。
しばらくデバッグして、原因が判明しました。前回と同じように、NUMAメモリのハンドリングに原因はありました。問題を説明する前に、少しSQL Server 2008のバッファプールについて、説明しておきましょう。
SQL Server 2012以前、SQL Serverが扱うメモリの大半はバッファプールでした。バッファプールの最大サイズは、max server memoryで設定することができ、最大値に達するまでの間、バッファプールはgrowフェーズという状態にあります。Growフェーズの間、バッファプールは必要に応じて徐々にメモリ確保(grow)していきます。メモリを確保する時の条件は、バッファプールのfree pageが一定量足りていないと判断した場合です。しかし、今回の場合、free pageが大量にあるにもかかわらず、メモリ確保が必要以上に行われているわけです。
バッファプールのメモリページは、NUMAノード (Buffer Node)、CPU (Buffer Partition)、の単位で管理されています。Free pageが十分に存在しているかどうかのチェックは、バッファプール全体ではなく、Buffer Partitionの単位で行われます。
起きていた現象は、次のようなことです。
バッファプールがgrowフェーズの時に、あるBuffer Partitionのfree pageが枯渇します。すると、そのBuffer Partitionに該当するCPUに所属しているスレッドがバッファプールへメモリを要求した時に、OSからメモリを確保してfree pageを充足しようとします。しかし、この時OSから返されたメモリページは、ローカルNUMAノードのメモリページではなかったとします。すると、そのページはfree pageとしては利用されず、対応するBuffer Nodeのaway listで管理されます。Free pageを充足しようとしたのですが、失敗しているわけです。Away listにあるページは、対応するBuffer Nodeがgrowする時にOSへ解放されます。この部分は、前回説明したSQL Server 2012と同じような仕組みです。
Buffer Partitionをgrowすべきかどうかという判断には、free pageが枯渇しているかどうかに加えて、Buffer Partitionが所属しているBuffer Nodeのaway listに1件でもページが存在しているかどうか、という条件があります。バッファプールには、Buffer Node毎にlazy writerというスレッドがいて、このスレッドは1秒間隔でBuffer Nodeをgrowすべきか(free pageを充足すべきか)どうかを常にチェックしています。
例えば、あるBuffer Partitionのfree page充足処理でローカルノードからのアロケーションに失敗し、他ノードのaway listへページが追加されたとします。すると、追加された側のBuffer Nodeを管理しているlazy writerスレッドは、away listにページが存在しているが故にそのBuffer Partitionに対するfree pageの充足処理が走ってしまいます。たとえそのノードに所属するBuffer Partitionが十分なfree pageを確保していたとしても充足してしまうのです。Lazy writerスレッドだけでなく、そのノードに該当するスレッドがバッファプールにメモリを要求した時も、away listにページが存在しているが故、同様にfree pageの充足処理が走ってしまいます。
まとめると、次のような状態に陥っていたのです。
• あるNUMAノードのOS Free/Zeroページが枯渇している一方で、別のノードに大量のFree/Zeroページが存在しています。この時、枯渇しているノードに所属しているスレッドのメモリアロケーションで返されるページは、必ず別のノードのメモリページです。
• Free/Zeroページが枯渇しているノードに所属するBuffer Partitionのfree pageが枯渇します。その為、このノードに所属するスレッド(lazy writerやユーザートランザクションなど)によってfree pageの充足処理が行われます。
• しかし、この充足処理ではローカルノードのページを確保できず、確保したページは他ノードのaway listへ追加します。
• Away listにページが追加されたことで、追加された側のノードでfree pageの充足処理が発動します。
• Free pageが枯渇しているBuffer Partition は一向にfree pageが充足されないままです。その為、そのノードに所属するスレッドによって充足処理が繰り返されますが、失敗を繰り返し、他ノードのaway listへページを追加し続けます。その結果、他ノードのfree pageが急激に増加します。(パフォーマンスカウンターのBuffer Manager\Free Pagesカウンターが急激に跳ね上がる)