2011/12/07(Wed)Linux KVM Tips & IPv6 LiveMigration

2011/12/07 22:55 Software::VM/ESXi
最近VPS等でも用いられるようになっていたLinux KVM(Kernel-based Virtual Machine)についてのメモ。
VMを停止させずに別のサーバへ移動できるマイグレーションが面白そうということで、ちょっと触ってみた。

検証環境

検証マシン
hp 8200 Elite SF (Core i7-2600, Q67 Express, 8GB)
hp dx7500 SF (Core2 Duo E8400, G45 Express, 4GB)
hp EliteBook 2730p (Core2 Duo L9400, GS45 Express, 4GB)
DELL vostro 3300 (Core i5-520M, HM57 Express, 4GB)

※KVMはIntel VT対応が必須。VT対応しているCPUでも、BIOSでDisabledにされていたり、そもそも設定項目がない場合もあるので注意。メーカー製PCは基本的にDisabledになっていることが多い。
Linuxなら/proc/cpuinfoに"vmx"(Intelの場合), "svm"(AMDの場合)があるかを確かめておく必要がある。
ちなみにWindowsならVirtualChecker等を使えば把握できる。
ディストリビューション
Fedora 14
カーネル
Linux mica 2.6.35.14-103.fc14.i686.PAE #1 SMP Thu Oct 27 15:58:03 UTC 2011 i686 i686 i386 GNU/Linux
qemu
以下のバージョンにパッチをあてたもの
qemu-system-x86-0.13.0-1.fc14.i686
qemu-common-0.13.0-1.fc14.i686
qemu-img-0.13.0-1.fc14.i686
gpxe-roms-qemu-1.0.1-3.fc14.noarch
libvirt
以下のバージョンにパッチをあてたもの
libvirt-0.8.3-10.fc14.i686
libvirt-debuginfo-0.8.3-10.fc14.i686
libvirt-python-0.8.3-10.fc14.i686
libvirt-devel-0.8.3-10.fc14.i686
libvirt-client-0.8.3-10.fc14.i686
virt-manager
virt-manager-0.8.7-2.fc14.noarch

環境構築

基本的にはyumで一発。普通に使う分にはこれでよさそう。
sudo yum install virt-manager libvirt qemu-system-x86
ただし、後述のパッチを当てたりするために、一旦アンインストールし、独自rpmをインストールしたりしている。

ちなみにFedoraのデフォルトカーネルでは、KVM自体が組み込まれており、「仮想マシンマネージャー」ことvirt-managerを立ち上げれば(要X Window System)自動的にカーネルモジュールがロードされるはず。(以下はIntel PCの場合)
$ lsmod | grep kvm

kvm_intel              35769  4
kvm                   213316  1 kvm_intel

virt-manager, libvirt, qemuの関係

はじめ、この関係が理解できなくて苦労した…。
virt-manager
「仮想マシンマネージャー」ということで、仮想マシンを作ったり消したり、構成を変更したり、あと仮想マシンのコンソール(画面)を見る事も可能。
要するに、ただのガワ。
libvirt
ググると分かるように「仮想化API」という代物らしい。
virt-managerは基本的にlibvirtのAPIを叩いているので、KVM以外の仮想化技術もそのまま使える。(Xen, Linux Containers, OpenVZ, User Mode Linux, Virtual Box, VMware ESX, Open Nebula等)
しかも、x86, x86_64, IA64, Powerなどの複数のアーキテクチャに対応し、C, Python, Perl, Ruby, Java等からAPIを叩けるらしい。
libvirt APIを対話シェル(引数に書いてバッチ処理も可能)から扱うためのvirshというコマンドが用意されている。
またlibvirt自体はサービスとして常駐可能なため、常に起動させておく*1と、virt-manager経由で他のサーバ上のVMを操作する(コンソールを除くだけでなく構成変更やマイグレーションその他も)ことが可能である。
qemu
「エミュレータ」というジャンルで、あるプラットフォーム上で別プラットフォーム環境を完全エミュレーションするらしい。
つまりQEMU単体でも仮想化は可能だが、Intel VTなどのハードウェア支援を受けられないため、とてつもなく遅い。
というわけで、今回Linuxに統合されているKVMというハイパーバイザを利用し、その上でQEMUを使ってエミュレータを動かすという形になる。

*1 : # chkconfig libvirtd on

基本的に

このサンプルでもそうだが、VTなどの仮想化支援機能を使うせいか、一般的には推奨されていないであろうrootでのアプリケーション起動が前提となっているものが多い。
virt-managerのGUIには初期状態で何も登録されていないが、[ファイル]-[接続を追加...]とやれば、自ホストやネットワーク上の他ホストが追加できる。

他ホストを捜査する場合、root経由でsshを行う必要があるため、/etc/ssh/sshd_configにPermitRootLogin yesを設定しておく必要がある。
毎回パスワードを求められるのが鬱陶しい場合は、ローカルユーザとローカルのrootの公開鍵を、相手ホストの~root/.ssh/authorized_keysに書いておくと良い。

なおvirt-managerがイマイチで、ホスト名欄にはIPv6アドレスを生で記述できない。*2
/etc/hostsまたはDNSでAAAAを解決できるようにしておけば、相手がIPv6しか喋れないホストでもlibvirt経由でアクセスできる。

*2 : 2001:db8::1234とか書くと、2001というホストにアクセスしようとして失敗する。[]をつけても一緒。

ブリッジデバイス

作り方

基本的に初期状態では、NATデバイスしか出来ていない。
仮想マシンにおいてブリッジ、いわゆるサーバに刺さっているネットワークセグメントと同一のネットワークを利用したいシーンでは、ブリッジ専用のI/Fを介して接続しなければならない。

そして、そのブリッジ機能はLinux bridgeに任されているので、brctlを叩いてブリッジI/Fを用意する必要がある……のであるが、network-scriptsを直に設定したり、virt-managerから設定する事でブリッジI/Fは簡単に作れる。

次の節に構成図を示しているように、eth0という物理I/Fはただの出入り口という扱いになるため、アドレスやゲートウェイを割り振るデバイスはbr0になる。
eth0に設定しても無視されるので注意。
太字の部分に特に注意して(大文字小文字も)、あとはeth0の設定をコピペすればよい。
$ cat /etc/sysconfig/network-scripts/ifcfg-br0

DEVICE=br0
TYPE=Bridge
ONBOOT=yes
HWADDR=XX:XX:XX:XX:XX:XX
BOOTPROTO=dhcp
IPV6INIT=yes
DELAY=0
STP=no

$ cat /etc/sysconfig/network-scripts/ifcfg-eth0

DEVICE=eth0
TYPE=Ethernet
ONBOOT=yes
BRIDGE=br0
ちなみに、DELAY=0が死ぬほど重要。
書き忘れるとForwarding delayというのが15秒にセットされ、マイグレーションしてからその時間だけネットワークが不通になる模様。STPで必要な事なのか、通信を安定させる目的か分からないけど、とにかく注意。

virt-managerのGUIでは、「ブリッジの設定」の[設定]ボタンの中にその設定がある。
kvm-make_bridge.png

構成図

先ほどのブリッジの状態ではこんな風になっている。
 Guest VM   |                 VM Server
  [eth0]  <===>  [vnet0]  <===>  [br0]  <===>  [eth0]
ここでまた見慣れないvnet0というデバイスが出来ているのだが、いわゆるこれはtapデバイスというもので、VM内のeth0がサーバ側でvnet0と見えていると思うと直感的かな。
したがってVMをシャットダウンすると、ホスト側ではvnet0が見えなくなる。

ブリッジコントローラで見るともっと分かりやすいかもしれない。
$ brctl show

bridge name     bridge id               STP enabled     interfaces
br0             8000.xxxxxxxxxxxx       no              eth0
                                                        vnet0
virbr0          8000.000000000000       yes
つまりbr0は、「eth0とvnet0をブリッジしている状態」なのである。
例えVM(もしくはその中で利用するNIC)が増えて場合でも、vnet1, vnet2, ...をbr0に属させる事で、この例ではeth0をそれぞれのVMで共有する事が出来る。

virbr0

ちなみに上の例では使われていないvirbr0というのが、libvirtをインストールした時点で出来ているNATデバイスである。
どこにNATされるかはシステム依存の設定になっているようだが、VMの設定でNATデバイスに接続したときには、$ brctl showでvnet0がvirbr0に属しているように見えるはず。

エラー: internal error could not get interface XML description

一応ここまでで普通にVMが使えてはいるのだが、/var/log/messagesに不審なメッセージが定期的に出ていた。
Jul 12 22:37:21 vostro2 libvirtd: 22:37:21.234: error : interfaceGetXMLDesc:355 : internal error could not get interface XML description (netcf: NETLINK socket operation failed - couldn't find ifindex for interface `eth1`)
Jul 12 22:37:21 vostro2 libvirtd: 22:37:21.246: error : interfaceGetXMLDesc:355 : internal error could not get interface XML description (netcf: NETLINK socket operation failed - couldn't find ifindex for interface `eth2`)
Jul 12 22:37:21 vostro2 libvirtd: 22:37:21.255: error : interfaceGetXMLDesc:355 : internal error could not get interface XML description (netcf: NETLINK socket operation failed - couldn't find ifindex for interface `eth3`)
これって正しくインタフェース情報が登録されていない?
virt-manager経由でなく、network-scriptsをいじってブリッジデバイスを作ったりしたので、その過程でlibvirtに正しくI/FのXMLが定義されなかったんだろうと思う。
undefineしてやってやったらこのエラーは消えた。どこかで定義し直した覚えはないし、使えているので問題ないようなのだが。
# virsh iface-undefine eth1
# virsh iface-undefine eth2
# virsh iface-undefine eth3
ちなみに、上とは別のマシンだが、eth0, eth1が存在し、br0 <=> eth0としている構成ではこういう風に見えた。
virsh # iface-list
Name                 State      MAC Address
--------------------------------------------
br0                  active     XX:XX:XX:XX:XX:XX
eth1                 active     XX:XX:XX:XX:XX:XX
lo                   active     00:00:00:00:00:00

virsh # iface-dumpxml eth0
error: failed to get interface 'eth0'
error: Interface not found: couldn't find interface with MAC address 'eth0'

virsh # iface-dumpxml br0
<interface type='bridge' name='br0'>
  <protocol family='ipv4'>
    <ip address='192.168.xxx.xxx' prefix='24'/>
  </protocol>
  <protocol family='ipv6'>
    <ip address='2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx' prefix='64'/>
    <ip address='fe80::xxxx:xxxx:xxxx:xxxx' prefix='64'/>
  </protocol>
  <bridge>
    <interface type='ethernet' name='vnet0'>
    </interface>
    <interface type='ethernet' name='eth0'>
    </interface>
  </bridge>
</interface>

virsh # iface-dumpxml eth1
<interface type='ethernet' name='eth1'>
  <protocol family='ipv4'>
    <ip address='10.xxx.xxx.xxx' prefix='24'/>
  </protocol>
  <protocol family='ipv6'>
    <ip address='2001:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx' prefix='64'/>
    <ip address='fe80::xxxx:xxxx:xxxx:xxxx' prefix='64'/>
  </protocol>
</interface>

エラー: getaddrinfo(127.0.0.1,5900): Name or service not known

virt-managerからVMを起動する際、コンソール(画面)を開くと
getaddrinfo(127.0.0.1,5900): Name or service not known
と怒られる事があった。

これは何故かというと、QEMU/KVMではコンソール接続にQEMUの-incomingオプションを利用してVNCプロトコルでアクセスしているらしい。
その部分がIPv4でしかlistenしていないようなのだが、VMのホストOS側にIPv4アドレスが1つも付いていないと(ループバックを除く)このエラーが起きる。

いやー、接続しようとしている127.0.0.1:5900は、loに127.0.0.1というアドレスが付いているのだから、別に他のI/FにIPv6しかついてなくてもうまくいくはずなんだけどな…。
getaddrinfoがloをうまく解決できていないのかどうかまでは見ていないけど、とにかくbr0等になんかIPv4アドレスを振ってやれば解決する。

未解決のエラー

ライブマイグレーションを行う際、こんなメッセージが/var/log/messagesに出ることがある。
要は失敗した事だけは分かるのだが、ほとんど意味をなしていないので、全然当てにならない。
(ライブマイグレーション送信側)
Jul 12 22:37:44 vostro2 libvirtd: 22:37:44.489: error : qemuDomainMigrateSetMaxDowntime:11781 : invalid argument in qemuDomainMigrateSetMaxDowntime: unsupported flags (0xbc614e)
Jul 12 22:37:44 vostro2 libvirtd: 22:37:44.498: error : qemuDomainMigrateSetMaxDowntime:11781 : invalid argument in qemuDomainMigrateSetMaxDowntime: unsupported flags (0xbc614e)
Jul 12 22:37:46 vostro2 libvirtd: 22:37:46.124: error : qemuMonitorTextMigrate:1182 : operation failed: migration to 'tcp:vostro1:49174' failed: migration failed#015#012
Jul 12 22:40:36 vostro2 libvirtd: 22:40:36.698: error : qemuMonitorTextMigrate:1182 : operation failed: migration to 'tcp:vostro1:49175' failed: migration failed#015#012


(ライブマイグレーション受信側)
Jul 12 22:44:06 vostro1 libvirtd: 22:44:06.255: error : qemuDomainHasManagedSaveImage:5650 : Domain not found: no domain with matching uuid '91df7b16-fcfb-3803-cd4f-103e53dbe1b3'
Jul 12 22:44:06 vostro1 libvirtd: 22:44:06.261: error : qemuDomainGetJobInfo:11684 : Domain not found: no domain with matching uuid '91df7b16-fcfb-3803-cd4f-103e53dbe1b3'
Jul 12 22:44:06 vostro1 libvirtd: 22:44:06.263: error : qemudDomainDumpXML:6743 : Domain not found: no domain with matching uuid '91df7b16-fcfb-3803-cd4f-103e53dbe1b3'

qemuとqemu-kvm

実際にソースコードをいじろうと思ってハマったのだが、QEMUとQEMU/KVMは別物である。
といっても、全く違うのではなくてQEMU/KVMの方がQEMUからブランチしているのだろうけど、間違えてqemu-xxxx.tar.gzというのをQEMUのサイトからダウンロードしてLinuxをブートしてみたら、妙に遅すぎて使い物にならなかった。
KVMをサポートしている*3QEMUは、こっちが正解。qemu-kvm-xxxx.tar.gzというファイル名になっているはず。

*3 : という表現でいいのか…

QEMUコンパイルの注意

qemu-kvm-0.13.0のSRPMを素の状態でコンパイルしてみたが、何故か最終的にエラーになってしまうので悩んだ。
しかも、単なるコンパイルエラーではなくて、OOMKillerに殺されているという状況。
物理メモリが約2.5GB利用可能で、さらに1GBのswap領域があるコンパイル環境では、全てのメモリとスワップを使いつぶしたあげく、失敗してしまう。(hp Elitebook 2730p)
以下はそのログ。
gcc -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/slirp -m32 -fstack-protector-all -Wold-style-definition -Wold-style-declaration -I. -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0 -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -Wstrict-prototypes -Wredundant-decls -Wall -Wundef -Wendif-labels -Wwrite-strings -Wmissing-prototypes -fno-strict-aliasing -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m32 -march=i686 -mtune=atom -fasynchronous-unwind-tables  -DHAS_AUDIO -DHAS_AUDIO_CHOICE -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/fpu -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/tcg -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/tcg/i386  -DTARGET_PHYS_ADDR_BITS=64 -I.. -I/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/target-i386 -DNEED_CPU_H     -MMD -MP -MT i8254.o -MF ./i8254.d -O2 -g   -c -o i8254.o /home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/hw/i8254.c
{standard input}: Assembler messages:
{standard input}:15420942: Warning: end of file not at end of a line; newline inserted
{standard input}:15421105: Error: unknown pseudo-op: `.by'
gcc: Internal error: Killed (program cc1)
Please submit a full bug report.
See <http://bugzilla.redhat.com/bugzilla> for instructions.
make[1]: *** [translate.o] Error 1
make[1]: Leaving directory `/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/sparc64-linux-user'
make: *** [subdir-sparc64-linux-user] Error 2
make: *** Waiting for unfinished jobs....
{standard input}: Assembler messages:
{standard input}:9258539: Warning: end of file not at end of a line; newline inserted
{standard input}:9258676: Error: can't resolve `.text' {.text section} - `.Ltext' {*UND* section}
gcc: Internal error: Killed (program cc1)
Please submit a full bug report.
See <http://bugzilla.redhat.com/bugzilla> for instructions.
make[1]: *** [translate.o] Error 1
make[1]: Leaving directory `/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/x86_64-softmmu'
make: *** [subdir-x86_64-softmmu] Error 2
{standard input}: Assembler messages:
{standard input}:10119177: Warning: end of file not at end of a line; newline inserted
gcc: Internal error: Killed (program cc1)
Please submit a full bug report.
See <http://bugzilla.redhat.com/bugzilla> for instructions.
make[1]: *** [translate.o] Error 1
make[1]: *** Deleting file `translate.o'
make[1]: Leaving directory `/home/kero/rpmbuild/BUILD/qemu-kvm-0.13.0/sparc32plus-linux-user'
make: *** [subdir-sparc32plus-linux-user] Error 2
エラー: /var/tmp/rpm-tmp.CRYtEl の不正な終了ステータス (%build)


RPM ビルドエラー:
    /var/tmp/rpm-tmp.CRYtEl の不正な終了ステータス (%build)
どうやらこれは、gccのバグを踏んでいるようで、新しいブランチでは解決されたらしい。ソース1,ソース2

検証環境のgcc
$ gcc --version
gcc (GCC) 4.5.1 20100924 (Red Hat 4.5.1-4)
これだけのためにgccを用意するのも大変なので、今回はPAEカーネルをインストール*4して、4GB全てのメモリが可能な状態にしてやればうまくいった。

*4 : # yum install kernel-PAE

IPv6 Live Migration

IPv6 Ready!という内容をちらほら目にしたので、案外楽に行くかと思ったのだけど、いざソースコードを眺めていると、IPv4前提の書き方が多い事…。
多分IPv6 Readyといってるのは、VM内のゲストOSでv6が使えるという事なんでしょうね。
今回試したライブマイグレーションは、対象環境のバージョンではIPv6で扱えませんでした。

IPv6マイグレーション対応パッチ

幸いにも、qemu-develのMLにAllow ipv6 for migrationというパッチを投稿している人が居たので、これを参考にQEMUにパッチしてみたら扱えるようになった。
また、virt-managerからQEMUを呼び出す部分にもIPv4依存な部分があるので、これも併せてパッチが必要である。
ちなみに、QEMUはマルチプラットフォームではあるが、検証は最初に示したLinux環境のみで行っているので注意。

qemu-ipv6mig.patch
libvirt-ipv6mig.patch

そして、このパッチをrpmを作るときに組み込むために、qemu.specとlibvirt.specをそれぞれ改変する必要がある。
それぞれのバージョンのsrpmをダウンロードしてきて、rpm -Uvh *.srpmすると、~/rpmbuild/SPECS/辺りにあるはず。

基本的にPatch~という行と、%patch~という行を足すだけで良い。
このままだとバージョン番号が紛らわしいので、Version:行やRelease:行を適当に変える事をオススメ。

▼qemu.spec
Patch41: 0041-vmmouse-adapt-to-mouse-handler-changes.patch
Patch42: 0042-vhost-net-patches-for-qemu-0.13.0-tarball.patch
Patch43: qemu-ipv6mig.patch


BuildRoot: %_tmppath/%name-%version-%release-root-%(%__id_u -n)
BuildRequires: SDL-devel zlib-devel which texi2html gnutls-devel cyrus-sasl-deve

---------

%patch41 -p1
%patch42 -p1
%patch43 -p1


%build
# By default we build everything, but allow x86 to build a minimal version
# with only similar arch target support

▼libvirt.spec
# Patch 7  CVE-2011-2511
Patch7: %name-%version-remote-protect-against-integer-overflow.patch
Patch8: libvirt-ipv6mig.patch


BuildRoot: %_tmppath/%name-%version-%release-root
URL: http://libvirt.org/
BuildRequires: python-devel

---------

%patch6 -p1
%patch7 -p1
%patch8 -p1


%build
%if ! %with_xen
%define _without_xen --without-xen
あ、念のためコンパイル方法も書いておくと…
# cd ~/rpmbuild/SPECS

# rpmbuild -bb qemu.spec

# rpmbuild -bb libvirt.spec

~/rpmbuild/RPMS/に完成したものができるはず。
後は、一旦yumで入れたものを消してrpmで突っ込むだけ。
virt-managerはlibvirtに依存しているので一旦消す必要がある点と、rpmインストール直後はlibvirtdが起動していないので起動し直すことに注意。
# yum remove virt-manager python-virtinst libvirt libvirt-client libvirt-python qemu-system-x86 qemu-img qemu-common

# rpm -ivh libvirt-0.8.3-10.fc14.i686.rpm libvirt-client-0.8.3-10.fc14.i686.rpm libvirt-python-0.8.3-10.fc14.i686.rpm qemu-system-x86-0.13.0-1.fc14.i686.rpm qemu-img-0.13.0-1.fc14.i686.rpm qemu-common-0.13.0-1.fc14.i686.rpm

# yum install virt-manager python-virtinst

# service libvirtd start

ついでに、yumで更新されないようにexcludeリストを書いておく。
#  vim /etc/yum.conf


exclude=libvirt* qemu*

パッチの後処理

さて。
これを適用することで、これまで0.0.0.0でしかlistenしていなかったものが、anyというホスト名でlistenすることになる。
本当は"::"でlistenさせたかったのだが、QEMUの-incomingオプションは、"-incoming tcp:0.0.0.0:49123"のような形式で書かれているため、::だとparseの部分で問題が生じるためである。

したがって、このパッチを当てた上で、/etc/hostsにanyを解決するように書く必要がある。
# echo ":: any" >> /etc/hosts

また、マイグレーションするホスト群のアドレスも直には書けないので、ホスト名でhostsから引けるようにしておく。
# echo "2001:db8::1234 vostro1" >> /etc/hosts

# echo "2001:db8::1235 vostro2" >> /etc/hosts

マイグレーションテスト

これでテストしてみる。(vostro2というホストからvostro1というホストへのマイグレーション)
libvirt経由で起動したQEMUのマイグレーションでは、TCP 49152~49215が使用されるようなので、ポートの開け忘れに注意。
もちろんlibvirt経由で試しても良いが、今回はqemuを直接叩く方法で検証してみる。
qcow(シンプロビジョニング形式?)で1Gのディスクを作成
# qemu-img create -f qcow /tmp/test.qcow 1G

# cd qemu-kvm-0.13.0/x86_64-softmmu/

# sudo ./qemu-system-x86_64 -m 1024 -hda /tmp/test.qcow -boot c -monitor stdio -incoming tcp:any:49200

VNC server running on `127.0.0.1:5900`
QEMU 0.13.0 monitor -type 'help' for more information
この状態でlistenしている事を確認。
$ netstat -an | grep 49200

tcp        0      0 :::49200                    :::*                        LISTEN
マイグレーションスタート。
OSはインストールしていないので、すっからかんの状態で一瞬で移動するはず。
(qemu) migrate -d tcp:vostro1:49200

(qemu) info migrate

Migration status: completed
libvirtやvirt-managerからマイグレーションを行う場合も同様に、IPv6アドレスはそのまま書けないので、AAAAが引けるホスト名で書いてやるとマイグレーション可能となる。