めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

32core CPU / 160GB Memory サーバと RHEL6 KVM で 100VM 起動してみました。

通りすがりのマシンルームに 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