先日、SQL Server と AppDomain という投稿をしました。
SQL Server では、ユーザー定義の CLR を使用していなくても、
- 空間データ型 (geography / geometry)
- 階層データ型 (hierarchyid)
- FORMAT 関数
等を使用した場合、CLR による実装が行われているため、CLR がロードされ AppDomain (アプリケーション ドメイン) が生成されていることについて触れました。
前回の投稿では「ロード」部分が主内容となっていましたが、本投稿では「アンロード」部分について触れてみたいと思います。
AppDomain のアンロード
SQL Server では、CLR がロードされた場合には、ERRORLOG / イベントログに次のような内容が出力されていました。
- AppDomain 6 (master.sys[runtime].5) created.
- AppDomain 6 (master.sys[runtime].5) が作成されました。
CLR がロードされ AppDomain が生成されると、そのことを示すログが SQL Server から出力されます。
ロードされた CLR の AppDomain は永続的にメモリ上に存在しているかというと「アンロード」される可能性があります。
アンロードされた AppDomain が発生した場合には、次のようなログとして出力が行われます。
- AppDomain 6 (master.sys[runtime].5) unloaded.
- AppDomain 6 (master.sys[runtime].5) はアンロードされました。
このようなログが出力されている場合は、AppDomain のアンロードが行われている状態になります。
前回の投稿でも触れましたが、AppDomain については、sys.dm_clr_appdomains から確認をすることができます。
以下のような appdomain_id : 3 / 6 の AppDomain が生成されている状態で、上述のメッセージの出力が発生したとします。
そうすると、DMV の取得内容は次のように変化します。
「AppDomain 6 (master.sys[runtime].5) unloaded.」の 6 の部分は、DMV の appdomain_id の部分を表しています。appdomain_id = 6 AppDomain がアンロードされていることが DMV で実際に確認することができていますね。
アンロードのメッセージが出力された場合は、該当の appdomain_id の AppDomain がアンロードされたことになります。アンロード後の初回アクセス時に CLR がロードされるため、初回アクセスのオーバーヘッドが発生します。
アンロード時の動作
ロードされるときには、拡張イベントの「assembly_load」等のイベントを取得することで動作の確認を行うことができました。
アンロードについては拡張イベントが用意されていないようなので、アンロード時にどのような動作が行われているかについては、スレッドの動作を確認する必要が出てくるかと思います。
今回は、Windbg でブレークポイントを設定してアンロード時の動作を確認してみます。
どの関数のコール時にブレークポイントを設定すればよいかを検討するため「x clr!AppDomain::Unload*」で、AppDomain のアンロードの関数を確認してみます。
いくつかの関数が確認できますね。
AppDomain のアンロードは頻繁に発生するわけではありませんので、今回は確認できたすべての関数に「bm clr!AppDomain::Unload*」でブレークポイントを設定してしまいます。
次のような AppDomain の生成状態となっている場合に、アンロードを発生させてみます。
ブレークポイントにより、デバッガーに制御が戻った際の、コールスタックとレジスターの情報が次のようになります。(今回は SQL Server 2012 を使用していますので、バージョンによって出力される情報の内容は多少変わると思います。)
「clr!AppDomian::UnloadById」がコールされています。この関数に指定するのは、AppDomain の ID となります。レジスターの rcx の内容を見ると、今回アンロードの対象となった appdomain_id は 0x7 であることが確認できます。
この事から appdomain_id = 7 の AppDomain がアンロード対象となったと判断することができます。
デバッガーで処理を継続させて、SQL Server に制御を戻してから DMV やログを確認すると、先ほどはロードされていた appdomain_id = 7 がアンロードされていることが確認できますね。
アンロードに関しては、一般的な CLR のアンロードと同様の方法で実装されているのかと思います。
メモリプレッシャーによるアンロード
AppDomain のアンロードですが、次のメッセージとセットでアンロードされるケースが多いようです。
- AppDomain 7 (master.sys[runtime].6) is marked for unload due to memory pressure.
- AppDomain 7 (master.sys[runtime].6) unloaded.
- メモリの負荷により、AppDomain 7 (master.sys[runtime].6) はアンロードするように設定されています。
- AppDomain 7 (master.sys[runtime].6) はアンロードされました。
メモリ負荷が増加すると、SQL Server は AppDomain をアンロードしてメモリを確保するという動作を行うことがあるようです。
アンロードされる AppDomain については、sys.dm_clr_appdomains に情報が記載されています。
cost と value の値が使用されて、アンロードされる AppDomain が制御されているようです。
cost が高く、value が小さい AppDomain が優先的に削除が行われるようですね。
メモリプレシャーの種類
メモリ負荷が高くなり、メモリプレッシャーが発生すると AppDomain がアンロードされる可能性が出てきます。
SQL Server 2012 (11.x) 以降のメモリ管理の変更点 で記載されていますが、SQL Server 2012 以降は、CLR によって割り当てられるメモリは、max_server_memory の制御下となりました。
SQL Server の Process のメモリ割り当てを確認する際には、
- Working Set
- Private Bytes
の 2 種類を確認することがありますが、CLR によって使用されるメモリは Managed Heap となり、Working Set として確保がされているようです。
この領域に確保されているということは Lock Pages in Memory (LPM / メモリ内のページのロック / SeLockMemoryPrivilege) の対象外となっており、ページアウトの対象となる (ワーキングセットのトリミング) 可能性があるということを表すかもしれません。
私が確認できている範囲では、AppDomain がアンロードされるメモリプレシャーとしては、
- SQL Server 以外のプロセスがメモリを必要とした場合
- SQL Server 内で CLR 以外の領域でメモリを必要とした場合
の 2 種類のケースは該当しているようで、これらのケースであれば、実際に該当するワークロードを発生させ、アンロードを誘発させることができました。(windbg で Unload のブレークポイントを発生させる際には、これらの 2 種類の方法を使用しました)
「1.」については、Sysinternals の Testlimit を使用すると検証を容易に行うことができます。
次のようなコマンドで Testlimit によるメモリ確保を続けていくと、SQL Server のプロセスで確保しているメモリに対してもメモリプレッシャーが発生し、ロードされている AppDomain のアンロードを発生させることができます。
Testlimit64.exe -d 1
「2.」については、データキャッシュの肥大化では AppDomain のアンロードを誘発することはできなかったのですが、SQL Server の各クエリのワーク領域のメモリとなる「ワークスペースメモリ」を肥大化させることで、SQL Server 内の特定用途のメモリの確保を起因として、AppDomain のアンロードを発生させることができます。
RML Utilities の ostress を使用して、大量のスレッドを生成してワークスペースメモリを肥大化させます。
Target Server Memory の大半を Granted Worksace Memory が占めており、Workspace Memory の確保待ち (Memory Grants Pending) が発生している状態を作り出しています。
このような状態となった場合、AppDomain のアンロードもメモリ確保のため方法の一つとなるようで、AppDomain のアンロードが発生します。
最後に
調べてみた範囲では、AppDomain のアンロードを禁止するための方法は見当たりませんでした。
そのため、メモリ負荷が高い環境では AppDomain のアンロードが発生するのを防止することはできないようです。
AppDomain に関連するエラーメッセージとしては次のようなものがあります。
select * from sys.messages where language_id = 1041 and text like '%AppDomain%'
AppDomain がアンロードされた後、次回の CLR 使用時に AppDomain を生成する際には、リソースのオーバーヘッドがあり、メモリが必要となります。AppDomain の作成の失敗についてのエラーがありますので、ロード時に必要となるメモリが確保できない場合は、クエリ実行でエラーが発生する可能性があるかもしれません。
また、CLR のロード時のオーバーヘッドは CLR 関連の待機の発生により、クエリ実行時間に影響を亜猛可能性もあります。
AppDomain のアンロードはメモリプレッシャーによって発生する可能性がありますので AppDomain のアンロードが発生している環境では、
- SQL Server 内 / 外のプロセスのどちらが起因してメモリプレッシャーが発生しているか?
- SQL Server 内のメモリ使用状況が起因している場合、どの領域のメモリ使用状況が影響を与えているか?
を調査し、どのようなメモリ使用を緩和させるのことで AppDomain のアンロードを抑制できるかを考えていく必要があるのではないでしょうか。