GitLab シークレット検出 ADR 007: VectorscanベースのGoスキャンエンジンへの切り替え
背景
このADRはシークレット検出スキャンのために、RE2ベース(ポート)のRubyスキャンエンジンからVectorscanベース(ポート)のGoスキャンエンジンに切り替えるための決定を記録しています。
以前にRubyが選択された理由とそれに関連する問題に関する歴史的な経緯を以下に示します。
以前にRubyが選択された理由は?
シークレットプッシュ保護(SPP)機能開発の初期において、SDスキャンの実行にRubyを選択した主な理由は2つありました:
SPP機能をそのまま構築するために期待されていたチームのベロシティはかなり高く、そこに厳しい時間的制約が加わったことで、品質をあまり犠牲にすることなく最も抵抗の少ないアプローチを選択することになりました。チームのRubyへの親しみとGitLab Railsとの統合の容易さを考えると、Ruby Gem内にSPP機能を構築することは理にかなっていました。
Rubyにポートされたバリアントのにおいて、RE2エンジンを使用することで、もう一方のバケット候補であるGoの組み込み正規表現エンジンよりもレイテンシとメモリ消費の面で大幅に優れたパフォーマンスを発揮したことで、その決定に自信を持てました。
シークレット検出サービス(SDS)の構築に向けた技術的調査フェーズに進んでも、いくつかの理由から同じ言語(Ruby)とエンジン(RE2ポート)を使用するという決定を維持しました:
シークレット検出サービス(SDS)の初期スコープはシークレット検出に関連する_あらゆること_を含むものでした。つまり、スキャンのための特定のソースからのファイルの取得、DBへのメタデータの更新、失効などのポスト処理アクションなどが挙げられます。このアイデアのもとでは、GitLab Railsから既存のシステム(ワーカー、キャッシュ、DBなど)を活用して統合することができました。しかし後に、ステートレスなサービスとしてスコープを縮小し、Pre- およびPost-処理ステップはGitLab Rails側で処理しながら、与えられたペイロードのスキャンのみを担当するようになりました。
SDのRuby Gemをベンダーとしてを組み込むことによるSPP機能のアクティブな開発があり、移行によってSPP機能のリリースがさらに遅れることになっていました。厳格なリリースタイムラインがあったため、それは許容できませんでした。非破壊的なアプローチが必要でした。
RunwayはJason(現在もまだ)自己管理とDedicated環境へのデプロイをサポートしていませんでした。Cloud Connectorは労力のかかる選択肢でした。Runwayはそれをサポートする計画がありましたため、進行中の作業(ベンダーとして組み込まれたGem)を再利用する一時的なアプローチがより理にかなっていました。
RE2の正規表現パフォーマンスはレイテンシとメモリ消費の面でJasonのGoの組み込み正規表現よりも_十分に_優れていました。Goの代替正規表現エンジンは探っていませんでした。
Rubyを選択することによるいくつかの癖
特にHTTPリクエストを通じてスキャンを提供する場合に全体的なパフォーマンスに影響する他の重要な要素を見逃すほど、意思決定においてRuby RE2の生の正規表現パフォーマンスを過度に強調しました。見逃した要因のいくつかは、サーバースループット、リクエストレイテンシ、メモリ消費+GC、リソース使用率によるコスト効率などで、Rubyは以下の理由からこれらすべての要素でパフォーマンスが低下します:
RubyはそのランタイムVMから正規表現処理まで一般的に高いメモリフットプリントを持ち、GCは最適化されていません(他のインタープリタ型言語と同様)。メモリに対処するために“サブプロセス内でのスキャン"アプローチを検討しましたが、GRPCがプロセスのフォーキングをサポートしていないため、SDSで採用することができませんでした。
グローバルインタープリタロック(GIL)は1プロセス1CPUあたり1リクエストしか許可せず、他のリクエストはより長い間ブロックされ、gitプッシュ操作で顧客をブロックする可能性があります。
並行性の組み込みサポートがありません。RubyはGILにより複数のCPUコアとスレッドを効果的に利用できないため、並列性も簡単には実現できません。
RubyにはGRPCの第一級サポートが欠けているため、内部ではCベースのGRPCライブラリを使用しています。C-Rubyバインディングは追加のオーバーヘッドをもたらし、Go向けに持っているような言語固有の最適化も欠如しています。
SDSをさまざまなスキャン対象タイプにわたってスキャンを実行する唯一のプラットフォームとする計画を考慮すると、高スループットと低レイテンシは必須要件になります。GRPCサーバーのメトリクスを見ると、RubyはリストJasonの中で最もパフォーマンスが低い部類に入ります。
効率的なバイナリ実行ファイルを生成するための第一級サポートがないため、シークレット検出スイートのすべての機能にわたってシークレットスキャンエンジンを統一することが困難になります。
提案
異なるスキャンモード(分散サービスとして、および実行可能バイナリとして)でSDスキャンを実行するのに適した、他のパフォーマンスの高い言語と正規表現エンジンを探ることが提案されました。
決定で検討された言語(とエンジン)
RE2ポートを使用したRuby
VectorscanポートJasonを使用したGo
RE2(ネイティブ)とVectorscan(ネイティブ)を使用したC++
Std RegexおよびSdt Regexと並列ルール処理を使用したRust
結論
言語と正規表現エンジンの最終結論は**GoとVectorscan(ポート)**の組み合わせとなりました。
マイクロベンチマークとgRPCスループットテストの結果はこちらで確認できます。テストに使用したソースコードとデータセットはこちらで確認できます。
今後Goが適切な理由
Goは全体的なパフォーマンス(上記参照)においてRubyに対して大きな優位性(約8〜10倍)を持っており、その結果として正規表現パフォーマンスが中程度であっても補うことができます。
前述した他の制約からRubyを使用することを確信していたため、スパイク中にGoの他の正規表現の代替案を探りませんでした。サードパーティのGoの正規表現ライブラリの方がより優れたパフォーマンスを発揮する可能性がありました。
実行可能バイナリを構築するための第一級サポートにより、SDサービスへのアクセスが困難な領域でもGoは適しています。また、単一のGoソースコードでシークレット検出をサービス、バイナリ実行ファイル、CLIとして提供できるため、統合スキャナーエンジンの構築にも役立ちます。
第一級のGRPCサポートを持ち、監視、テスト、デバッグのための優れたツール群と豊富なミドルウェア拡張機能があります。GoはgRPCの型システムとのより優れた整合性があり、型関連のクラッシュが予期せず発生することが少なくなります。
SDSの将来における重要な役割を考えると、Goの保守性、依存関係管理、型システムは重要な優位性を持っています。
さらに、SDSへのCloud Connector統合において、Rubyで現在持っているような認証SDKの欠如というブロッカーが発生することがなくなります。
上記に加えて、効率的なGC、低ランタイムリソース消費、より速いデプロイを可能にする小さいコンテナサイズ、効率的なファイル処理、クリーンな並行性コードのためのネイティブGoルーティンとチャネル、コンパイル時チェック、成熟したプロファイリング/インストゥルメンテーションツール、クラウドネイティブ(コンテナ化およびマイクロサービスアーキテクチャに適している)など、GoはRubyよりも多くの利点があります。
なぜ今が適切なタイミングか
スキャナーエンジンは現在、即時のロードマップに重大な機能の予定がない安定した状態にあります。シークレット検出サービスはまだベータ版であり、SDSを積極的に使用する主要な機能は計画段階にあります。この決定をさらに遅らせると、SDSが成熟してより深く統合されるにつれて移行の複雑さと技術的負債が指数関数的に増加します。これが長期的な適切な技術的決定を確立するための唯一の機会です。
