通りすがりのマシンルームに 32core CPU / 160GB Memory のサーバが 2 台と 10TB の SAN ストレージがあったので、おもむろに RHEL6 の KVM + RHCS でクラスタを組んで 100VM 起動してみました。
本当にそんなサーバがあるの?
ありました。
# cat /proc/cpuinfo | grep "model name" | uniq model name : Intel(R) Xeon(R) CPU X6550 @ 2.00GHz # cat /proc/cpuinfo | grep "model name" | wc -l 32 # free total used free shared buffers cached Mem: 165347204 3962872 161384332 0 19764 184604 -/+ buffers/cache: 3758504 161588700 Swap: 4095992 0 4095992 # fdisk -l /dev/mapper/mpatha ディスク /dev/mapper/mpatha: 10995.1 GB, 10995116277760 バイト ヘッド 255, セクタ 63, シリンダ 1336746 Units = シリンダ数 of 16065 * 512 = 8225280 バイト セクタサイズ (論理 / 物理): 512 バイト / 512 バイト I/O size (minimum/optimal): 512 bytes / 512 bytes ディスク識別子: 0x00000000 ディスク /dev/mapper/mpatha は正常なパーティションテーブルを含んでいません
100VM もどうやってインストールするの?
スクリプトで自動化しました。GUI で 100回インストールとかできません。今回は 2VM だけ手でインストールして、それを virt-clone コマンドでクローニングしました。
仮想ディスク領域の準備
今回はクラスタ構成なので、Cluster LVM 環境の LV を仮想ディスクとしてアサインします。VG (datavg01) を作って libvirt のストレージプール(プール名も datavg01)として登録したら、おもむろに下記のスクリプトで LV を 100 個作ります。(ストレージが 10TB あるので、100GB x 100個にしています。なんと贅沢な。)
#!/usr/bin/perl use strict; my $tmpfile = "/tmp/lvtmp$$.xml"; my ( $num, $snum ); foreach $num (1..100) { $snum = sprintf( "%03d", $num ); open ( OUT, ">$tmpfile" ); print OUT <<EOF; <volume> <name>rhel60vm$snum</name> <source> <device path='/dev/mapper/mpatha'> </device> </source> <capacity>104857600000</capacity> <allocation>104857600000</allocation> <target> <path>/dev/datavg01/rhel60vm$snum</path> <permissions> <mode>0660</mode> <owner>0</owner> <group>6</group> </permissions> </target> </volume> EOF close OUT; print ( "virsh vol-create datavg01 $tmpfile\n" ); system ( "virsh vol-create datavg01 $tmpfile" ); } unlink $tmpfile;
(2011/04/25 追記)上のスクリプトでは vol-create で xml ファイルから Volume を定義していますが、次のコマンドでも Volume 定義が可能でした。こっちの方が簡単ですね。
# virsh vol-create-as datavg01 rhel60vm$snum 100G
※ libvirt のストレージプールの概念などは、このあたり を参照ください。
※ Cluster LVM の構成手順は、ここを参考にしてください。
VM のクローニング
はじめに 2 個の VM(rhel60vm001, rhel60vm002)を virt-manager のウィザードから普通にインストールします。ここでは、vcpu x 2, 1GB memory で作成しました。インストールするパッケージは rhel60vm001 がデスクトップ環境で、rhel60vm002 が最小構成。
次にいくつかクローニング用のスクリプトを用意します。
まずは、汎用的なクローニング用スクリプト
/root/work/kvmclone.pl
#!/usr/bin/perl use strict; use vars qw( $opt_t $opt_o $opt_n $opt_i $opt_m $opt_g $opt_s $opt_h $opt_a ); use Getopt::Std; $ENV{'LANG'} = "C"; sub chkopts { getopts( 'ahs:t:o:n:i:k:g:b:c:m:' ); $opt_h = 1 unless ( $opt_t && $opt_o && $opt_n && $opt_i && $opt_m && $opt_g && $opt_s ); if ( $opt_h ) { print <<EOF; usage: kvmclone.pl [-aontsimg] -a: Activate vm after cloning -o: Original domain name -n: New domain name -t: Target disk image file -s: Server hostname -i: IP Address -m: Net mask -g: Default gateway EOF exit 0; } $opt_t = "/var/lib/libvirt/images/" . $opt_t unless ( $opt_t =~ m/^\// ); } sub log_cmd { print "===> Exec: $_[ 0 ]\n"; system ( $_[ 0 ] ); } sub chkvm { if ( system( "virsh dominfo $opt_o | grep -E \"^State: +shut off\$\" >/dev/null 2>&1" ) ) { print "Domain $opt_o is running, shutdown first.\n\n"; exit 1; } } sub clonfiles { my ( $old_mac, $mac, $loopdev, $s ); my @dpart; my $tmpmnt = "/tmp/tmpmnt$$"; print( "\nCloning disk image file of $opt_o to $opt_t...\n" ); log_cmd( "virt-clone --original $opt_o --name $opt_n --file $opt_t --force" ); $_ = `grep \"mac address\" /etc/libvirt/qemu/${opt_o}.xml`; $_ =~ m/address=\'(\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)\'/; $old_mac = $1; $_ = `grep \"mac address\" /etc/libvirt/qemu/${opt_n}.xml`; $_ =~ m/address=\'(\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)\'/; $mac = $1; print "\nModifying network information in the new disk image...\n\n"; print "Now trying to find root partition...\n"; $loopdev = `losetup -f`; chomp $loopdev; log_cmd ( "losetup $loopdev $opt_t" ); $s = `kpartx -av $loopdev`; @dpart = split( /\n/, $s ); log_cmd ( "mkdir -p $tmpmnt" ); foreach $s ( @dpart ) { $s = $1 if ( $s =~ m/add map (\w+) / ); log_cmd ( "mount /dev/mapper/$s $tmpmnt" ); if ( -d "$tmpmnt/etc/sysconfig" ) { print "\nFound root partition on $s\n"; goto OUT; } else { log_cmd ( "umount $tmpmnt" ); } } print "Failed to find root partition.\n"; log_cmd ( "kpartx -d $loopdev; losetup -d $loopdev; rmdir $tmpmnt" ); exit 1; OUT: open ( IN, "<$tmpmnt/etc/udev/rules.d/70-persistent-net.rules" ); open ( OUT, ">$tmpmnt/etc/udev/rules.d/70-persistent-net.rules_new" ); while (<IN>) { $_ =~ s/\r\n$/\n/; $_ =~ s/==\"$old_mac\"/==\"$mac\"/; print OUT $_; } close OUT; close IN; log_cmd ( "mv -f $tmpmnt/etc/udev/rules.d/70-persistent-net.rules_new $tmpmnt/etc/udev/rules.d/70-persistent-net.rules" ); print "\nNew mac address in udev file....\n"; print "---------------------------\n"; system ( "grep \"address\" $tmpmnt/etc/udev/rules.d/70-persistent-net.rules" ); print "---------------------------\n"; open ( IN, "<$tmpmnt/etc/sysconfig/network" ); open ( OUT, ">$tmpmnt/etc/sysconfig/network_new" ); while (<IN>) { $_ =~ s/\r\n$/\n/; $_ =~ s/HOSTNAME\s*=\s*[\"\w-]+/HOSTNAME=$opt_s/; $_ =~ s/GATEWAY\s*=\s*[\"\d\.]+/GATEWAY=$opt_g/; print OUT $_; } close OUT; close IN; log_cmd ( "mv -f $tmpmnt/etc/sysconfig/network_new $tmpmnt/etc/sysconfig/network" ); print "\nNew network config file....\n"; print "---------------------------\n"; system ( "cat $tmpmnt/etc/sysconfig/network" ); print "---------------------------\n"; open ( IN, "<$tmpmnt/etc/sysconfig/network-scripts/ifcfg-eth0" ); open ( OUT, ">$tmpmnt/etc/sysconfig/network-scripts/ifcfg-eth0_new" ); while (<IN>) { $_ =~ s/\r\n$/\n/; $_ =~ s/HWADDR\s*=\s*[\"\w:]+/HWADDR=$mac/; $_ =~ s/IPADDR\s*=\s*[\"\d\.]+/IPADDR=$opt_i/; $_ =~ s/NETMASK\s*=\s*[\"\d\.]+/NETMASK=$opt_m/; $_ =~ s/GATEWAY\s*=\s*[\"\d\.]+/GATEWAY=$opt_g/; print OUT $_; } close OUT; close IN; log_cmd ( "mv -f $tmpmnt/etc/sysconfig/network-scripts/ifcfg-eth0_new $tmpmnt/etc/sysconfig/network-scripts/ifcfg-eth0" ); print "\nNew interface eth0 config file....\n"; print "---------------------------\n"; system ( "cat $tmpmnt/etc/sysconfig/network-scripts/ifcfg-eth0" ); print "---------------------------\n"; log_cmd ( "umount $tmpmnt" ); log_cmd ( "kpartx -d $loopdev; losetup -d $loopdev; rmdir $tmpmnt" ); } MAIN: { chkopts(); chkvm(); clonfiles(); if ( $opt_a ) { print "\nActivate new vm $opt_n\n"; log_cmd ( "virsh start $opt_n" ); } print "Done.\n"; }
ちなみにこれは、ここ のスクリプトを RHEL6 & LVM 環境用に修正したものです。RHEL6 は /etc/udev/rules.d/70-persistent-net.rules に MAC Address と ethX の対応が書き込まれているので MAC Address の変更時には要注意です。
次に、このスクリプトを繰り返し呼び出す使い捨てのスクリプトです。
clone_even.pl
#!/usr/bin/perl use strict; my $tmpfile = "/tmp/lvtmp$$.xml"; my ( $ip, $num, $snum ); for ( $num = 4; $num < 101; $num += 2 ) { $snum = sprintf( "%03d", $num ); $ip = sprintf( "%03d", 100 + $num ); print ( "/root/work/kvmclone.pl -o rhel60vm002 -n rhel60vm$snum -t /dev/datavg01/rhel60vm$snum -s rhel60vm$snum -i 10.7.24.$ip -m 255.255.0.0 -g 10.7.0.1\n" ); system ( "/root/work/kvmclone.pl -o rhel60vm002 -n rhel60vm$snum -t /dev/datavg01/rhel60vm$snum -s rhel60vm$snum -i 10.7.24.$ip -m 255.255.0.0 -g 10.7.0.1" ); } unlink $tmpfile;
clone_odd.pl
#!/usr/bin/perl use strict; my $tmpfile = "/tmp/lvtmp$$.xml"; my ( $ip, $num, $snum ); for ( $num = 3; $num < 101; $num += 2 ) { $snum = sprintf( "%03d", $num ); $ip = sprintf( "%03d", 100 + $num ); print ( "/root/work/kvmclone.pl -o rhel60vm001 -n rhel60vm$snum -t /dev/datavg01/rhel60vm$snum -s rhel60vm$snum -i 10.7.24.$ip -m 255.255.0.0 -g 10.7.0.1\n" ); system ( "/root/work/kvmclone.pl -o rhel60vm001 -n rhel60vm$snum -t /dev/datavg01/rhel60vm$snum -s rhel60vm$snum -i 10.7.24.$ip -m 255.255.0.0 -g 10.7.0.1" ); } unlink $tmpfile;
clone_even.pl と clone_odd.pl を実行して、一晩と半日待つと完成です。
# virsh list --all Id 名前 状態 ---------------------------------- - rhel60vm001 シャットオフ - rhel60vm002 シャットオフ - rhel60vm003 シャットオフ (中略) - rhel60vm099 シャットオフ - rhel60vm100 シャットオフ
VM の起動/停止
こんな感じのスクリプトで一気に起動/停止します。libvirt の python API 使ってみました。
startvm_all.py
#!/usr/bin/python import libvirt, time Conn = libvirt.open( "qemu:///system" ) BlackList = [] #####BlackList = [ "rhel60vm001", "rhel60vm002" ] if __name__ == "__main__": stopped_vms = Conn.listDefinedDomains() stopped_vms.sort() for name in stopped_vms: vm = Conn.lookupByName( name ) if vm.name() in BlackList: continue if not vm.name().startswith( "rhel60vm" ): continue print "Starting %s ..." % vm.name() vm.create() time.sleep( 1 ) print "Done."
stopvm_all.py
#!/usr/bin/python import libvirt, time Conn = libvirt.open( "qemu:///system" ) BlackList = [] ####BlackList = [ "rhel60vm001", "rhel60vm002" ] if __name__ == "__main__": running_vms = map( Conn.lookupByID, Conn.listDomainsID() ) running_vms.sort( lambda x, y: cmp( x.name(), y.name() ) ) for vm in running_vms: if vm.name() in BlackList: continue if not vm.name().startswith( "rhel60vm" ): continue print "Stopping %s ..." % vm.name() vm.shutdown() time.sleep( 0.5 ) print "Done."
100VM 起動できた?
はい。普通に起動しちゃいました。クラスタ環境なので Live Migration もできます。100VM 起動するのにかかる時間は・・・。
すいません。大人の事情でここには書けません。下記の無料セミナーにご参加いただければ、実機でお見せできると思います。(そういうことですか。。。)
・『クラウドを支えるKVMの現在と未来』
レッドハット株式会社 / 日本アイ・ビー・エム株式会社 共催
KVM の仮想ネットワーク管理については、「プロのためのLinuxシステム・ネットワーク管理技術」 もぜひご参照ください。m(_ _)m