ハンドルリーク
参照カウントやハンドルカウントの数え違いなどにより、不要になったハンドル(ファイル・イベント・スレッド・GDIオブジェクト・アクセストークンなどカーネルオブジェクトへの参照)が解放されずに積み上がっていく状態。
Handle Count
プロセスが現在開いているハンドルの総数を表す、Windowsプロセスの計測値。
ハンドルカウント
failure path(異常系の途中失敗経路)
timeout・reconnect・early returnなど、正常系ではなく処理が途中で失敗して抜けるコード経路。資源の解放漏れが起きやすい。
異常系経路
メモリリーク
確保したメモリを解放し忘れ、Private BytesやCommitが増え続ける現象。
Private Bytes
プロセスが専有しているコミット済みメモリ量を表す、Windowsプロセスの計測値。
プライベートバイト
CreateEvent
イベントオブジェクトを作成し、そのハンドルを返すWin32 API。呼び出し元は使用後にCloseHandleでハンドルを解放する必要がある。
CloseHandle
プロセスのハンドルテーブルからエントリを1つ削除するWin32 API。「ファイルを閉じる」ことそのものではない。
構造化ログ(structured log)
文章ではなくkey=valueのように項目を決めて出力し、あとで機械的に集計できるログの形式。
Application Verifier
Windowsのuser-modeアプリに対するランタイム検証ツールで、OS API利用や資源の扱い方を監視し誤用の検出や意図的な失敗の注入を行う。
RAII(Resource Acquisition Is Initialization)
オブジェクトの生成時に資源を確保し、デストラクタでの解放を保証することで、途中失敗時にも資源が漏れないようにするC++の設計パターン。
二次障害(secondary failure)
資源の枯渇などが積み重なった結果として、本来の原因(漏らした場所)から離れた場所で表面化する障害。
GDIオブジェクト
ペン・ブラシ・デバイスコンテキストなどGDIが管理する描画資源で、プロセスあたりの既定上限に達するとそれ以上作成できなくなる。
長時間運転後クラッシュの切り分け
数日〜数週間の連続稼働後にだけ発生する障害を、資源の増え方と失敗経路の観測から原因究明する調査手法。
Thread Count
プロセスが持つスレッド数を表す計測値。
スレッド数
timeout・reconnect・early returnなどの途中失敗経路でCloseHandleが呼ばれないと、ハンドルリークの原因になることがある。
ハンドルリークは、reconnectなどの操作後にHandle Countが戻らずじわじわ増え続けることで確認できる。
メモリリークはPrivate Bytes(やCommit)がじわじわ増えることで確認できる。
CreateEventが返したイベントハンドルは、使用後にCloseHandleで解放することを前提とする。CloseHandle自体はファイル・プロセス・スレッド・ミューテックスなど多くのカーネルオブジェクトのハンドルを閉じるもので、CreateEventに限らない。
create/openとclose/disposeの対応が崩れている箇所は、sessionId・resourceId・actionを持つ構造化ログで探すことが本記事の推奨対応である。
後編で扱うApplication Verifierを使う前に、自前の構造化ログでlifetimeの崩れを追えるようにしておくのが土台になる。
RAII型でハンドルの生存期間をオブジェクトの生存期間に結び付けると、途中失敗の経路でも自動的にCloseHandleが呼ばれるためハンドルリークを防げる。
Windowsのハンドルを保持するRAII型では、デストラクタが保持しているハンドルに対してCloseHandleを呼んで解放する。
Application Verifierは低リソース時のfailure pathを前倒しで踏ませることで、ハンドルリークのような出にくい壊れ方を早期に表面化させる有効な手段とされる。
ハンドルリークは、蓄積の結果として新しいevent/file/threadを作るAPIの失敗という形で、数週間後に二次障害として表面化することが多い。クラッシュ地点は漏らした場所ではなく最後の被害者であることが多い。
session/resource lifecycleの構造化ログをたどることで、落ちた場所ではなく漏らした場所という二次障害の本当の原因を特定することが本記事の推奨対応である。
途中失敗の経路も含めてCloseHandleを漏れなく呼ぶことが、ハンドルリークの防止そのものになる。
GDIオブジェクトのハンドルを漏らした場合、プロセスあたりの既定上限(GDIProcessHandleQuotaで256〜65,536)がカーネルオブジェクトの理論上限より低いため、GUIが同居するアプリではこちらに先に達し、新しい描画資源を作れなくなって二次障害として表面化することがある。CreateEventのようなカーネルハンドルの漏れ自体はGDIの枠を消費しない。
長時間運転後にだけ落ちるアプリの切り分けでは、Private Bytesだけでなく Handle Countも必ず見ることが推奨される。
落ちた瞬間の例外だけでなく、operation/session/resource lifecycleの構造化ログを先に整えておくことが、長時間運転後クラッシュの切り分けに効く。
長時間運転後クラッシュの切り分けでは、月単位の再現を待たず、timeout・reconnect・early returnなどのfailure pathを短いループで大量に踏んで再現を圧縮することを本記事は推奨している。
長時間運転後にだけ落ちるアプリの切り分けでは、Handle Countと合わせてPrivate Bytesも継続的に見ることで、主犯がメモリかハンドルかを切り分けられる。
長時間運転の切り分けでは、スレッドリークの可能性を除外するため、Handle Count・Private Bytesと並んでThread Countも一緒に見ることが基本になる。