QEMU と KVM の協調動作

このページの目次

IOThread を追加しても、すべてのデバイス操作が並列実行される保証はありません。プレコピーを何回実行しても、ダーティページが減少する保証はありません。QEMU と KVM の協調動作には、⁠実行コンテキスト、デバイス状態、移行整合性を同時に管理する必要があります。完全なバックエンドは他のプロセスで実行されている場合もあります。

参照目標読書位置
マシンと実行の構成スレッド、作成、実行ループ
通知とデータの役割分担eventfd
収束、切り替え、障害特定移行関連セクション

スレッドはデバイスに固定で 1 対 1 対応するわけではない

vCPU スレッドは KVM_RUN に入り、メインイベントループは管理と対応するデバイスイベントを処理します。IOThread が設定されたデバイスは独立したイベントループを使用できます。すべての virtio デバイスが自動的にスレッドを取得するわけではなく、スレッドプール、バックエンド、デバイス実装も I/O に参加します。IOThreads

マルチスレッドは並列実行を可能にしますが、状態の同期と実行コンテキストの制約をもたらします。QEMU のグローバルロック、デバイスロック、AioContext などのメカニズムは特定の状態を担当しており、「IOThread を追加する」だけですべてのデバイス操作が並列化され、ロックフリーになるとは限りません。

作成から実行可能なマシンへ

段階ユーザー空間の役割KVM の提供機能
インターフェース検出API と必要な機能の照会システム fd、拡張照会
マシンの作成マシンレイアウトとバックアップの選択VM オブジェクトとメモリ登録インターフェース
CPU 初期化アーキテクチャに必要なレジスタ、機能、起動状態の設定vCPU オブジェクト、アーキテクチャ ioctl
デバイス構築デバイスモデルとバックエンドの設定オプションのカーネルデバイス、irqfd/ioeventfd など
実行と終了処理ユーザー空間で完了する必要がある操作の処理ゲスト実行とカーネルで完了可能な終了

CREATE_VM、RAM の登録、CREATE_VCPU の呼び出しだけでは、オペレーティングシステムの起動はできません。CPU モード、起動コードまたはファームウェア、割り込みコントローラ、デバイスなどの設定が不足しているためです。チュートリアルの簡略化されたフローは、「完全な起動器」と区別する必要があります。

mmap によってバックアップを作成した後、物理ページは必要に応じて割り当てられる可能性があります。MAP_NORESERVE は、その後のメモリが常に十分であることを保証するものではありません。memslot はゲストアドレスのマッピングを表現しますが、VMM は依然としてバックアップのライフサイクルを維持する必要があり、マッピングの変更や解除は KVM と調整する必要があります。KVM API

実行ループの 2 層の戻り値

ハードウェアの終了は、KVM によって処理された後、ゲストに直接再入される場合があります。KVM_RUN が返って初めて VMM の終了処理に入ります。エラーが返された場合は、まず errno を処理し、成功して返された後に run->exit_reason を読み取る必要があります。すべての ioctl の戻り値を正常なデバイスリクエストとして解釈してはいけません。

完了が必要な MMIO/PIO の読み書きの場合、VMM がデータを埋め戻してから再度 KVM に入ってから、対応するゲスト操作が完了します。移行前にレジスタのみを保存し、未完了の終了を無視すると、状態の境界が失われる可能性があります。QEMU にはアーキテクチャとデバイスの協調ロジックが既に存在しており、エラー処理を欠いた数行の擬似 C 関数をその実際のソースコードとして扱うのは適切ではありません。

eventfd は通知の迂回を減らすだけで、すべてのデータを担うわけではない

ioeventfd は、一致するゲスト I/O 書き込みを eventfd 通知に変換できます。irqfd は、対応する eventfd シグナルをゲスト割り込み注入に関連付けることができます。要求データは依然として virtqueue またはデバイスプロトコルで記述されたバッファ内にあり、eventfd は完全な要求内容そのものではありません。

カーネル vhost バックエンドはこれにより、QEMU ユーザー空間でのデータ要求処理の往復を減らすことができますが、初期化、機能のネゴシエーション、デバイス状態、移行制御には依然として VMM が必要です。ユーザー空間バックエンドには、共有メモリとプロセス間プロトコルの別のセットがあります。

プレコピー移行と収束

プレコピー移行:ソース側の実行を維持し、最後に一斉に切り替える ターゲットと互換状態の準備 マシンタイプ、CPU 機能、デバイス、ストレージパス 1 メモリの複製と変更の追跡 後のラウンドでまだダーティになっているページを送信 2 ソース側を停止し、最終状態転送を完了 残りのダーティページ、CPU、デバイス、および進行中の操作の調整 3 ターゲットでの実行再開 切り替え結果を確認し、2 つのインスタンスが同時に外部に書き込まないようにする 4 ダーティページレートはラウンドごとに減少する保証はありません。停止時間はワークロードと移行戦略に依存します。

ソース VM が実行中にメモリを継続的に変更するため、移行マネージャーは、このラウンド以降もダーティになるページを追跡する必要があります。書き込み保護による追跡が一種の方法ですが、ハードウェアのダーティビット、ダーティビットマップ/リングなども関与する可能性があります。すべてのメカニズムを「最初に全 RAM を読み込み専用にする」と記述することはできません。

ダーティになる速度が転送が処理できる速度に近づいたり超えたりする場合、プレコピーを繰り返しても収束しない可能性があります。新しいラウンドのダーティページ量が前のラウンドより少なくなる保証はありません。帯域幅、圧縮、ワークロードのスロットリング、切り替え戦略などが結果に影響します。移行フレームワーク

教育条件レート
有効な複製能力1 GiB/s
ワークロードが広範なページを継続的に再ダーティ化約 1.5 GiB/s

この場合、「数回コピーすれば自然にゼロになる」のを待つことはできません。実際の収束は、同じページへの繰り返し書き込み、ページ粒度、ウィンドウにも影響され、2 つのレートをそのまま正確な予測式として扱うことはできません。

8 GiB から最終停止まで:ラウンドごとに計算

複製する必要があるメモリを M=8 GiB、有効な送信レートを B=2 GiB/s、各ラウンドで新たに生成され、次のラウンドで送信する必要がある異なるダーティページを D=0.5 GiB/s の速度で増加すると仮定します。レートは固定、圧縮なし、ページ粒度の切り捨てや追加の転送オーバーヘッドは考慮しないものとします。これはラウンドごとに集約されたモデルであり、D は異なるダーティページに換算されたデータ量を表し、アプリケーションが実行するストアの元のバイト数ではありません。

このラウンドでの送信量を R とすると、所要時間は R/B です。この間に発生する新しいラウンドの作業量は min(M, D×R/B) です。M の上限に達していない場合、収縮率は D/B=1/4 です。

実行中のラウンドこのラウンドの送信 GiBこのラウンドの所要時間 s次のラウンドで送信予定 GiB
1842
2210.5
30.50.250.125
40.1250.06250.03125

固定の CPU/デバイス切り替えオーバーヘッドを 40 ms とした場合、第 3 ラウンド直後にソース側を停止すると、0.125/2 s + 40 ms = 102.5 ms が必要です。第 4 ラウンドをさらに実行すると、残りは 0.03125 GiB、つまり 32 MiB となり、予想される停止時間は 15.625 + 40 = 55.625 ms となり、ようやくこの例の 60 ms の予算内に収まります。

from fractions import Fraction as F

def precopy(memory, bandwidth, dirty_rate, rounds):
    assert memory > 0 and bandwidth > 0 and dirty_rate >= 0 and rounds >= 0
    remaining = F(memory)
    rows = []
    for _ in range(rounds):
        seconds = remaining / bandwidth
        new_dirty = min(F(memory), dirty_rate * seconds)
        rows.append((remaining, seconds, new_dirty))
        remaining = new_dirty
    return rows, remaining

rows, remaining = precopy(F(8), F(2), F(1, 2), 4)
assert [r[2] for r in rows] == [F(2), F(1, 2), F(1, 8), F(1, 32)]
fixed_stop = F(40, 1000)
assert rows[2][2] / 2 + fixed_stop == F(1025, 10000)
assert remaining / 2 + fixed_stop == F(55625, 1000000) <= F(60, 1000)
for dirty in (F(2), F(3)):
    unstable, left = precopy(F(8), F(2), dirty, 4)
    assert left == 8 and all(row[2] == 8 for row in unstable)
assert precopy(F(8), F(2), F(0), 1)[1] == 0

D≥B の場合、このモデルではラウンドごとに再び 8 GiB 全体が得られ、ラウンド数を増やしても最終的な停止時間が短縮されません。実際のワークロードではホットページの集合は非常に小さい場合があります。同じページが 1 万回書き込まれていても、特定の統計ウィンドウでは 1 回しか送信する必要がない場合があります。したがって、異なるダーティページ量、ウィンドウ、実際の帯域幅を組み合わせる必要があります。メモリの書き込み帯域幅のカウントを直接 D に代入することはできません。QEMU の dirty-limit などのメカニズムは、ダーティワークロードを制限することで収束を助けますが、その代償としてソース側のワークロードに影響が出ます。QEMU Dirty limit

複製後にダーティビットをクリアすると、新しく発生した書き込みが消去される可能性がある

現在値が 0 で、ダーティビットが既にセットされている 1 ページを考慮します。移行マネージャーが 0 をターゲットにコピーした後、ゲストがソースページを 1 に変更し、ダーティビットは 1 のままです。移行マネージャーが「このページのコピーが完了した」ことを示すために無条件でダーティビットをクリアすると、新しい書き込みの証拠も一緒に消去されます。次のラウンドではダーティページが見えず、値 1 も補送されません。

誤ったクリアタイミング:ターゲットは 0 のまま、新しい書き込みはダーティとしてマークされない ソースページ 0 をコピー ターゲットは 0 を取得 ゲストが 1 に書き込み ソースページのダーティビットは 1 のまま 移行マネージャーがダーティビットをクリア 新しい書き込みの証拠が消去 次のラウンドでこのページをスキップ ターゲットは値 1 を見逃す 次のラウンドの追跡を確立してから、現在のラウンドのページを読み取って送信する必要がある 図は単一ページの論理的競合を示している。実際の処理では、再保護、ダーティビット収集、DMA の調整も必要。

正しいプロトコルでは、明確な追跡境界を確立し、このラウンドで処理される古いダーティ状態と、その後の新しい変更を分離する必要があります。以下では、ダーティ集合の原子交換を使用してこの境界を表現します。現在のラウンドの集合を取り出して次のラウンドの追跡を有効にし、その後コピーします。コピー中に発生した変更は新しい集合に入ります。これは実際の KVM のダーティビット、書き込み保護の復元、メモリバリア、DMA 追跡を実装したものではありません。

class Memory:
    def __init__(self):
        self.pages = {0: 0}
        self.dirty = {0}

    def write(self, page, value):
        self.pages[page] = value
        self.dirty.add(page)

    def take_epoch(self):
        old, self.dirty = self.dirty, set()
        return old  # モデル内ではこの操作は書き込みによって中断されない

# 誤った順序:コピー後のクリアが新しい変更を飲み込む。
bad = Memory()
target = {0: bad.pages[0]}
bad.write(0, 1)
bad.dirty.clear()
assert not bad.dirty and target != bad.pages

# 正しい境界:このラウンドのコピー後の新しい変更は次のラウンドに残す。
source = Memory()
target = {}
for page in source.take_epoch():
    target[page] = source.pages[page]
source.write(0, 1)
assert source.dirty == {0} and target[0] == 0
for page in source.take_epoch():
    target[page] = source.pages[page]
assert target == source.pages and not source.dirty
for value in range(2, 102):
    source.write(0, value)
assert len(source.dirty) == 1  # 100 回の書き込みでも、異なるダーティページは 1 つのみ

実際のシステムには、KVM、QEMU RAM 会計、移行送信などの多層の追跡があり、モデル内の take_epoch() を任意の clear ioctl に直接置き換えることはできません。カーネルによるページ再保護、ハードウェアダーティビットの収集、送信順序は、「読み取り後の変更が失われない」ことを共同で保証する必要があります。コピー中にページの内容が変化する可能性もあります。最終的にソース側を停止し、残りのダーティ状態を再同期して、最終的な整合性のある切り替えの境界を形成します。

次に、有効な複製帯域幅 B とダーティページ発生レート D を変更し、プレコピーをどのラウンドで終了するかを選択します。残りの集合と停止時の複製時間を比較し、D/B が 1 に近づいたときになぜさらに縮小しにくくなるかを観察します。

M は総メモリ、M_n は第 n ラウンドで複製する必要があるデータです。このモデルは、一定の帯域幅と異なるページの有効なダーティ化レートを仮定し、総メモリで上限を設定します。同じページへの繰り返し書き込みは、書き込みごとのバイト数で無限に累積することはできません。

図を準備しています
さらに数ラウンドコピーすれば、残りのダーティデータは必ず減るのか?

このラウンドのコピーには Mᵢ/B の時間が必要で、その間に新しいダーティデータは概ね D×Mᵢ/B 発生する。D/B<1 の場合は減少するが、1 に近づいたり超えたりすると、プレコピーを続けても収束する保証はない。

最後の瞬間に整合性が必要なのは RAM だけではない

切り替え時の状態一緒に調整する理由
vCPU レジスタ、仮想割り込み、タイマー正しい実行位置と処理待ちイベントの復元
デバイスキュー、バックエンド、残りの I/O未完了操作の境界の保持
CPU 機能、マシンタイプ、デバイスバージョンターゲットがソースの状態を復元できること
ストレージとネットワークアクセス権システムレベルの切り替えを完了し、両端への書き込みを避ける

したがって、移行の整合性は「RAM のコピーが完了した」だけでは判断できません。

postcopy は、ターゲットでの復元後に不足しているページを必要に応じて取得するため、停止時間とページフォルト待機のトレードオフを変え、切り替え後のソース/ターゲットの障害復旧をより複雑にします。それはプレコピーの「最後のラウンドを速くする」ものではなく、単一の downtime 数値だけを比較すべきではありません。

移行成功前後では、外部への書き込み権をどのインスタンスが持っているかを明確にし、分裂実行を避ける必要があります。パsthrough デバイスの移行可否は、デバイス状態と DMA 追跡能力に依存します。メモリとデバイスの仮想化 を参照してください。

問題を観察する際は、まず所在する層を見つける

CPU 使用率が高い場合、ゲストの有効な実行、頻繁な終了、ホストのスケジューリング待機を区別する。I/O 遅延の場合、ゲストのキューイング、通知、QEMU/バックエンドの実行、実際のストレージネットワークを区別する。移行が停止している場合、ダーティページレート、有効帯域幅、デバイスブロックを参照する。

/dev/kvm の可用性検出については、KVM アーキテクチャ を参照してください。さらに詳細なソースコードは、QEMU の accel/kvm/kvm-all.c、Linux の virt/kvm、および対応する arch ディレクトリから入手できます。バージョン固有のパスは、実際の checkout に準拠する必要があります。