QEMU Monitor ConsoleとQMP Protocol:KVMハイパーバイザー層から仮想マシンを制御・デバッグ・障害注入する

Virtualization tutorial - IT technology blog
Virtualization tutorial - IT technology blog

稼働中のVMを管理していて、ゲストにSSHせずにデバッグしたいことはありませんか?あるいは、ディスクが突然障害を起こしたときにアプリケーションがどう反応するかテストしたいこともあるでしょう。QEMU Monitor ConsoleとQMP Protocolを使えば、まさにそれが可能です——ハイパーバイザー層から直接操作でき、ゲストOSは一切感知しません。

クイックスタート — QEMU Monitorに5分で接続する

QEMU/KVMで手動VMを起動している場合、起動コマンドに以下の2つのオプションを追加します:

qemu-system-x86_64 \
  -monitor unix:/tmp/qemu-monitor.sock,server,nowait \
  -qmp unix:/tmp/qemu-qmp.sock,server,nowait \
  -drive file=/path/to/disk.qcow2,if=virtio \
  -m 2048 -smp 2

monitorへの接続にはsocatを使います——ncはUbuntuでコマンドのバッファリングが正しく動作しないことが多く、socatはreadlineをより適切に処理します:

socat readline UNIX-CONNECT:/tmp/qemu-monitor.sock

プロンプト(qemu)が表示されたら、QEMU Monitor内にいます。いくつかのコマンドを試してみましょう:

(qemu) info status
(qemu) info cpus
(qemu) info block
(qemu) stop
(qemu) cont

VMがlibvirtで動作している場合、インタラクティブセッションを開かずにvirshでコマンドを実行できます:

virsh qemu-monitor-command --hmp myvm "info status"
virsh qemu-monitor-command --hmp myvm "info block"

QEMU Monitor Consoleとは何か、どんな時に使うのか

QEMUプロセス自体へのバックドアと考えてください——ゲストOSへのものではありません。CPUを停止させようとRAM全体をダンプしようと、monitorを通じて操作している間、ゲストはいかなるシグナルも受け取りません。

monitorと通信するためのプロトコルは2種類あります:

  • HMP (Human Monitor Protocol) — テキストコマンドで手入力でき、クイックデバッグに使用
  • QMP (QEMU Machine Protocol) — JSONベースで、スクリプトや自動化に向いている

私はProxmox VEで12台のVMとコンテナを管理するホームラボを運用しています——本番環境に投入する前にすべてをテストするためのプレイグラウンドです。そして、ゲストへのSSHがそもそも不可能な場面もあります:

  • VMが完全にフリーズしてSSHを受け付けないが、QEMUプロセスはまだ生きている
  • ゲストを中断せずにRAMの状態を分析するためにメモリスナップショットが必要な時
  • ディスク障害を注入して、ディザスタリカバリスクリプトが実際に機能するか確認したい時
  • ゲストカーネルに入る前の仮想NICレイヤーでネットワークパケットをデバッグしたい時

QMP Protocol — JSONとPythonによる自動化

QMPはすべてのレスポンスにJSONを返します。パースすれば即座にクリーンなdictが手に入ります——HMPのように正規表現や文字列の手動分割は不要です。

QMPへの手動接続

socat - UNIX-CONNECT:/tmp/qemu-qmp.sock

サーバーはすぐにグリーティングを送信します。コマンドを実行する前に必ずqmp_capabilitiesを送信する必要があります:

{ "execute": "qmp_capabilities" }
{ "execute": "query-status" }

QMPと通信するPythonスクリプト

JSONを一つ一つ手入力する代わりに、小さなラッパーを書いた方がはるかに便利です:

import socket
import json

class QMPClient:
    def __init__(self, sock_path):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.sock.settimeout(10)
        self.sock.connect(sock_path)
        self._recv()  # greeting
        self.execute("qmp_capabilities")

    def _recv(self):
        data = b""
        while True:
            chunk = self.sock.recv(4096)
            data += chunk
            try:
                return json.loads(data.decode())
            except json.JSONDecodeError:
                continue

    def execute(self, cmd, **kwargs):
        payload = {"execute": cmd}
        if kwargs:
            payload["arguments"] = kwargs
        self.sock.sendall(json.dumps(payload).encode() + b"\n")
        return self._recv()

# 使用例
qmp = QMPClient("/tmp/qemu-qmp.sock")

status = qmp.execute("query-status")
print(status)  # {"return": {"status": "running", "running": true}}

qmp.execute("stop")
qmp.execute("cont")

ハイパーバイザー層からの障害注入とデバッグ

デバッガのブレークポイントのようにVMを一時停止する

特定の時点でVMを停止し、全状態を観察します——CPUレジスタからメモリマッピングまで、すべてがそのままの状態で止まります:

(qemu) stop
(qemu) info registers      # 停止時点のCPUレジスタ
(qemu) info mem            # memory mapping
(qemu) x /10i $rip         # instruction pointerから10命令をディスアセンブル
(qemu) cont                # 実行を再開

フォレンジック分析のためのメモリダンプ

# HMP経由
(qemu) dump-guest-memory -z /tmp/vm-memory.dump
# QMP経由
qmp.execute("dump-guest-memory",
    paging=False,
    protocol="file:/tmp/vm-memory.dump",
    format="elf"
)

ダンプファイルはVolatilityで開いて実行中のプロセス、オープンファイルハンドル、RAM内の任意のアーティファクトを調査できます。私はこの方法をルートキット感染が疑われるVMの分析に使用しました——ダンプ後にvolatility3 -f vm-memory.dump linux.pslistを実行したところ、kworker/1:2Hという名前で親PIDが異常な隠しプロセスを発見しました。マシンを一度もシャットダウンせずに調査を完了でき、揮発性データもそのまま保持できました。

ディザスタリカバリテストのためのディスクエラー注入

これは私が最もよく使うユースケースです——ディスク障害時にバックアップスクリプトが実際にリカバリできるか確認します:

# ブロックデバイスの一覧
(qemu) info block
# Output: drive-scsi0-0-0-0: /var/lib/vz/images/100/vm-100-disk-0.qcow2

# ディスクの遅延/障害をシミュレートするためにI/Oをほぼ0に制限
(qemu) block_set_io_throttle drive-scsi0-0-0-0 1 0 1 0 0 0

6つの数値はそれぞれ以下を制限します:総帯域幅(bytes/s)、読み取り帯域幅、書き込み帯域幅、総IOPS、読み取りIOPS、書き込みIOPS——値が0の場合、その方向への個別制限を設定しないことを意味します。帯域幅を1 byte/sに設定するとディスクは動作しますが極めて遅くなり、タイムアウトをトリガーしてアプリケーションのリトライロジックを検証するのに十分です。

より低いレイヤーでI/O障害を注入するには、VM起動時にblkdebugドライバを使用します:

qemu-system-x86_64 \
  -drive file=blkdebug::/path/to/disk.qcow2,if=virtio,format=qcow2 \
  -m 2048

VMを停止せずにライブスナップショットを取得する

# 全状態を保存(CPU + RAM + ディスク)
(qemu) savevm checkpoint-before-test

# スナップショットの一覧
(qemu) info snapshots

# 以前の状態に戻す
(qemu) loadvm checkpoint-before-test

注意:savevmはQEMU内部スナップショットを作成し、libvirtのスナップショットとは異なります。スナップショットファイルはqcow2イメージに直接保存されます。

仮想NICでのネットワークパケットキャプチャ

# 仮想インターフェースでパケットキャプチャを有効化(netdev idは起動設定から取得)
(qemu) object_add filter-dump,id=f1,netdev=net0,file=/tmp/vm-traffic.pcap

# 完了したら無効化
(qemu) object_del f1

このpcapファイルはWiresharkで通常通り開けます。優れている点は、仮想NICに入ってくる内容を正確に確認できることです——ゲスト内のiptablesや任意のファイアウォールルールが介入する前の段階で。まったく異なる2つの問いを切り分けるのに役立ちます:「トラフィックがVMに届いているか」と「VMがそのトラフィックを処理しているか」。

QEMU Monitor使用時の実践的なヒント

ソケットパスが不明な場合の確認方法:

ps aux | grep qemu | grep -o 'qmp [^ ]*'
# libvirtの場合
virsh dumpxml myvm | grep -i monitor

リモートコントロール用のQMP over TCP:

qemu-system-x86_64 \
  -qmp tcp:127.0.0.1:4444,server,nowait \
  ...

# 接続(ファイアウォールやトンネルがある場合のみ外部にポートを開放、QMPは認証なし)
socat - TCP:127.0.0.1:4444

スクリプト用のvirsh one-liner:

# HMP command
virsh qemu-monitor-command --hmp myvm "info block"

# QMP command (JSON)
virsh qemu-monitor-command myvm '{"execute": "query-block"}'

フリーズ時にVMが生きているか確認する:

# monitorが応答すればゲストがハングしていてもQEMUプロセスは生きている
virsh qemu-monitor-command --hmp myvm "info status"

Monitorコンソールは、SSHや通常の監視ツールの代替ではありません。しかし、他のすべてが使えなくなったとき——VMが完全にフリーズし、SSHがタイムアウトし、エージェントが応答しない——monitorはそこにあります。30秒でメモリダンプ。実際のハードウェア障害なしにリカバリをテストするためのディスクスロットル。慣れてくると、難しそうに見えたVMの問題の多くが数分で解決できると気づくでしょう。

Share: