代表的な面接トピック

システム設計面接:集中ログ管理システムの設計

システム設計難しい
Offer.cc 編集チーム公開日 更新日

質問

マルチテナントの集中ログ管理システムを設計してください。10万のサービスインスタンスが定常状態で毎秒100万イベントを生成し(1イベント平均600バイト)、インシデント発生時には毎秒300万イベントが15分間継続します。直近のログは30秒以内に検索可能になり、直近15分間に対する一般的なフィルタークエリはp95で2秒未満で完了する必要があります。収集、データモデル、バッファリング、パーティショニング、インデックス作成とアーカイブ、配信セマンティクス、テナント分離、キャパシティ、障害時の縮退、セキュリティ、検証について説明してください。

問題とスコープ

複数のエンジニアリングチームが共有する集中ログ管理システムを設計します。10万のサービスインスタンス、コンテナ、バッチジョブが定常状態で毎秒100万イベントを生成し、イベントごとの生データは平均600バイトです。大規模なインシデントでは、リクエストの失敗、リトライ、スタックトレースが同時に増加するため、毎秒300万イベントが15分間継続することがあります。イベント作成から検索可能になるまでのp99目標は30秒です。テナント、時間、サービス、環境、重大度(severity)フィルターを使用した直近15分間の一般的なクエリは、p95で2秒未満で完了する必要があります。

ユーザーは時間、サービス、ホスト、重大度、traceIdによるフィルタリングに加え、範囲指定された全文検索、ダウンロード、アラートを必要とします。ログクラスによって保持期間や信頼性の要件が異なります。デバッグログは保持期間を短く設定でき、緊急時にはサンプリングされる場合があります。運用エラーログには高耐久な保持が必要です。セキュリティ監査ログには通常の縮退ポリシーを適用できません。システムにはテナントクォータ、フィールドのマスキング(redaction)、アクセス監査、ライフサイクル管理、履歴データの復元も必要です。

スケール、レイテンシ、保持期間、信頼性の目標は面接における前提条件であり、ログベンダーの公開コミットメントではありません。スコープには、面接中に検索エンジン全体を実装したり、メッセージブローカーを再実装したりすることは含まれません。また、特定の企業の内部アーキテクチャをコピーする必要もありません。2026年の公開システム設計プロンプトでも、バースト処理、検索、保持、テナント分離を備えた集中ログ管理の設計が依然として問われています。OpenTelemetryの安定したログデータモデル、そのCollectorのトラブルシューティングガイダンス、およびデータ層とマッピング爆発(mapping explosion)に関するElasticのドキュメントが、重要なエンジニアリング境界の主要な根拠となります。

面接官が評価している点

第1に、候補者は高耐久な受付(durable acceptance)と即時の検索可能性を分離して考えられているか?ゲートウェイは、複製された高耐久ストリームにイベントをコミットした後に受信確認(ACK)を返すことができます。検索インデックスへの反映は数十秒遅れる可能性があります。成功条件として検索クラスタへの書き込みを必須にしてしまうと、インデックスのメンテナンスやスケーリングがすべてのプロデューサーに直接バックプレッシャーを与えてしまいます。

第2に、インシデント発生時の相関障害(correlated failure)を認識しているか?ログが最も価値を持つ時間帯は、アプリケーションエラー、リトライストーム、検索クラスタへの負荷が同時に上昇する時間帯でもあります。完全な回答には、ローカルディスクバッファリング、中央の高耐久キュー、バックプレッシャー、有限のキャパシティ、優先度ベースの負荷遮断(shedding)、測定可能なドロップカウントが含まれます。「KafkaとElasticsearchを追加する」とだけ答えても障害経路を塞ぐことはできません。

第3に、インデックス設計においてコストとカーディナリティを理解しているか?任意のJSON属性を動的にインデックス化すると、高カーディナリティフィールドによってマッピング、ディクショナリ、シャードのオーバーヘッドが発生します。堅牢な設計では、安定的で頻繁にクエリされ、明確に型付けされたフィールドのみにインデックスを作成します。その他の属性は、デフォルトでインデックスを作成せずに保存のみを行うことができます。

第4に、テナント分離が取り込みとクエリの両方をカバーしているか?テナントのアイデンティティは、ログ本文内で提供されるtenantIdではなく、認証されたクレデンシャルから取得する必要があります。特定の大規模テナント、広範囲の正規表現、またはフィールドの爆発によって、共有キュー、インデックス作成スレッド、キャッシュ、またはクエリの同時実行数が枯渇してはなりません。

最後に、キャパシティ、配信セマンティクス、セキュリティ、検証が1つの首尾一貫したシステムを形成しているか?候補者は、生の書き込みスループットとバースト時のバックログを計算し、at-least-once(少なくとも1回)配信によって重複が発生することを認め、監査ログとデバッグログに異なるポリシーが必要な理由を説明し、エンドツーエンドのカナリアを使用してデータの損失が絶対にサイレントに発生しないことを証明する必要があります。

回答前の明確化のための質問

  • どのログを失ってはならないか? 監査およびセキュリティイベントには高耐久な保持が必要です。デバッグログは、明示的な縮退ポリシーの下でサンプリングまたは破棄される場合があります。
  • 成功の境界はどこか? 中央ゲートウェイは、アベイラビリティゾーン間で複製された高耐久ストリームにイベントをコミットした後にACKを返し、検索インデックスのリフレッシュは待ちません。
  • ログ記録がプロデューサーをブロックすることは許容されるか? 通常のアプリケーションログがビジネスリクエストを同期的にブロックしてはなりません。ノードエージェントは非同期バッチと有限のローカルディスクを使用します。
  • 主要なクエリパターンは何か? テナントと時間範囲を指定した構造化フィルター、完全一致のtraceId検索、および制限された時間枠に対する全文検索です。
  • グローバルな順序付けは必要か? 不要です。単一のソースインスタンス内でおおよその順序を維持し、ソース間でイベントを順序付けるためにイベントタイムスタンプと観測タイムスタンプを使用します。
  • 保持期間の前提は何か? デバッグログはホットインデックスで24時間、生アーカイブで7日間です。運用ログはホットで7日間、アーカイブで90日間です。監査ログは30日間の検索可能期間と365日間の不変アーカイブを持ちます。
  • リージョンをまたぐアクティブな書き込みは必要か? プライマリ設計ではリージョンごとのパイプラインに収集します。統合コントロールプレーンとディザスタリカバリのクエリはアーカイブを読み取ることができます。
  • すべてのフィールドにインデックスを作成するか? いいえ。安定したトップレベルフィールドと許可リストに登録された属性のみにインデックスを作成できます。任意の高カーディナリティ属性は、デフォルトでは保存のみとします。
  • 機密情報はどのように処理されるか? SDKとノードエージェントが最初に入力を防止またはマスキングし、中央プロセッサが再度チェックします。シークレットやトークンがログに入力されてはなりません。
  • クエリの検索範囲はどこまで広くできるか? 時間範囲、カーソルベースのページネーション、スキャン予算、同時実行制限、およびキャンセル処理を必須とします。大規模な履歴データのエクスポートには非同期ジョブを使用します。

30秒の回答フレームワーク

「アプリケーションは標準出力または非同期SDKに書き込みます。ノードエージェントがバッチ処理、マスキングを行い、有限のディスクスプールを使用します。リージョンゲートウェイはワークロードを認証し、テナントをバインドし、クォータを適用し、フォーマットを検証してから、アベイラビリティゾーン間で複製された高耐久ストリームに書き込みます。このコミットがACKの境界となります。プロセッサはイベントを正規化し、生のレコードをオブジェクトストレージに書き込み、承認されたフィールドと選択されたテキストをホット検索インデックスに書き込みます。クエリサービスはテナントと時間の述語を必須とし、ホット、ウォーム、アーカイブの各ストレージに時間枠をルーティングします。定常状態の生トラフィックは約600 MB/秒(51.84 TB/日)です。15分間の3倍のバーストにより、定常処理分に加えて約1.08 TBのバックログが生成されます。配信はeventIdによる重複排除を伴うat-least-onceです。過負荷時には、監査ログとエラーログを最優先で保護し、デバッグログを真っ先にサンプリングし、すべてのドロップをアラート可能なメトリクスとして公開します。」

ステップバイステップの詳細設計

まず6つの不変条件(インバリアント)から始めます:

  1. 通常のビジネスリクエストがリモートのログ基盤に同期的に依存してはならない。
  2. 中央システムは、イベントが複製された高耐久バッファに到達した後にのみイベントをACKする。
  3. テナントのアイデンティティは認証に由来し、取り込み、ストレージ、検索、エクスポートがテナントの境界を越えることはできない。
  4. 監査ログは、デバッグログのサンプリングおよび負荷遮断ポリシーを使用しない。
  5. 任意のユーザー属性はデフォルトで動的インデックスを作成されないため、高カーディナリティフィールドが無制限にスキーマを肥大化させることはない。
  6. ドロップ、遅延、パース失敗、およびマスキング失敗は測定可能であり、絶対にサイレントに発生しない。

ステップ 1:収集パスと受信確認(ACK)境界を定義する。

text
Application stdout / asynchronous SDK
  -> node agent or sidecar: batch, compress, redact, local disk spool
  -> regional ingestion gateway: authenticate, bind tenant, quota, mandatory redaction, format and size limits
  -> durable stream replicated across availability zones
  -> normalization and routing processors
       -> raw object-storage archive
       -> hot search index
       -> alerts and streaming subscriptions
  -> query coordinator -> hot / warm / archive readers

ノードエージェントがビジネスプロセスから疎結合のままであるように、アプリケーションは通常stdoutに書き込みます。明示的に構造化されたイベントに対して非同期SDKを使用することは合理的ですが、それでもインメモリキューまたはローカルエージェントに書き込みます。エージェントはバイト数と時間によってバッチ処理を行い、個々のイベントサイズを制限し、自身の位置を永続化し、ネットワーク障害時には有限のディスクスプールを使用します。ディスクがいっぱいになった場合、無期限にブロックしたりディスクを無制限に消費したりするのではなく、ログクラスごとのポリシーに従います。

リージョンゲートウェイは、mTLSのアイデンティティ、ワークロードのアイデンティティ、または有効期限の短いクレデンシャルからテナント、サービス、環境を導出し、本文内の偽装されたフィールドを上書きします。レート制限の適用、圧縮のデコード、基本的なスキーマチェックの実行、最大イベントサイズの強制を行い、データが中央の高耐久バッファに入る前にシークレットとトークンの必須マスキングを適用します。アベイラビリティゾーン間で高耐久ストリームにイベントが複製された後にのみACKを返します。検索、アーカイブ、アラートは個別のコンシューマであるため、1つのコンシューマが失敗してもすべてのプロデューサーの直接の依存関係にはなりません。

ステップ 2:制御された拡張ポイントを持つ共通データモデルを使用する。

text
LogEvent(
  eventId, timestamp, observedTimestamp,
  tenantId, service, environment, instance,
  severityNumber, severityText, body,
  traceId, spanId, schemaVersion,
  attributes, sensitivityClass
)

timestampはイベントが発生した時刻であり、observedTimestampは収集システムが最初にそれを観測した時刻です。ソースのクロックがドリフトした場合でも、後者は取り込み順序と遅延を明らかにします。traceIdspanIdはログを分散トレースと接続します。severityNumberは正規化された比較をサポートし、severityTextはプロデューサーの表現を保持します。schemaVersionにより、プロセッサはパースルールを安全にアップグレードできます。

安定したトップレベルフィールドには、明示的な型とインデックスが設定されます。attributesは追加の構造化データを保持しますが、登録された許可リストのフィールドのみがインデックスマッピングに入ることができます。未知の属性には、フラット化されたオブジェクト、Key-Valueカラム、または生の本文を使用できます。同じフィールドが数値からオブジェクトに変更された場合、プロセッサはそれをパース失敗として隔離するか、バージョン管理されたフィールドを書き込みます。1つの不正なデプロイによって共有インデックス全体が破壊されてはなりません。

エージェントは、ソースインスタンス、起動エポック、およびローカルシーケンスからeventIdを生成できます。配信はat-least-onceであるため、エージェントはACKを受信しない場合にリトライします。コンシューマとインデックス書き込みはeventIdを冪等に使用します。重複排除ウィンドウは有限であり、アーカイブには重複が保持される場合があります。クエリと集計は、高コストで脆弱なエンドツーエンドのexactly-once配信を主張するのではなく、このセマンティクスを理解しておく必要があります。

ステップ 3:パーティショニングとテナント分離を計画する。

高耐久ストリームは仮想パーティションを通じてスケールします。ルーティングキーは(tenantId, sourceInstance)をハッシュ化し、単一ソースのおおよその順序を維持しながら、テナントを複数のパーティションに分散させることができます。tenantIdのみでパーティショニングすると、大規模テナントがホットスポットになります。完全なランダムパーティショニングでは、ソースローカルの順序が失われます。大規模テナントには専用のパーティションプールを割り当て、小規模テナントはプールを共有させることができ、コントロールプレーンはイベントフォーマットを変更せずに割り当てをリバランスできます。

各テナントには、取り込みバイト数、イベントレート、バーストトークン、ローカルおよび中央のバックログ、インデックス化フィールド数、ホットストレージ、クエリの同時実行数、スキャンバイト数、エクスポートジョブに対するクォータが設定されます。拒否と縮退はテナントごとに記録されます。システム過負荷時の優先順位は次のようになります:

  1. 監査およびセキュリティイベントを保護する。
  2. エラーおよび重要な運用イベントを保護する。
  3. 宣言されたポリシーに従って、反復的な警告、情報ログ、デバッグログをサンプリングする。
  4. 優先度の低い新しい広範囲のクエリおよびエクスポートを拒否する。

小規模テナントには共有ホットインデックスが機能しますが、すべてのドキュメント、キャッシュキー、およびクエリプランには信頼できるtenantIdを含める必要があります。大規模または規制の厳しいテナントには、分離されたインデックスと暗号化境界を使用できます。すべての小規模テナントに対して毎日多数の空のシャードを作成することも、すべてのテナントを1つの無制限の共有インデックスに配置することも避けるべきです。

ステップ 4:生アーカイブと検索インデックスを分離する。

オブジェクトストレージは、リージョン、テナント、日付、時間、およびオプションでサービスごとにパーティショニングされた、大きな圧縮カラムナーファイル形式で正規化された生イベントを保持します。これは、コンプライアンスのためのエクスポート、履歴スキャン、および破損したホットインデックスの再構築を行うための低コストな長期ソースとなります。ライターはログ1行ごとに1つのオブジェクトを作成するのではなく、イベントを適切なサイズのオブジェクトに集約します。

ホット検索インデックスには、最近の検索可能なデータのみが保存されます。安定したフィールドには転置インデックスまたはカラム指向インデックスを使用し、本文の全文検索インデックス作成はログクラスによって変えることができます。高カーディナリティのリクエストID、ユーザーID、または任意のタグは、頻繁にクエリされない場合は保存のみのフィールド(stored fields)のままとします。traceIdも高カーディナリティですが、明確な完全一致検索のユースケースがあるため、すべての動的フィールドの先例とするのではなく、保持期間を制限した専用の完全一致フィールドを使用できます。

ライフサイクルコントローラーは、クラスごとにデータをホット層からウォーム、コールド、フローズン、またはアーカイブ層に移動します。ホットストレージには、書き込みと低レイテンシ検索のためにより多くのコンピュートとレプリカが割り当てられます。古いデータはコストが低く、アクセスが遅くなることを許容します。保持期間による削除は、インデックス、アーカイブ、キャッシュ、エクスポート、およびリーガルホールドをカバーする必要があります。1つの検索インデックスを削除するだけでは完全な削除ワークフローとは言えません。

ステップ 5:制御されたクエリパスを構築する。

クエリAPIはセッションからtenantIdを導出し、デフォルトでstartTimeendTime、およびサービスまたは別の選択的な条件を必須とします。コーディネーターは時間範囲とログクラスを調べ、リクエストをホットインデックス、ウォームストレージ、または非同期アーカイブスキャンにルーティングします。インターフェースの例は以下のとおりです:

text
POST /logs/search
  { startTime, endTime, services, severities, traceId, query, cursor, limit }
  -> { events[], nextCursor, partial, scannedBytes }

POST /logs/exports
  { startTime, endTime, filters }
  -> { jobId }

結果には深いオフセットではなく、安定したソートキー(timestamp, eventId)とカーソルベースのページネーションを使用します。インタラクティブなクエリには、スキャンバイト数、返却行数、実行時間、同時実行数に制限が設けられます。制限に達した場合、レスポンスはデータをサイレントに省略するのではなく、明示的なpartialマーカーを返します。広範囲の正規表現、90日間の全文検索、大規模なエクスポートは非同期キューに入り、キャンセル可能です。一般的なフィルターのメタデータはキャッシュされる場合がありますが、クエリ結果を完全な分離なしにテナント間で再利用することはできません。

2秒未満というp95目標は、健全なクラスタ上での直近15分間に対する選択的なフィルタークエリに適用されます。保持されているすべてのデータに対する無制限の全文スキャンはそのSLOを共有しません。すべてのクエリが高速であると約束するよりも、クエリクラスを定義する方が現実的で信頼性があります。

ステップ 6:キャパシティを計算する。

定常状態の生のスループットは次のとおりです:

text
1,000,000 events/s × 600 bytes = 600 MB/s
600 MB/s × 86,400 s = 51.84 TB/day

トラフィックは15分間、毎秒100万イベントから300万イベントに増加します。ダウンストリームの処理が定常レートしか維持できない場合、この追加のバックログを吸収する必要があります:

text
(3,000,000 - 1,000,000) × 600 bytes × 900 s = 1.08 TB

バーストにより、その15分間に合計1.62 TBが生成されますが、0.54 TBは定常処理が同時に処理するベースラインです。実際のバッファリングにはレプリケーション、バッチのオーバーヘッド、復旧用キャパシティ、安全マージンも必要となるため、1.08 TBはそのままハードウェア購入の数値になるわけではありません。

ログの20%のみが全文検索または構造化ホットインデックスに入り、そこに7日間保持される場合、インデックスのオーバーヘッドを考慮する前の生の入力は依然として次のようになります:

text
51.84 TB/day × 20% × 7 = 72.576 TB

実際のインデックスサイズは、フィールド、圧縮、シャード、レプリカに依存し、負荷テストによって測定する必要があります。90日分の生アーカイブは51.84 TB × 90 = 4.6656 PB、つまり圧縮前で約4.67 PBになります。この規模では、階層化、クラスベースの保持、圧縮、低価値ログの排除が、単に検索クラスタを拡大することよりも重要になります。

パーティション数は、パーティションごとの持続バイト数、パーティションごとのイベント数、および必要なリプレイスピードから導出します。キャパシティテストでは、アベイラビリティゾーンの喪失、コンシューマの追いつき処理、3倍のバーストを組み合わせ、最古のメッセージの経過時間(oldest-message age)が最終的に回復することを確認する必要があります。スタックトレースによって平均サイズが急激に変化する可能性があるため、イベント数のみをカウントするのでは不十分です。

ステップ 7:バックプレッシャー、ログストーム、ダウンストリームの停止を処理する。

すべての層には有限のキャパシティがあります:SDKメモリ、エージェントディスク、ゲートウェイ接続、ストリームの保持期間、プロセッサの同時実行数、インデックス書き込みキュー、クエリスレッドです。システムは、retry-after、バッチサイズの縮小、コンシューマの一時停止、優先度付きキューを介して圧力を伝播させます。エージェントディスクが制限に近づくと、まずポリシーに従ってデバッグログをサンプリングし、ドロップされたクラスごとのローカルカウンターを記録します。監査イベントは個別の予約プールを使用するか、明示的なビジネス障害ポリシーをトリガーします。

OpenTelemetry Collectorのトラブルシューティングガイダンスでは、送信先の利用不可とCollectorのサイズ不足の両方がドロップの原因になり得ると指摘されています。送信キューとリトライは一時的な障害をカバーしますが、キューが大きすぎるとメモリ圧迫を引き起こす可能性もあります。したがって、「リトライを有効にする」こと自体は信頼性計画にはなりません。キューの使用率、最古のイベントの経過時間、拒否数、恒久的な障害、プロセスメモリを監視し、キャパシティが枯渇する前にアラートを発します。

インデックスクラスタが利用できない場合でも、高耐久ストリームはデータの受け入れを継続し、生アーカイブとインデックスのコンシューマは独立して処理を進めます。復旧中、インデックスコンシューマはテナントの公平性とリプレイ制限を持って追いつき処理を行い、復旧したばかりのクラスタに再度過負荷をかけないようにします。インデックス作成が遅延している場合、UIには検索鮮度のウォーターマークと欠落している時間間隔が表示されます。ユーザーは「検索結果がない」ことを「ログが存在しない」と解釈してはなりません。

ステップ 8:セキュリティ、プライバシー、削除のループを閉じる。

機密データに対する最善の制御は、その値を生成しないことです。SDKは構造化フィールドの許可リストを提供します。エージェントは一般的なトークン、パスワード、Cookie、個人データパターンをマスキングします。ゲートウェイは中央の高耐久ストリームの前に必須のルールを適用し、後続のプロセッサはセマンティックなスキーマベースのマスキングを実行し、失敗したイベントを隔離(quarantine)にルーティングします。生本文が単にアーカイブに行くだけであるという理由でセキュリティをバイパスすることはできません。

トランスポートにはmTLSまたは有効期限の短いワークロードアイデンティティを使用し、データは環境または規制対象テナントのキー境界を使用して保存時に暗号化(暗号化 at rest)されます。RBACによりテナント、環境、サービス、フィールド、時間範囲を制限します。クエリ、エクスポート、保持期間の変更、リーガルホールドは、不変のアクセス監査イベントを生成します。機密性の高いフィールドにはフィールドレベル暗号化を使用するか、完全に削除することができます。UIでそれらを非表示にするだけでは不十分です。

削除は、ホットインデックス、ウォーム層、オブジェクトパーティション、キャッシュ、エクスポートコピーにわたる追跡可能なワークフローです。監査の保持がプライバシーに基づく削除リクエストと競合する場合、プロダクトおよび法務のポリシーによって優先順位、例外、リーガルホールドを定義する必要があります。システムはその明示的なポリシーを実行します。削除義務を回避するために「ログは不変である」という理由を使うことはできません。

ステップ 9:メトリクスとフォールトインジェクションでシステムを検証する。

主要なメトリクスには、作成からゲートウェイ、ゲートウェイから高耐久ストリーム、ストリームからアーカイブ、ストリームから検索可能になるまでのp50/p95/p99、各ステージでのキュー使用率、最古イベントの経過時間、リトライ、ドロップ、テナントごとの取り込み、スロットリング、フィールドカーディナリティ、コスト、インデックス拒否、マッピングの増加、スキャンバイト数、クエリタイムアウト、キャンセル、キャッシュヒット、マスキング失敗、認可拒否、アーカイブ復元の成功が含まれます。

エンドツーエンドのカナリアは、毎分各リージョンから一意のIDを持つ構造化イベントを書き込み、高耐久な受付、検索の可視性、オブジェクトアーカイブの存在、および最終的な保持期間による削除を検証します。カウントの突合(count reconciliation)により、エージェント送信数、ゲートウェイ受信数、ストリームコミット数、アーカイブ書き込み数、インデックス成功数の合計を比較します。説明のつく重複は許容されますが、原因不明の差異は許容されません。

障害テストには、15分間の3倍のログストーム、30分間の検索停止、オブジェクトストレージのスロットリング、1つのアベイラビリティゾーンの喪失、ホットテナント、高カーディナリティフィールド攻撃、無効なスキーマ、ソースのクロックスキュー、機密データの挿入、エージェントディスクの枯渇、重複バッチ、ストリームのリプレイ、アーカイブからのホットインデックスの再構築が含まれます。重要なアサーションは、監査イベントがポリシーに従って復旧可能であること、通常のログのすべてのドロップにテナント、クラス、時間、カウントの証跡があること、そしてユーザーとアラートが検索鮮度の低下を認識できることです。

質の高い模範解答

「私ならまず、高耐久な受付(durable acceptance)と検索の可視性を分離します。アプリケーションはstdoutまたは非同期SDKに書き込み、ノードエージェントがバッチ処理、マスキング、有限のディスクバッファリングを処理するため、ログ基盤の障害が通常のビジネスリクエストを同期的にブロックすることはありません。リージョンゲートウェイはワークロードアイデンティティからテナントを導出し、クォータとフォーマットルールを強制してから、アベイラビリティゾーン間で複製された高耐久ストリームにバッチをコミットします。これが中央の受付ポイントとなります。検索インデックスとオブジェクトアーカイブは非同期にデータを消費します。

イベントには、イベント時刻、観測時刻、テナント、サービス、環境、インスタンス、重大度、本文、traceIdspanId、スキーマバージョン、機密性に関する安定したトップレベルフィールドがあります。任意の属性はデフォルトで保存のみとされます。マッピングとインデックスの肥大化を防ぐため、実際のクエリ価値を持つ安定した明確に型付けされたフィールドのみがインデックスに入ります。配信はat-least-onceです。エージェントは安定したeventIdを使用してリトライし、インデックス書き込みは冪等ですが、アーカイブには識別可能な重複が保持される場合があります。

オブジェクトストレージはリプレイ可能な長期の生レコードを保持し、ホット検索層は最近の検索可能なデータのみを保持します。クエリには信頼できるテナントと時間範囲が必要であり、ホット、ウォーム、アーカイブ層にルーティングされ、スキャンバイト数、同時実行数、結果サイズが制限されます。広範な履歴全文検索とエクスポートは非同期ジョブになります。小規模テナントはインデックスを共有し、大規模テナントは分離される場合がありますが、取り込み、バックログ、フィールド、クエリのクォータは常にテナントごとに適用されます。

定常状態では、生の取り込みは600 MB/秒(51.84 TB/日)です。ダウンストリームの処理が定常キャパシティのままであれば、15分間の3倍のバーストによって約1.08 TBのバックログが追加されます。20%のみを7日間インデックス化する場合でも、インデックスのオーバーヘッド前に約72.6 TBが生成され、90日分の生アーカイブは圧縮前で約4.67 PBになります。したがって、選択的インデックス作成、クラスベースの保持、階層化は必須です。

障害発生時、高耐久ストリームは短期的なバックログを吸収し、アーカイブとインデックスのコンシューマは独立して復旧します。すべてのバッファは有限です。システムは監査イベントとエラーイベントを最優先で保護し、デバッグイベントを真っ先にサンプリングし、ドロップ、最古メッセージの経過時間、検索鮮度のウォーターマークをモニタリングとUIに公開します。最後に、プロデューサー、ストリーム、インデックス、アーカイブを通過する一意のカナリアを実行し、3倍のトラフィック、ダウンストリームの停止、高カーディナリティフィールド、ディスク枯渇を注入して、サイレントな損失がないこと、およびアーカイブからインデックスを再構築できることを証明します。」

よくある間違い

  • アプリケーションからリモートログAPIを同期的に呼び出す → ログの停止がビジネスリクエストに悪影響を与える

有限のバッファリングと明示的な縮退を備えたローカルの非同期エージェントに書き込む。

  • 検索クラスタへの書き込み後にのみACKを返す → インデックスのメンテナンスがすべてのプロデューサーにバックプレッシャーを与える

複製された高耐久ストリームにコミットした後にACKを返す。

  • すべてのJSONフィールドを動的にインデックス化する → 高カーディナリティがマッピング爆発と制御不能なコストを引き起こす

安定した許可リスト登録済みフィールドのみをインデックス化する。

  • テナントIDごとに1つのストリームパーティションを使用する → 大規模テナントが単一パーティションのホットスポットになる

テナントとソースから仮想パーティションを使用し、正当な理由がある場合は専用プールを使用する。

  • ランダムパーティショニング後にグローバルな順序を主張する → クロックと並行するソースではそれを提供できない

ソースローカルの順序を維持し、観測タイムスタンプを保持する。

  • 重複を隠すためにexactly-onceを主張する → 応答の喪失やリプレイは依然として発生する

at-least-once配信、安定したeventId、冪等な書き込みを使用する。

  • 無制限のキューによって損失が防げると想定する → ディスク、メモリ、保持期間は最終的に満杯になる

キャパシティ制限を設定し、最古の経過時間を監視し、クラスに優先順位を付け、ドロップの証跡を保持する。

  • ストーム中にすべてのログを一律にサンプリングする → 監査証跡も破棄されてしまう

信頼性と縮退をログクラスごとに個別に定義する。

  • すべてのクエリが2秒で完了すると約束する → 90日間の全文スキャンはインタラクティブクラスタを損なう

選択的なホットクエリと非同期の履歴スキャンを分離する。

  • UIの結果が空であることをログが存在しない証拠として扱う → インデックスの遅延がインシデントの証拠を隠してしまう

ウォーターマーク、部分的な結果、欠落している時間枠を表示する。

  • ホットインデックスのみを削除する → アーカイブ、キャッシュ、エクスポートに機密情報が残ったままになる

すべてのコピーにわたる監査可能な削除ワークフローを使用する。

  • Collectorプロセスが生存しているかどうかのみを監視する → 健全なプロセスであってもデータをドロップすることがある

ステージごとのカウントを突合し、キュー、拒否、恒久的な障害を監視する。

フォローアップの質問と回答

フォローアップ 1:エージェントから検索クラスタに直接書き込ませないのはなぜですか?

コンポーネントが少なく、直接的な検索鮮度が得られるため、小規模な社内ツールであれば合理的です。しかしこの規模では、検索シャードの変更、マッピングの失敗、書き込み拒否が10万のソースに波及してしまいます。高耐久ストリームはACK境界を作成し、バーストを吸収し、コンシューマが独立して復旧できるようにし、リプレイをサポートします。これには追加の運用コスト、重複、非同期遅延というコストが伴います。測定された規模が小さい場合は、単に図を埋めるためだけにブローカーを追加するのではなく、直接書き込むパスを選択してください。

フォローアップ 2:1つのフィールドが突然数百万の異なる値を持った場合はどうなりますか?

スキーマレジストリが、インデックス可能なフィールド、型、所有者チーム、カーディナリティ予算を記録します。プロセッサは近似ユニークカウント(approximate distinct counts)を使用してカーディナリティを監視します。フィールドが制限を超えると、新しいインデックス構造の作成を停止し、その値をインデックス化されていない属性として保持し、テナントに通知します。すでにマッピング爆発が発生している場合は、まず新しいフィールドをブロックしてアップストリームのフォーマットを修正し、健全なフィールドを新しいインデックスに再構築します。同じ無制限のフィールドセットをより多くのインデックスに分散させても、その増大は解決しません。

フォローアップ 3:高耐久ストリームとローカルスプールの両方が満杯になった場合はどうなりますか?

有限のシステムでは、無限のバーストを保証することはできません。エージェントはログクラスごとにキャパシティを予約します。監査イベントは最も強力なチャネルを使用し、エラーは情報イベントよりも優先され、デバッグログは最初にサンプリングまたはドロップされます。すべてのドロップはローカルでカウントされ、復旧後に報告されます。ビジネスオペレーションが特定の監査イベントの高耐久性を必要とする場合、そのオペレーションは明示的に失敗するか、ローカルのトランザクショナルアウトボックスを使用できます。通常のデバッグ呼び出しがアプリケーション全体を無期限にブロックしてはなりません。

フォローアップ 4:オブジェクトストレージから検索インデックスをどのように再構築しますか?

アーカイブオブジェクトには、スキーマバージョン、時間パーティション、テナント、チェックサム、マニフェストが含まれます。再構築ジョブはテナントと時間枠を選択し、マニフェストを読み取って検証し、現在のマッピングでレコードを変換し、eventIdによって新しいインデックスに冪等に書き込みます。エイリアスまたはルートをアトミックに切り替える前に、カウント、時間境界、カナリアを比較します。再構築トラフィックには個別のクォータが割り当てられているため、リアルタイムのインデックス作成のリソースを奪うことはありません。

フォローアップ 5:1つのテナントのクエリが全員に悪影響を与えるのをどのように防ぎますか?

スケジューラは、同時実行数、CPU時間、スキャンバイト数、返却行数に対するテナントトークンバケットを維持し、重み付け公平キューイング(WFQ)を使用します。インタラクティブなクエリはエクスポートよりも優先され、高負荷な処理はキャンセルされるか非同期に移行されます。共有キャッシュキーにはテナントと権限のサマリーが含まれます。大規模または規制の厳しいテナントには分離されたインデックスプールが割り当てられる場合がありますが、小規模テナントが過剰な極小シャードを作成しないよう、ボトルネックを測定した後にのみ分離します。

フォローアップ 6:なぜ監査ログには別のポリシーが必要なのですか?

監査ログは、「誰が、いつ、何をしたか」を証明するために存在します。サンプリング、変更可能なコンテンツ、通常の管理者による削除は、その目的を台無しにします。監査ログには、より厳格なスキーマ、認証された出所(provenance)、整合性チェック、不変アーカイブ、制限されたクエリ、アクセス監査が必要です。これらもプライバシーに基づく削除やリーガルホールドのポリシーには従うため、「不変」とは通常の操作パスでは変更できないことを意味し、承認されたコンプライアンスワークフローが決して操作できないという意味ではありません。

フォローアップ 7:この設計をマルチリージョンに拡張するにはどうすればよいですか?

各リージョンはまずローカルイベントをローカルの高耐久ストリームとアーカイブに書き込むため、ビジネスリクエストが大陸を越えて待たされることはありません。イベントにはリージョンとグローバルテナントIDが付与され、コントロールプレーンがスキーマ、クォータ、保持ルールを配布します。グローバルクエリコーディネーターは時間とリージョンごとにファンアウトし、リージョンが利用できない場合は部分的な結果としてマークします。規制でデータレジデンシー(データの国内保持)が義務付けられている場合、生ログはローカルに留まり、承認されたインデックスまたは集計のみがリージョンを越えます。ディザスタリカバリは、ローカルまたはコンプライアンスに準拠した複製オブジェクトアーカイブから復元します。

フォローアップ 8:システムがログをサイレントに失っていないことをどのように証明しますか?

各バッチはソースのカウントとチェックサムを記録します。すべてのステージで、受信数、重複数、拒否数、恒久的な失敗数、成功数が発行されます。一意のカナリアがエージェント、ゲートウェイ、高耐久ストリーム、インデックス、アーカイブを継続的に通過します。突合処理では、eventIdによって説明がつく重複と、明示的なクォータポリシーによって説明がつくドロップが許容されます。原因不明の差異はインシデントとなります。さらに、インデックス作成を30分間停止してから復旧させ、単にプロセスが再起動したことを確認するだけでなく、メッセージの経過時間が減少し、カナリアがギャップを埋め、UIの鮮度ウォーターマークが回復することを検証します。

公開情報ソース

関連する質問

関連面接ツール

システム設計の回答には「回答する」を使用

まず要件を明確にし、スケール、アーキテクチャ、コンポーネント選定、トレードオフの順に進めます。

ツールを見る