代表的な面接トピック

システム設計面接:リカバリ可能なユーザ空間ブロックデバイスドライバをどのように設計しますか?

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

質問

仮想ブロックデバイスのロジックをユーザ空間に配置するシステムを設計してください。高並行性I/O、ユーザ空間プロセスのクラッシュリカバリ、権限分離、および可観測性をサポートする必要があります。コントロールプレーンとデータプレーンのプロトコル境界を説明してください。

プロンプトと背景

仮想ブロックデバイスのロジックをユーザ空間に配置するシステムを設計してください。カーネルがブロックデバイスを公開し、ユーザ空間サービスがループ、リモートブロックストレージ、またはqcow2マッピングを処理します。高並行性I/O、ユーザ空間プロセスのクラッシュリカバリ、権限分離、および可観測性をサポートする必要があります。

Linuxのublkは、このフレームワークをコントロールプレーンとデータプレーンに分離します。/dev/ublk-controlがデバイスを管理し、/dev/ublkb*がブロックI/Oを伝送し、ユーザ空間サービスがio_uring passthroughを介してリクエストの取得と結果のコミットを行います。この面接では、単にファイルI/Oをプロセスへ移行することではなく、リクエストのライフサイクル、障害セマンティクス、およびセキュリティ境界を評価します。

面接官がテストしていること

コントロール、カーネルブロック層、io_uring、およびユーザ空間バックエンド間の境界を示します。キュー、タグ、バッファ、完了(completion)間における1対1の関係を説明し、I/O単位モードとバッチモードのいずれかを選択し、サーバ終了時のrequeue、fail、replayセマンティクスを定義し、ゼロコピー、特権、コンテナ分離、およびメトリクスを適切に処理します。

最初に明確にすべき質問

ワークロードとバックエンド

読み取り/書き込みの比率、I/Oサイズ、キュー数、レイテンシの目標、バックエンドがローカルファイル、リモートNBD、またはコピーオンライト形式のいずれであるか、および順序付けの保証が必要かどうかを確認します。

障害とデータ安全性

ユーザ空間のクラッシュ、ネットワーク分断、バックエンドの部分書き込み(short write)、重複書き込み、およびデバイス削除に対するセマンティクスを確認します。リプレイが二重書き込みを許容できるかどうかを確定します。

権限とデプロイ

誰がデバイスを作成できるか、誰が/dev/ublkc*を読み取れるか、サービスがコンテナ内で実行されるか、どのゼロコピー特権が許可されるか、およびテナントがどのように分離されるかを確認します。

30秒の回答

「私はコントロールプレーンとデータプレーンを分離します。コントロールはデバイス起動前にキュー、深度、機能をネゴシエーションし、データはio_uringからキューとタグごとにリクエストを取得して結果をコミットします。各リクエストには所有者、状態、タイムアウトがあります。ユーザ空間サーバが終了した際は、デバイスを静止化(quiesce)し、バックエンドの保証に応じてrequeue、fail、またはreplayを選択します。コピーをデフォルトとし、ゼロコピーは信頼できる承認済みバックエンドに限定します。メトリクスはキュー深度、完了レイテンシ、リトライ、ドロップ、リカバリ時間をカバーし、削除およびリカバリに関する整合性テストを実施します。」

ステップバイステップの詳細な回答

ステップ 1: コントロールプレーンの分割

デバイスの追加、パラメータの設定/取得、起動、停止、削除を行うコマンドを公開します。追加時にはnr_hw_queuesqueue_depth、および最大I/Oバッファサイズをネゴシエーションします。パラメータは起動前に固定され、その後/dev/ublkb*が公開されます。ユーザ空間サービスは、デバイスIDとバックエンド固有の情報を保存します。

ステップ 2: データプレーンリクエストの設計

ブロック層はキューごとに一意のタグを割り当て、ユーザ空間サービスは(queue, tag)によってリクエストを関連付けます。固定されたマッピング領域にオフセット、長さ、オペレーション、フラグが記述されます。サービスはio_uring passthroughを介して通知を受信し、ステータスと完了バイト数をカーネルにコミットします。

ステップ 3: I/O単位モードまたはバッチモードの選択

従来のI/O単位コマンドは、1つのデーモンが各タグを所有するため、ロジックを把握しやすいです。バッチモードはキューごとに複数のリクエストを準備してコミットし、システムコールのオーバーヘッドを削減し、タスクが動的に作業を共有できるようにします。移行中にコマンドセットを混在させないでください。高負荷時のテールレイテンシ、CPU、ロードバランシングを比較します。

ステップ 4: リカバリ状態マシンの定義

running、quiescing、recovering、failedなどの状態を使用します。サーバ終了時には、新規I/Oのディスパッチを停止し、インフライトのリクエストを待機またはマークし、START_USER_RECOVERYを発行します。REISSUEは重複書き込みを許容するバックエンドに適しており、FAIL_IOは成功を偽装する代わりに、インフライトおよび将来のリクエストを明示的に失敗させます。

text
on_server_exit:
  quiesce_device()
  if policy == REISSUE:
    requeue_inflight()
  else:
    fail_inflight_and_future_io()
  wait_new_server_ready()
  end_user_recovery()

ステップ 5: バッファとゼロコピーの処理

通常のパスでは、事前に割り当てられたユーザ空間バッファとカーネルコピーを使用するため、境界がシンプルになります。ゼロコピーには、登録済みの固定バッファ、バックエンドのセグメントアライメント、およびREADデータを入力してバイト数を正しく報告する信頼できるサービスが必要です。ミスがあると初期化されていないカーネルバッファが公開される可能性があるため、特権を制限し、ライフタイムを監査します。

ステップ 6: 権限とコンテナの分離の構築

特権コントロールコマンドをデバイスアクセスから分離します。非特権デバイスの場合でも、カーネルは関連するキャラクタデバイスの所有権をチェックします。コンテナには自身のデバイスノードのみが見えるようにする必要があります。ユーザ空間バックエンドは、ターゲットデバイス以外のファイル、ネットワーク、またはKMS権限を受け取ってはなりません。

ステップ 7: パフォーマンスと正確性の検証

fioまたは本番環境に近いワークロードを使用して、IOPS、p50/p99レイテンシ、キュー深度、CPU、コピーバイト数、およびリカバリ時間を測定します。サーバクラッシュ、バックエンドタイムアウト、部分書き込み、デバイス削除、重複送信を注入(fault injection)します。すべてのリクエストが1回完了するか、ポリシーに従って明示的に失敗することを確認します。コピーモードとゼロコピーモードの両方でバウンダリとチェックサムをテストします。

模範解答

私ならublkのようなシステムを、コントロールプレーン、カーネルブロック層、io_uringデータプレーン、ユーザ空間バックエンドに分割します。コントロールは起動前にキューとバッファをネゴシエーションし、データは(queue, tag)によってリクエストを追跡し、完了時にステータスとバイト数を検証します。サーバクラッシュ時は静止化とリカバリをトリガーします。重複を許容できるバックエンドに対してのみリプレイを行い、そうでない場合は失敗させます。コピーをデフォルトとし、ゼロコピーは信頼され、アライメントされ、承認されたサービスに限定します。リリースには、テールレイテンシ、フォールトインジェクション、権限分離、削除整合性のテストが必要です。

よくある間違い

  • 間違い: ユーザ空間サーバにデバイスを直接閉じさせる。 → なぜ失敗するか: インフライトのリクエストやカーネルキューの状態が未定義のままになります。 → 修正方法: ディスパッチを停止し、静止化してからリカバリポリシーを適用します。
  • 間違い: すべてのバックエンドでゼロコピーを有効にする。 → なぜ失敗するか: バッファのライフタイム、特権、未初期化データのリスクが増大します。 → 修正方法: デフォルトでコピーを使用し、ケイパビリティと監査に基づいてゼロコピーを制限します。
  • 間違い: 1つのグローバルロックですべてのキューを保護する。 → なぜ失敗するか: マルチキューの並行性が直列化されてしまいます。 → 修正方法: キュー/タグごとに状態をシャード(分割)し、競合を測定します。
  • 間違い: 正常時のI/Oスループットのみを測定する。 → なぜ失敗するか: クラッシュ、部分書き込み、重複書き込みが整合性を左右するためです。 → 修正方法: 受け入れテストにリカバリ状態マシンとフォールトインジェクションを含めます。

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

バッチI/Oはどのような場合に選択すべきですか?

システムコールや通知のオーバーヘッドが支配的で、キューに十分な並行性があり、タスクが作業を動的に共有できる場合はバッチを選択します。I/O単位の所有権が重要な場合や並行性が低い場合は、従来のモードの方がデバッグしやすくなります。

REISSUEはどのようにデータ破損を回避しますか?

リクエストID、書き込みバージョン、ログの重複排除を使用して、冪等性があるバックエンドまたは重複を検出できるバックエンドに対してのみ有効にします。冪等性が証明できない場合は失敗させ、上位層にリカバリを委ねます。

ゼロコピーのセキュリティ影響をどのように制限しますか?

バッファの登録・登録解除およびデバイス権限を1つの信頼できるサービスにバインドし、アドレス、長さ、アライメント、完了バイト数を検証し、テナント間で共有される書き込み可能なマッピングを禁止します。

デバイス削除中の整合性をどのように維持しますか?

新規リクエストを停止し、送信済みリクエストが完了または失敗するのを待ち、ユーザ空間キューが空であることを確認してから、デバイスノードとマッピングを解放します。タイムアウトは黙って破棄するのではなく、追跡可能な障害として記録します。

公開情報ソース

関連する質問

関連面接ツール

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

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

ツールを見る