GitLab シークレット検出 ADR 003: サブプロセス内でスキャンを実行する
背景
Pre-receive シークレット検出の正規表現評価のために実施されたスパイクにおいて、RE2ライブラリを使用したRubyがリストのトップに来ました。Rubyは許容できる正規表現パフォーマンスを持っていますが、言語の制限により、特にマルチスレッドとRactor(3.1以降)はI/Oバウンド操作の並列実行には適しているがCPUバウンド操作には適していないため、より多くのメモリ消費と並列性の欠如といったいくつかの落とし穴があります。
重要なパスでPre-receive シークレット検出機能を実行する際の懸念の1つはメモリ消費で、特にスキャンに関わる正規表現処理によるものです。コミットブロブの各行に300以上の正規表現ベースのルールパターンを実行するスキャンでは、メモリがコミットブロブサイズの最大約2〜3倍になる可能性があります1。占有されたメモリは、ガベージコレクターがトリガーされるまでスキャン操作が完了しても解放されません。最終的にサーバーがメモリで窒息する可能性があります。
元の議論Issueでは、これらの懸念事項のほとんどとその背景をカバーしています。
アプローチ
メインプロセスからフォークされた別のプロセス内でスキャンを実行することで、メモリ消費の問題をある程度解決できます。スキャンが完了したら、スポーンされたプロセスを終了させることで、Rubyのガベージコレクションを待つのではなく、占有されたメモリが即座にOSに解放されます。
技術的なソリューション
プロセスのライフサイクルを管理する際に考慮すべきシナリオがいくつかあります。これを怠ると、制御が効かなくなったオーファンプロセスが発生し、メモリを節約するという目的が台無しになります。この負担をParallelというRubyライブラリに委ねます。このライブラリはサブプロセスを通じて操作を実行する機能を提供します。親子プロセス間の通信、終了シグナルの処理、プロセス数の制限が容易であることから、私たちのニーズを達成するための適切なソリューションです。さらに、このドキュメントでは取り上げていない別の問題を解決する並列性(複数のサブプロセスを同時にスポーンして実行)もサポートしています。
サブプロセス内の操作のスコープ
新しいプロセスをスポーンするとOSからの追加レイテンシオーバーヘッド(ファイルデスクリプタのコピーなど)が発生するため、どの操作をサブプロセス内で実行するかを決定することが重要です。例えば、新しいサブプロセス内の各ブロブに対してスキャンを実行すると、メインプロセスでスキャンを実行する場合と比較して約2.5倍遅くなります。一方、各コミットリクエストに1つのサブプロセスを専用にすることも実現可能ではありません。すべてのブロブでのスキャンが単一のプロセス内で実行され、すべてのスキャンが完了するまでメモリをすばやく解放できず、ふりだしに戻ってしまいます。
バケットアプローチ: 2つの極端の中間として、累積サイズが固定チャンクサイズ(私たちの場合は2MiB)以上のすべてのブロブをグループ化し、以下に示すように各グループを別々のサブプロセス内で実行します。

補足
サブプロセス内で操作を実行することは、上記の問題に対する万能な解決策ではありません。通常のGCプロセスよりもより速くメモリを解放することでサーバーが窒息するのを遅らせると言えます。このアプローチでも、リクエストのバーストが大きすぎて処理できない場合は失敗する可能性があります^。
プロセス作成のライフサイクルに常にレイテンシオーバーヘッドが伴います。小さなコミット^の場合、スキャン操作のレイテンシはメインプロセスで実行した場合よりも遅くなる可能性があります。
リクエストごとにフォークされるプロセスの並列性係数または数は、現在
5プロセスに制限されており、これを超えると過剰フォークを避けるために保留中のリクエストがキューで待機します。リソースの枯渇につながるためです。
^閾値の数値は近日中にこちらに追加される予定です。
