このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
セキュリティインサイトは、すべてのCloudflareアカウントに対して実用的なセキュリティ推奨事項を提供します。これらの洞察を検出するために、すべてのアカウント、ゾーン、DNSレコードの定期スキャンを実施し、潜在的なセキュリティリスクや設定ミスを探します。
しかし、2つの重要な問題が浮上しています。第一に、スキャンの頻度が低すぎました。スキャンは1~2週間に一度しか実行されていなかったため、新たに発生したセキュリティリスクは検出されないままとなる可能性があったのです。第二に、多くのFreeプランアカウントで自動スキャンがオプトインされており、多くのアカウントがまったくスキャンされていません。
スキャンの頻度が低い、または存在しないリスクが高まっています。自動化された攻撃が加速するにつれ、セキュリティの設定ミスを発見するためのウィンドウは縮小していきます。すべてのお客様についてこうした問題を発見することは、すべての人のためのより良いインターネットを構築するという当社の目的からみても重要なことです。
スキャン頻度を増やし、すべてのアカウントの自動スキャンを有効にするには、スキャンスループットを平均で約10倍、つまり毎秒10回から1秒あたり100回に増加させる必要があると計算しました。しかし、当社のシステムはすでに、その負荷に悩まされていました。数百万のイベントが処理待ちのバックログを満杯になっていたのです。 APIが頻繁にタイムアウトしていました。プロセスはクラッシュしていました。システムを修正し、拡張できるようにする必要がありました。
これは、Security Insightsのスキャンスループットを10倍以上に向上させ、数百万のお客様に対するセキュリティインサイトを提供し、すべてのお客様に対するスキャン頻度を2倍にした方法についてです。この改善をどのように実現したかについて、以下で説明します。
大まかに説明すると、Cloudflareの自動セキュリティスキャンはスケジューラによってトリガーされます。アカウントまたはゾーンがスキャンの対象になると、スケジューラはApache Kafka(オープンソースの分散型イベントストリーミングプラットフォーム)にメッセージ(またはメッセージ)を公開します。これらのメッセージは、特定のアセットや設定をスキャンする専用のGoマイクロサービスなど、多くのチェッカーに向けられます。
すべてのメッセージに対し、各チェッカーはその結果(検出したセキュリティインサイト)を内部APIに送信し、APIはこれらをPostgresデータベースに永続化します。
Apache Kafkaは厳密にはキューではありません。分割されたイベントストリームです(最近になってキューのセマンティクスも獲得しましたが)。パーティション内では、メッセージは順番に消費され、処理されなければなりません。これは、メッセージが順番に消費される可能性がありますが、順不同で処理される一般的なキューとは異なります。したがって、コンシューマーグループ内では、パーティションごとに1つのアクティブコンシューマーのみを持つことができます。
これは、当社にとって以下の2つの結果をもたらします。
パーティションをさらに追加することで、スケーリングを試みることもできました。しかし、これは他の多くのサービスと共有するKafkaブローカー自体のリソースの使用量が増加することになります。これは、最初にコードとアーキテクチャの改善を目的とした、最後の手段として確保しました。
メッセージを順番に消費するしかありませんが、複数のメッセージを一度に消費することを防ぐものはありません。
メッセージをバッチで消費するようにチェッカーを変更し、各メッセージを個別のゴルーチンで処理しました。その代償として、プロセスの途中でプロセスがクラッシュした場合、やり直す作業が増え、メモリ使用量がわずかに増加することになります。当社では、どちらも許容できる範囲でした。
一部のチェッカーによって処理されるメッセージの中には、他のメッセージよりも処理に時間がかかるものがあります。たとえば、あるアカウント/ゾーンが、別のアカウント/ゾーンよりもはるかに多くのアセットを持つことがあります。最悪の場合、これらのメッセージは、数秒または数ミリ秒の平均的な処理と比較して、数分から数時間かかることがあります。
当社が選択したのは、非常にシンプルなアプローチで、消費者グループとチェッカーを「低速レーン」と「高速レーン」の2つに分けました。メッセージの処理が遅いか、速いかを迅速に判断できます。「高速レーン」チェッカーが遅いメッセージを遭遇した場合、それをスキップします。
これにより問題が解決されました。遅いメッセージは専用のリソースと時間を確保することができ、最小限の遅延で処理され、速いメッセージは通常の高速ペースで進めることができました。
当社が見つけたインサイトはすべて、Postgresデータベースに書き込まれます。これは、チェッカーがインサイトのリストを使用して呼び出す単一のAPIエンドポイントによって処理されます。実装は次のようになりました:
for _, issue := range issues {
_, err = tx.Exec(ctx, `INSERT INTO table ... VALUES ($1, $2, ...) ON CONFLICT DO UPDATE ...`, ...)
if err != nil {
return err
}
}
情報通の読者であれば、このコードは大量のインサイトの場合、インサイトごとにデータベースとの往復を行うことに気づくでしょう。観測された最大規模は50万で、これは1回のAPI呼び出しで500万回のラウンドトリップ、クエリ、トランザクションを行ったことに相当します。
私たちは当初、Postgresにおける一括挿入の絶対的基準であるCOPYを一時テーブルに転送することを試みました。しかし、この方法ではPostgresシステムのテーブルが肥大化していることが判明しました。
当社は、ハイブリッド型のアプローチにたどり着きました。
問題数が閾値を下回った場合はUNUNESTを使用
問題数がこの閾値を超えた場合はCOPYを使用
これにより、膨大なインサイトのための比較的高速な挿入(秒)と、小さなインサイトのセットに対するより高速な挿入(ミリ秒)の両方の長所が得られました。
スケーリングを試みた際、内部APIにおけるいくつかの奇妙な動作に気付きました。
大量のリクエストがクライアント側のタイムアウトをトリガーしていた
多くのチェッカーが、処理時間の20~90%を単一のAPI呼び出しに費やしていました
大量のスキャンをトリガーすると、スループットが高くなり始め、\n低下する
これらの問題はすべて同じ根本原因がありました:遅延。
当社のプライマリデータベースは、オレゴン州ポートランドにあります。しかし、当社のAPIはポートランドとアムステルダムの両方でアクティブに実行されていました。光速であっても、ポートランドとアムステルダムの間の往復遅延は50ミリ秒です。
この遅延の結果、アムステルダムのAPIインスタンスからのデータベースクエリーに大幅な時間がかかり、クライアント側の接続プールからの接続が開いた状態が保たれるようになりました。APIに大量のリクエストを送信すると、接続プールはすぐに使い果たされる可能性があり、無料の接続を待機するタイムアウトが発生しました。当社の平均API呼び出しはポートランドでは10ミリ秒で完了しますが、アムステルダムではほぼ3秒でした。
しかし、なぜメッセージスループットが低下するのでしょうか?各チェッカープロセスは、消費するKafkaストリームのパーティションのセットを割り当てられます。当社のAPIは負荷分散されています。プロセスの存続時間を通じて接続を開いておくので、一部のプロセスはAmsterdam APIに接続し、あるプロセスはポートランドAPIに接続しました。ポートランドにリンクされたパーティションはすぐに処理されましたが、アムステルダムに向かうプロセスで消費されたものは遅れていました。
Kafkaラグ(単一のコンシューマーグループ内での処理待ちのメッセージ数)を分割します。この場合、30のパーティションがあることに注意してください。正確に15のパーティションが遅れているのがわかります(3月10日03:00頃より遅くゼロに到達するか、ゼロに近づいている行)。これは、ロードバランサーがAPIエンドポイント間でトラフィックを均等に分割するためです。
これは簡単な修正でした。APIをアクティブ-パッシブに切り替え、アクティブAPIがプライマリデータベースに従うようにしました。遅延の問題は一夜にして解決しました。
Kafkaを拡張したのです。データベースクエリーを最適化しました。APIを修正したのです。しかし、まだ問題がありました。スキャンが時間内にほぼ均等に分散されることを確認する必要があるのです。Kafkaトピックは、時間ベースの保持ポリシーを使用しているため、すべてのスキャンを同時にキューに入れることはできませんでした。スキャンはKafkaに蓄積され、最終的に処理される前に削除されます。
スケジューラーは、スキャンを均一に配布するのが得意ではありませんでした。一度に起動されるスキャンの数は、急増し、予測不可能でした。1週間を通してある時点で、何十万ものスキャンが数分以内に相互トリガーされるのです。何が起こっていたのでしょうか?
スケジューラは、固定された繰り返し期間でスキャンをトリガーします。擬似コードでは、スケジューラは次のようになります:
Loop forever:
Find accounts where last_scheduled_at + scanning frequency <= now
For each account:
Trigger scan for account
Trigger scan for all zones in the account
Update last_scheduled_at = now
last_scheduled_atがデータベース内の多くのアカウントと類似しており、これが不均一な原因になっていたことにすぐ気付きました。
しかし、完全に均等な分布であっても、スキャン頻度を増やすと、この問題はさらに悪化したでしょう。たとえば、スキャン頻度を15日ごとから7日ごとに変更した場合、53%のアカウントが突然スキャンの対象になることを意味します。
このロジックにはさらに問題がありました。アカウントによっては、非常に多くのゾーンを持つものもあります。これらのアカウントがスケジュールされた時は、すべてのゾーンのスキャンが連鎖的に行われました。このため、Kafkaパーティションが飽和状態になり、はるかに小さなアカウントのスキャンに遅れが生じていました。
こうした問題を解決するために、当社は3つの重要な変更を行いました。
ゾーンを個別にスケジュールすることは、大規模なアカウントの問題を解決する明らかな方法でした。last_scheduled_at時間をランダム化することで(そして、このプロセス中にスキャンが遅延しないことを確認する)、データベースに存在する不均一な状態を修正することができました。
適応型レート制限は少し興味深いものです。レート制限を使用することで、スキャン頻度を変更した場合のスキャンの急増の問題を解決できます。たとえば、スキャン頻度を7日ごとに増やしたい場合、5,000万件のアカウントがある場合、レート制限を最大83スキャン/秒に設定することで、7日間に均等に分散させることができます。
しかし、さらに1,000万件のアカウントを追加した場合はどうでしょうか?そして、このレート制限により、これらすべてのアカウントをスキャンするのに8日かかります。ここで、適応部分が活躍します。レート制限は、アカウントとゾーンの総数、およびスキャン頻度に基づいて、30分ごとに非同期的に再計算されます。これにより、何千、何百万ものアカウントやゾーンをオンボードできても、オンボードでスキャンを継続することができます。
func computeRate(free, pro, biz, ent int64) rate.Limit {
r := float64(free)/freeScanInterval.Seconds() +
float64(pro)/proScanInterval.Seconds() +
float64(biz)/bizScanInterval.Seconds() +
float64(ent)/entScanInterval.Seconds()
// Guard against zero counts. We always want to schedule at least one scan per second.
if r < 1 {
r = 1
}
// Increase rate limit beyond the 'perfect' value, to have a buffer in case of any downtime
// or spikes in load.
r *= rateLimitBufferFactor
return rate.Limit(r)
}
これらの修正により、チェッカーあたり7日間移動する平均スループットが、時間の経過とともに10倍以上増加しました。
今回の改善前は、1秒あたり約10回のスキャンを実行していました。目標とするスループットの1秒あたり100スキャンとのギャップは大きいように感じられました。私たちは、この問題にさらに多くのリソースを投入することや、Kafkaのトピックにさらに多くのパーティションを分割すること、つまりアーキテクチャ全体を放棄することについて議論しました。
しかし、当社の修正プログラムは違いをもたらしました。現在、Security Insightsは、ピーク時のスケジュール時に1秒あたり120回以上のスキャンを維持しており、当社の10倍の改善目標を上回っています。内部APIはタイムアウトしなくなり、Kafkaラグのメトリクスもずっと健全になりました。こうしたスケーラビリティの改善により、すべての無料アカウントとゾーンの自動スキャンを有効にし、すべてのお客様のスキャン頻度を高めることができました。
システムの安定性が向上したことで、これまで制約があった新機能の構築に自信が持てました。きめ細かなオンデマンドスキャンを実行する機能を追加しました。Cloudflareアカウント、ゾーン、インサイト、またはインサイトタイプを手動で再スキャンできるようになりました。
Cloudflareダッシュボードのセキュリティ概要ページから、詳細なオンデマンドスキャンを開始します
私たちが学んだ教訓は、何かを廃棄する前に、既存のシステムを深く理解することが重要であるということです。コード、SQLクエリ、ログ、メトリクス(特にメトリクス)を精査することで、ポッドやパーティションを追加するだけで容量を増やすことができました。私たちの仮定を疑い、奇妙に見えるメトリクスを調べ、簡単な近道(APIクライアント側のタイムアウトを増やすなど)を拒否することで、より安定した耐障害性の高いシステムを構築しました。
問題により多くのリソースを投入することが時には解決策になるかもしれませんが、Cloudflareでは、エンジニアリングが問題を解決する方法だと考えています。
セキュリティインサイトのスキャンは、すべてのCloudflareプランでデフォルトで有効になっています。今すぐCloudflareダッシュボードにログインして、セキュリティインサイトを確認し、管理しましょう。