[CentOS6][SOS JobScheduler] XenServer + JobScheduler でサーバを監視してスケールアウトを自動化する


Create: 2014/09/24
LastUpdate: 2014/09/25
[ メニューに戻る ]

XenServer6.2の仮想化環境で、Webサーバの自動スケールアウトを試してみました。
動作確認では、JobScheduler1.7 を使用しています。

シナリオ


ロードバランサ(LVS)の背後にいるWEBサーバ(Web01)の負荷を監視します。想定以上の負荷を検知したら、自動的にスケールアウトしてWEBサーバを1台追加し、WEBサーバ2台で負荷分散することにします。
下図は、JobSchedulerを利用した実装イメージです。
青のボックスは常駐のVMです。グレーのボックスはスケールアウトされるVMです。
楕円は、ジョブです。黄色のボックスは、スクリプトです。
ジョブ①を定期的に実行して、Web01上のCPU監視スクリプトを実行します。CPU監視スクリプトが閾値をこえる負荷を検知すると、JobSchedulerの外部APIで、JobChain(②~⑧) を実行してスケールアウトします。


各ジョブの概要は以下のとおり。
内訳は、①が監視用、②~⑤がWeb02生成用、⑥~⑦がWeb02コンテンツの最新化、⑧がWeb02のユーザへの解放となります。
コンテンツの最新化は無くてもいいのですが、JITL(scp)の使用例として入れています。

① web01_watch
このジョブは、一定間隔で実行するようにスケジューリングし、JITL(ssh)を利用してWeb01 上の 「CPU監視スクリプト」を実行します。
「CPU監視スクリプト」は、Apache の server-status から CPUのロードアベレージを取得します。
ロードアベレージが、閾値をこえたら、JobScheduler の外部API(http)を利用してJobChainを実行します。
なお、スケールアウトは1回のみとします。

② web_scaleout
JITL(ssh)を利用してXenServerのdom0(管理コンソール)上の「VM生成&起動スクリプト」を実行します。
「VM生成&起動スクリプト」は、事前に用意しておいた WEBサーバのテンプレートからVMを生成し、ホスト名、IPアドレスなどのパラメータを xenstore に設定してVMを起動します。
テンプレートには、VM起動時に xenstore からパラメータを取得して、ホスト名、IPアドレスなどを設定するスクリプトを仕込んでおきます。なお、この時点ではNICをダウンさせておきます。

③ wait1
上記②は、Web02 の起動完了を待たずにジョブが終了するので、Web02の起動が完了するまで、このジョブでスリープします。

④ reboot
上記②のホスト名、IPアドレス変更などを有効にしてネットワークに接続するために、Web02 をリブートします。この時点では、Web02 のNICがダウンしていて接続できないので、dom0上 で xe コマンドを実行して Web02をリブートします。

⑤ wait2
上記④は、Web02 の起動完了を待たずにジョブが終了するので、Web02の起動が完了するまで、このジョブでスリープします。

⑥ scp_get
JITL(scp)を利用して、Web01からコンテンツをGETします。

⑦ scp_put
JITL(scp)を利用して、上記⑥で取得したコンテンツを Web02 にPUTします。

⑧ lvs_add
JITL(ssh)を利用して、LVS上の「LVSに追加スクリプト」を実行します。
「LVSに追加スクリプト」は、LVSの weight を0以上に設定して、ロードバランサからWeb02にリクエストが振り向けられるようにします。

XenServerの準備(テンプレート作成)


Web01 をコピーしてテンプレートを作成します。
XenServerのテンプレート作成手順は、「 [XenServer 6.1.0] VM作成時にIPアドレス設定」を参考にしてください。
テンプレートに仕込むスクリプトは、以下のとおり。
  • [ファイル名]  /etc/init.d/xe-install
#!/bin/bash
#
# xe-install   setting hostname & network
#
# chkconfig: - 99 15
# description: setting hostname & network
# processname:
# config:
# pidfile:
#

# Source function library.
. /etc/rc.d/init.d/functions

# groval variable
prog=xe-install
lockfile=/var/lock/subsys/xe-install
RETVAL=0
LOG=/root/xe-install.log

# alredy run
if [ -e "${lockfile}" ]; then
  echo "ERROR: alredy run." >> ${LOG}
  exit ${RETVAL}
fi

# xenstore read
XENSTOREREAD=`which xenstore-read`
if [ ! -e "${XENSTOREREAD}" ]; then
  echo "ERROR: not found xenstore-read." >> ${LOG}
  exit ${RETVAL}
fi

NAME=`${XENSTOREREAD} vm-data/name` # hostname
IP=`${XENSTOREREAD} vm-data/ip`     # ip address
GW=`${XENSTOREREAD} vm-data/gw`     # default gateway
NM=`${XENSTOREREAD} vm-data/nm`     # netmask

# start
start() {

  touch ${lockfile}

  if [ "${NAME}" != "" ]; then
    /bin/cp -p /etc/sysconfig/network /etc/sysconfig/network.ORG
    cat << EOF > /etc/sysconfig/network
NETWORKING=yes
HOSTNAME=${NAME}
EOF
    echo "create /etc/sysconfig/network" >> ${LOG}
  else
    echo "ERROR: unknown vm-data/name." >> ${LOG}
  fi

  if [ "${IP}" != "" -a "${GW}" != "" -a "${NM}" != "" ]; then
    /bin/cp -p  /etc/sysconfig/network-scripts/ifcfg-eth0  /etc/sysconfig/network-scripts/ifcfg-eth0.ORG
    cat << EOF > /etc/sysconfig/network-scripts/ifcfg-eth0
DEVICE=eth0
BOOTPROTO=none
GATEWAY=${GW}
IPADDR=${IP}
NETMASK=${NM}
ONBOOT=yes
TYPE=Ethernet
NM_CONTROLLED=no
IPV6INIT=no
IPV6_AUTOCONF=no
USERCTL=no
EOF
    echo "create /etc/sysconfig/network-scripts/ifcfg-eth0" >> ${LOG}
  else
    echo "ERROR: unknown vm-data/ip or vm-data/gw or vm-data/nm." >> ${LOG}
  fi

}

#
case "$1" in
  start)
        start
        ;;
  stop)
        ;;
  *)
        echo $"Usage: $prog {start|stop}"
        RETVAL=2
esac

exit ${RETVAL}
このスクリプトは、xenstore から入力したパラメータをもとに、/etc/sysconfig/network と /etc/sysconfig/network-scripts/ifcfg-eth0 を作り直します。ただし、1回実行するとロックファイルを作成して、2回目以降は処理をおこないません。
VM起動時に自動実行するように設定します。
# chmod +x /etc/init.d/xe-install
# chkconfig --add xe-install
# chkconfig xe-install on
スクリプトを設置したら、下記の名称でテンプレート化します。
  • [テンプレート名]  DEVWEB00

XenServer準備(dom0のスクリプト作成)


上記テンプレートからVMを生成して起動するスクリプトを用意します。
スクリプトの内容は以下のとおり。
  • [ファイル名]  /root/tools/vm_install.sh
#!/bin/bash

# 引数チェック
if [ $# -ne 4 ]; then
  echo "usage: $0 hostname ip gateway netmask"
  exit 1
fi

TEMPLATE_NAME=DEVWEB00
HOSTNAME=$1
IP=$2
GW=$3
NM=$4

# テンプレートからVM作成

UUID=`xe vm-install template=${TEMPLATE_NAME}  new-name-label=${HOSTNAME}`
if [ "${UUID}" = "" ]; then
  echo "ERROR: vm-install"
  exit 1
fi
echo "vm-install ok."

# xenstore にパラメータを設定する
# ここで設定した値をVMの /etc/init.d/xe-install で参照する

xe vm-param-set uuid=${UUID} xenstore-data:vm-data/ip=${IP}
if [ $? -ne 0 ]; then
  echo "ERROR: vm-param-set ip"
  exit 1
fi

xe vm-param-set uuid=${UUID} xenstore-data:vm-data/gw=${GW}
if [ $? -ne 0 ]; then
  echo "ERROR: vm-param-set gw"
  exit 1
fi

xe vm-param-set uuid=${UUID} xenstore-data:vm-data/nm=${NM}
if [ $? -ne 0 ]; then
  echo "ERROR: vm-param-set nm"
  exit 1
fi

xe vm-param-set uuid=${UUID} xenstore-data:vm-data/name=${HOSTNAME}
if [ $? -ne 0 ]; then
  echo "ERROR: vm-param-set name"
  exit 1
fi

echo "vm-param-set ok."

# VM起動

xe vm-start uuid=${UUID}
if [ $? -ne 0 ]; then
  echo "ERROR: vm-start"
  exit 1
fi

echo "vm-start ok."

exit 0

このスクリプトは、上記で作成したテンプレートからVMを生成し、xenstore にパラメータをセットした後、VMを起動します。パラメータにセットする値は引数で指定します。
ジョブから実行できるように、実行権限をつけます。
# chmod 777 /root/tools/vm-install.sh

LVSの準備


LVSはロードバランサです。LVS(NAT)については、「 [CentOS6][IPVS+Keepalived] 異なるサブネットでWEBサーバの負荷分散(NAT)」と「 [TIPS][CentOS 6] iptables で IPマスカレード と DNAT を設定する」を参考にしてください。
まず、Keepalived は、Web01とWeb02に負荷分散するように設定します。
ただし、Web02 は、Weight を "0" にして、リクエストを振り向けないようにします。
以下は、LVSの状態です。
# ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.1.90:http rr
  -> 10.0.0.91:http               Masq    10     0          0
  -> 10.0.0.92:http               Masq    0      0          0
次に、LVSのweight を変更するスクリプトを用意します。
内容は以下のとおり。
  • [ファイル名]  /root/tools/lvs_add.sh
#!/bin/bash

# 引数チェック

if [ $# -ne 2 ]; then
  echo "usage: $0 ip weight"
  exit 1
fi

HOST=$1
WEIGHT=$2

# LVS の振り分け開始

ipvsadm -e -t 192.168.1.90:80 -r ${HOST}:80 -m -w ${WEIGHT}
if [ $? -ne 0 ]; then
  echo "ERROR: ipvsadm"
  exit 1
fi

# LVS のステータス表示

echo
ipvsadm -l

exit 0

このスクリプトは、引数で指定したIPアドレスの weight を変更します。
ジョブから実行できるように実行権限をつけます。
# chmod 777 /root/tools/lvs_add.sh

Web01の準備


Apache の server-status を利用して負荷を監視するスクリプトを用意します。
まず、server-status を利用できるように Apache の設定を変更します。
ExtendedStatus On
<Location /server-status>
    SetHandler server-status
    Order deny,allow
    Deny from all
    Allow from 127.0.0.1
</Location>
以下のように server-status が見れれば Apache の設定はOKです。
# curl -s http://127.0.0.1/server-status?auto
Total Accesses: 60454
Total kBytes: 8007
CPULoad: .0654992
Uptime: 32321
ReqPerSec: 1.87042
BytesPerSec: 253.679
BytesPerReq: 135.627
BusyWorkers: 1
IdleWorkers: 19
Scoreboard: .__..._.._....._...._......._...................._.......................................................W.........................._......_...._.._._..._._................_........................_.................................._._.....................

監視スクリプトの内容は、以下のとおり。
  • [ファイル名]  /root/tools/check_perf.sh
#!/bin/bash

# 引数チェック

if [ $# -ne 1 ]; then
  echo "usage: $0 limit"
  exit 1
fi

LIMIT=$1
LOCK=/root/tools/add_web02.lock
ADD_WEB02_XML=/root/tools/add_web02.xml
JETTY_URL=http://192.168.1.61:40444/jobscheduler/engine-cpp

# Apache server-status から CPU Load の値を取得

CPU_LOAD=`curl -s http://127.0.0.1/server-status?auto \
| grep 'CPULoad'\
| awk -F: '{printf("%f",$2)}'`

# CPU Load が Limit をこえていないかチェック

echo
if [ "${CPU_LOAD}" != "" ]; then

  # 値の比較
  okng=`echo "${CPU_LOAD} > ${LIMIT}" | bc`
  if [ ${okng} -eq 1 ]; then

    # Limit をこえたのでエラーメッセージ
    echo "ERROR: `hostname` : CPU LOAD=${CPU_LOAD}"

    # Web02 を追加済みなら処理しない
    if [ ! -e "${LOCK}" ]; then

      # JobSchedule に Web02 を追加する Order を登録
      touch ${LOCK}
      curl -s ${JETTY_URL} -X POST -d @${ADD_WEB02_XML}
      if [ $? -ne 0 ]; then
          echo "ERROR: add web02"
          exit 1
      fi
    fi
    exit 0
  fi
fi

echo "SAFE: `hostname` : CPU LOAD=${CPU_LOAD}"

exit 0

このスクリプトは、Apacheのserver-statusで取得したCPULoad の値が、引数で指定された閾値を超えると、スケールアウト用のJobChainを実行します。なお、2回目以降の閾値越えは、検知してもJobChainは実行しません。
JobShedulerのJettyにPOSTする外部APIコマンドは以下のとおり。
JonChain の即時実行を指示します。
  • [ファイル名]  /root/tools/add_web02.xml
<add_order job_chain="/AutoScale/AddWeb02" id="add_web02"/>

JobSchedulerの準備(監視Jobの作成)


Web01の監視スクリプトを実行するジョブの定義は以下のとおり。
[ファイル名]  web01_watch.job.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<job stop_on_error="yes">
    <params>
      <param name="host" value="192.168.1.91"/>
      <param name="user" value="root"/>
      <param name="password" value="p@ssw0rd"/>
      <param name="auth_method" value="password"/>
      <param name="command" value="/root/tools/check_perf.sh 0.3"/>
    </params>
    <script language="java" java_class_path="" java_class="sos.scheduler.job.JobSchedulerSSHJob"/>
    <run_time>
      <period absolute_repeat="00:05" begin="07:00" end="22:00"/>
    </run_time>
</job>
このジョブは、毎日7:00~22:00の間に5分間隔で実行するようにスケジューリングします。
JITL(ssh)を利用してWeb01の監視スクリプト(check_perf.sh) を実行します。
監視スクリプトの引数の閾値(0.3) はテスト用に低く設定しています。

JobSchedulerの準備(スケールアウトJobChain の作成)


スケールアウト用のJobChainの定義内容は以下のとおり。
  • [ファイル名]  AddWeb02.job_chain.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<job_chain title="Web Server02 Auto Scaleout">
    <job_chain_node  state="JobChainStart" job="/sos/jitl/JobChainStart" next_state="web_scaleout" error_state="!error"/>

    <!-- 1.VM create-start & hostname,network setting-->
    <job_chain_node  state="web_scaleout" job="ssh" next_state="wait1" error_state="!error"/>
    <job_chain_node  state="wait1" job="wait" next_state="reboot" error_state="!error"/>
    <job_chain_node  state="reboot" job="ssh" next_state="wait2" error_state="!error"/>
    <job_chain_node  state="wait2" job="wait" next_state="scp_get" error_state="!error"/>

    <!-- 2.Content update -->
    <job_chain_node  state="scp_get" job="scp" next_state="scp_put" error_state="!error"/>
    <job_chain_node  state="scp_put" job="scp" next_state="lvs_add" error_state="!error"/>

    <!-- 3.LVS add -->
    <job_chain_node  state="lvs_add" job="ssh" next_state="JobChainEnd" error_state="!error"/>

    <job_chain_node  state="JobChainEnd" job="/sos/jitl/JobChainEnd" next_state="success" error_state="!error"/>
    <job_chain_node  state="success"/>
    <job_chain_node  state="!error"/>
</job_chain>

各stateで実行するジョブのパラメータを定義した設定ファイルは以下のとおり。
  • [ファイル名]  AddWeb02.config.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<?xml-stylesheet type="text/xsl" href="scheduler_configuration_documentation.xsl"?>
<settings>
    <job_chain name="AddWeb02">
        <order>
            <params/>
            <process state="web_scaleout">
                <params>
                    <param name="host" value="192.168.1.20"/>
                    <param name="user" value="root"/>
                    <param name="password" value="p@ssw0rd"/>
                    <param name="auth_method" value="password"/>
                    <param name="command" value="/root/tools/vm_install.sh devweb02 10.0.0.92 10.0.0.90 255.255.255.0"/>
                </params>
            </process>
            <process state="reboot">
                <params>
                    <param name="host" value="192.168.1.20"/>
                    <param name="user" value="root"/>
                    <param name="password" value="p@ssw0rd"/>
                    <param name="auth_method" value="password"/>
                    <param name="command" value="xe vm-reboot vm=devweb02"/>
                </params>
            </process>
            <process state="wait1">
                <params>
                  <param name="wait_sec" value="30"/>
                </params>
            </process>
            <process state="wait2">
                <params>
                  <param name="wait_sec" value="60"/>
                </params>
            </process>
            <process state="scp_get">
                <params>
                  <param name="host" value="192.168.1.91"/>
                  <param name="user" value="root"/>
                  <param name="password" value="p@ssw0rd"/>
                  <param name="auth_method" value="password"/>
                  <param name="action" value="get"/>
                  <param name="file_list" value="/var/www/html/index.html"/>
                  <param name="local_dir" value="/home/jobs/data"/>
                </params>
            </process>
            <process state="scp_put">
                <params>
                  <param name="host" value="192.168.1.92"/>
                  <param name="user" value="root"/>
                  <param name="password" value="p@ssw0rd"/>
                  <param name="auth_method" value="password"/>
                  <param name="action" value="put"/>
                  <param name="file_list" value="index.html"/>
                  <param name="local_dir" value="/home/jobs/data"/>
                  <param name="remote_dir" value="/var/www/html"/>
                </params>
            </process>
            <process state="lvs_add">
                <params>
                  <param name="host" value="192.168.1.90"/>
                  <param name="user" value="root"/>
                  <param name="password" value="p@ssw0rd"/>
                  <param name="auth_method" value="password"/>
                  <param name="command" value="/root/tools/lvs_add.sh 10.0.0.92 10"/>
                </params>
            </process>
        </order>
    </job_chain>
</settings>

JITL(ssh) を利用したジョブは以下のとおり。
パラメータは、JobChainの設定ファイルで定義することにしています。
  • [ファイル名]  ssh.job.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<job order="yes" stop_on_error="yes">
    <params/>
    <script language="java" java_class_path="" java_class="sos.scheduler.job.JobSchedulerSSHJob"/>
    <monitor name="configuration_monitor" ordering="0">
        <script java_class="sos.scheduler.managed.configuration.ConfigurationOrderMonitor" language="java"/>
    </monitor>
    <run_time />
</job>

JITL(scp) を利用したジョブは以下のとおり。
パラメータは、JobChainの設定ファイルで定義することにしています。
  • [ファイル名]  scp.job.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<job order="yes" stop_on_error="yes">
    <params/>
    <script language="java" java_class_path="" java_class="sos.scheduler.job.JobSchedulerSCPJob"/>
    <monitor name="configuration_monitor" ordering="0">
        <script java_class="sos.scheduler.managed.configuration.ConfigurationOrderMonitor" language="java"/>
    </monitor>
    <run_time />
</job>
sleep で待機するジョブは以下のとおり。
パラメータは、JobChainの設定ファイルで定義することにしています。
  • [ファイル名]  wait.job.xml 
<?xml version="1.0" encoding="ISO-8859-1"?>
<job order="yes" stop_on_error="yes">
    <params/>
    <script language="shell">
        <![CDATA[
if [ "${SCHEDULER_PARAM_WAIT_SEC}" = "" ]; then
  exit 1
fi
echo "waiting  ${SCHEDULER_PARAM_WAIT_SEC} sec."
sleep ${SCHEDULER_PARAM_WAIT_SEC}
exit 0
        ]]>
    </script>
    <monitor name="configuration_monitor" ordering="0">
        <script java_class="sos.scheduler.managed.configuration.ConfigurationOrderMonitor" language="java"/>
    </monitor>
    <run_time />
</job>


動作確認


まず、上記設定を確認します。
XenServerの状況をXenCenter で表示すると下図のとおり。
テンプレート(DEVWEB00)とLVSサーバ(devlvs01)、Web01サーバ(devweb01)を見ることができます。


LVSの状況は以下のとおり。
Web02(10.0.0.92)の Weight が "0" なので、Web01(10.0.0.91) だけが仕事するようになっています。
# ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.1.90:http rr
  -> 10.0.0.91:http               Masq    10     0          0
  -> 10.0.0.92:http               Masq    0      0          0
JOCでジョブの状態を表示すると下図のとおり。
監視ジョブ(web01_watch) は定期的に実行されるようにスケジューリングされています。


JobChain は下図のように表示されます。



ここからは、動作を確認してみます。
監視ジョブ(web01_watch)は、スケジューリングされているので5分に1回、自動的に実行されます。
Web01の負荷が閾値以下の場合、監視ジョブの実行結果は下図のように表示されます。



ここで、ApacheBench を利用して Web01 に負荷をかけてみます。
1,000ユーザが同時に10,000 リクエストを実行します。
# ab -n 10000 -c 1000 http://192.168.1.90/
監視ジョブが、閾値を超える負荷を検知すると、実行結果は下図のように表示されます。
赤枠は、外部APIコマンドのレスポンスです。


下図は、スケールアウト実行中のJOCです。


スケールアウトが完了すると、XenCenterは下図のように表示されます。
Web02(devweb02) が生成され起動しています。


ロードバランサの状態は以下のとおり。
Web01とWeb02の2台で負荷分散しています。
# ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.1.90:http rr
  -> 10.0.0.91:http               Masq    10     0          106
  -> 10.0.0.92:http               Masq    10     1          107