案件の概要
ある日、お客様から「SQL Server 2012が異常な動作をする。」と報告を受けました。パフォーマンステストをしていると、突然、急激なメモリの確保と解放が発生し、この間ユーザートランザクションのスループットが低下してしまうらしいのです。SQL Serverは、本来、必要に応じて徐々にメモリを確保していきます。しかし、ユーザートランザクションのラッシュをかけている最中、突如、暴走したかのようにSQL Serverが急激なメモリの確保・解放を行い始めるのです。
「こ、これは!」
テストフェーズだったということもあり、お客様には現象発生時のXPerfと完全メモリダンプを採取してもらいました。しかし、XPerfのログは、なぜかモジュールのアドレスに関する情報がとれておらず、デバッグシンボルとマッチングできずうまく解析できませんでした。ホットスポットになっている関数名はわからなかったのですが、sqldkというdllの名前だけは記録されていて確認できました。Sqldk.dllは、SQLOSが実装されているモジュールです。メモリアロケーションは、SQLOSの仕事なのでsqldk.dllの部分に何か問題があるというのは間違いなさそうです。
メモリダンプをのぞいてみると、ユーザートランザクションを実行中のスレッドがメモリアロケーションの最中だったということがわかりました。通常、SQL Serverのメモリアロケーションは、それほど時間を要すことはないので妙です。また、リソースモニターと呼ばれるスレッドが並行してaway blockと呼ばれるメモリを解放している最中であることもわかりました。Away blockとは、メモリアロケーションをしたスレッドが、ローカルのNUMAノードからメモリを確保できなかった場合に退避しておくメモリ領域です。
どうやら今回の現象は、メモリアロケーションでローカルノードからのメモリ確保に失敗していることが関係していそうだということがわかりました。パフォーマンスカウンターを確認すると利用可能なメモリはたくさん余っているので、そもそも、ローカルノードからメモリを確保できないケースってどういう時なんだろうと思い、カーネル側のソースコードをのぞいてみました*。
*お客様は、Windows Server 2008にSQL Server 2012をインストールしていたので、確認したのはWindows Server 2008のコードです。
10~15分くらい眺めて、思わず「こ、これは..」とつぶやいてしまいました。Windowsはメモリをいくつかの種類に分けて管理しています。利用可能なメモリは、Free Page, Zero Page, Standby Cacheと呼ばれるメモリの合計です。これらのうち、Free Pageと Zero Pageは、NUMAノード毎に管理されているのですが、Standby Cacheについては、NUMAノード単位で管理されていなかったのです。