"Wakame"で始めるクラウドコントロール

第4回クラウド上でデータベースをスケールアウトさせる方法と実例

今回は、データベースとして広く普及しているMySQLを例に、Amazon EC2上でMySQLのレプリケーション機能を用いてMaster-Slave構成を作り、そのSlaveを実に手軽に追加できるようにするための手順について説明します。

MySQLのレプリケーション機能でWakameは本領を発揮する

一般的に、MySQLのレプリケーションとは、Masterと呼ばれるデータの原本1つを、可能な限り同期複製させることが出来る機能のことを言います。複製されたものはSlaveと呼ばれ、1つのMasterに対して複数接続することが可能です。

より複雑な構成も取れるのですが、基本的なトポロジはスター型となります。レコードの読み出しクエリであれば、各Slaveへ分散させて性能を向上させることが出来ます。そのため、サイトのトラフィックが増加して来た時、まず最初に行われる対策として良く利用される機能でもあります。

これらの作業に必要な手順は、非常に複雑なので、MySQLのプロダクトをうまくコントロールする仕組みが不可欠です。早速Wakameでこの手順を自動化してみることにしましょう。

基本原理を理解する

まず、基本原理を理解するために、Amazon EC2とAmazon EBSによるMySQLレプリケーション設定方法を示しておきます。Wakameで簡単に実行できるようにする方法については、後ほど詳しく見ていくことにします。

Amazon EC2で試せるスクリプトの概要と準備

参考のために、MySQLとAmazon EBSを連動させた動作確認目的でシェルスクリプトを作成しました。ここで作成されたシェルスクリプトを使うだけでもMySQL Slave追加作業が簡略化されます。

スクリプトの動作処理は、大きく分けて3つです。

  1. MySQL Master用datadirとしAmazon EBS Volumeを生成
  2. 1で生成したAmazon EBS VolumeからAmazon EBS Snapshotを生成
  3. 2で生成したAmazon EBS SnapshotからMySQL Slave用datadirとしてAmazon EBS Volumeを生成

スクリプトの動作解説

スクリプトの動作前提条件は下記の通りです。

  1. Amazon EC2インスタンスが起動している事
  2. 起動しているAmazon EC2インスタンスにAmazon EC2 API Toolsの環境が整っている事
    ツールの整備方法を参考に環境設定してください。

また、スクリプトとして全部で6つのファイルが登場します。

  1. _wakame-common.sh
  2. wakame-ebs-mysql-master-create-volume.sh
  3. wakame-ebs-mysql-master-init.sh
  4. wakame-ebs-mysql-master-add-repl-user.sh
  5. wakame-ebs-mysql-master-make-snapshot.sh
  6. wakame-ebs-mysql-slave-restore.sh

各スクリプトの概要は、以下の通りです。

なお、Amazon EC2上でMySQLレプリケーション構成を構築するには、以下の手順が必要になりますので、詳細を知りたい方はぜひスクリプトの流れと合わせて追いかけて見てください。

_wakame-common.sh
  • 全スクリプトからincludeされるファイル
  • 共通設定項目
wakame-ebs-mysql-master-create-volume.h
  • Amazon EBS Volumeを新規生成
  • 生成したAmazon EBS Volumeをデバイス名指定でattach
  • Amazon EBS Volume上にファイルシステムを生成
  • Amazon EBS VolumeをMySQL Master用datadirとしてマウント
wakame-ebs-mysql-master-init.sh
  • MySQL Master用datadirのオーナーをmysqlへ変更
  • MySQL Master用datadirを初期化
wakame-ebs-mysql-master-add-repl-user.sh
  • レプリケーション用MySQLアカウントをMySQL Masterに作成
wakame-ebs-mysql-master-make-snapshot.sh
  • attach済みAmazon EBS Volumeのvolume-idを取得
  • MySQL Masterに対し"FLUSH TABLES WITH READ LOCK"でデータが更新されないようにテーブルをロック
  • MySQL Masterのポジション情報を取得
  • MySQL Slave用にmaster.infoを生成
  • MySQL Master用datadirからAmazon EBS Snapshotを生成
  • Amazon EBS Snapshot生成後、"UNLOCK TABLES;"でMySQL Masterのテーブルロックを解除
wakame-ebs-mysql-slave-restore.sh
  • MySQL Master用datadirをもとに生成したAmazon EBS Snapshotのsnapshot-idを指定し、Amazon EBS Volumeを新規生成
  • Amazon EBS Snapshotから生成したAmazon EBS Volumeを、Amazon EC2インスタンスIDとデバイス名を指定してattach
  • attachしたAmazon EBS VolumeをMySQL Slave用datadirとしてマウント

実行方法と結果

今回のスクリプトを用いてMySQLレプリケーション構成を構築するには、MySQL Master用インスタンスとMySQL Slave用インスタンスが必要です。

どちらのインスタンスでの作業であるかを区別するため、プロンプトを以下の通りとします。

mysql-common#共通
mysql-master#MySQL Maser
mysql-slave#MySQL Slave

まず、同一ディレクトリ内にシェルスクリプトを保存します。次に、共通項目ファイル_wakame-common.shを除き、chmodコマンドでシェルスクリプトに実行許可を付与します。

mysql-common# chmod +x wakame-ebs-mysql-create-volume.sh
mysql-common# chmod +x wakame-ebs-mysql-master-init.sh
mysql-common# chmod +x wakame-ebs-mysql-master-add-repl-user.sh
mysql-common# chmod +x wakame-ebs-mysql-master-make-snapshot.sh
mysql-common# chmod +x wakame-ebs-mysql-slave-restore.sh

各MySQLのdatadir用ディレクトリを作っておきます。

mysql-master# mkdir -p /home/wakame/mysql/data
mysql-slave# mkdir -p /home/wakame/mysql/data-slave

my.cnfでdatadirの値にそれぞれのディレクトリを設定します。以下は、my.cnfの例です。

MySQL Master用my.cnfのmysqldセクション
[mysqld]
server-id = 1
datadir = /home/wakame/mysql/data
log_bin = /home/wakame/mysql/data/mysql-bin.log
MySQL Slave用my.cnfのmysqldセクション
[mysqld]
server-id = 2
datadir = /home/wakame/mysql/data-slave

それでは、シェルスクリプトを実行してみます。root権限を必要とするため、rootでシェルスクリプトを実行します。

wakame-ebs-mysql-master-create-volume.sh
mysql-master# ./wakame-ebs-mysql-master-create-volume.sh
$ ec2-create-volume -z us-east-1c -s 1
ebs_master_volume:vol-dbaa45b2

$ ec2-attach-volume -d /dev/sdm -i i-6dce9f04 vol-dbaa45b2
ATTACHMENT      vol-dbaa45b2    i-6dce9f04      /dev/sdm        attaching       2009-06-10T07:58:59+0000

. .
$ yes | mkfs -t ext3 /dev/sdm >/dev/null 2>&1

/dev/sdm on /home/wakame/mysql/data type ext3 (rw)
  1. Amazon EBS Volumeを生成。volume-idはvol-dbaa45b2
  2. デバイス名/dev/sdmvol-dbaa45b2をattach
  3. /dev/sdmext3でファイルシステム構築
  4. /dev/sdm/home/wakame/mysql/dataにマウント
wakame-ebs-mysql-master-init.sh
mysql-master# ./wakame-ebs-mysql-master-init.sh
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sdm               1032088     34072    945588   4% /home/wakame/mysql/data
  1. /home/wakame/myql/dataをMySQLデータ領域として初期化

MySQL Masterの起動は今回のスクリプトでは行わないので、手動で起動します。

mysql-master# /etc/init.d/mysql start
wakame-ebs-mysql-master-add-repl-user.sh
mysql-master# ./wakame-ebs-mysql-master-add-repl-user.sh
  1. レプリケーション用MySQLアカウントを登録、反映
wakame-ebs-mysql-master-make-snapshot.sh
mysql-master# ./wakame-ebs-mysql-master-make-snapshot.sh
volume-id:vol-dbaa45b2

... flush tables with read lock;
... show master status;
... make master.info
$ ec2-create-snapshot vol-dbaa45b2
. .
... unlock tables;
>>> snapshot_id = snap-3b0fcc52
  1. MySQL Master用datadirのAmazon EBS Volumeのvolume-idを取得。volume-idはvol-dbaa45b2
  2. MySQL Masterのデータが更新されないように、テーブルをロック
  3. MySQL Masterのポジション情報を取得
  4. MySQL Slave用にmaster.infoを生成、オーナー変更
  5. vol-dbaa45b2のAmazon EBS Snapshotを生成。snapshot-idはsnap-3b0fcc52
  6. テーブルロックを解除
wakame-ebs-mysql-slave-restore.sh
mysql-slave# ./wakame-ebs-mysql-slave-restore.sh snap-3b0fcc52
.
ATTACHMENT      vol-8caa45e5    i-6dce9f04      /dev/sdn        attaching       2009-06-10T08:18:31+0000
. . /dev/sdn on /home/wakame/mysql/data-slave type ext3 (rw)

VOLUME  vol-8caa45e5    1       snap-3b0fcc52   us-east-1c      in-use  2009-06-10T08:18:18+0000
ATTACHMENT      vol-8caa45e5    i-6dce9f04      /dev/sdn        attached        2009-06-10T08:18:31+0000
  1. snapshot-idsnap-3b0fcc52からリストアしてAmazon EBS Volumeを生成。volume-idはvol-8caa45e5
  2. デバイス名/dev/sdnvol-8caa45e5をattach
  3. /dev/sdn/home/wakame/mysql/data-slaveにマウント

MySQL Slaveの起動は今回のスクリプトでは行わないので、手動で起動します。

mysql-slave# /etc/init.d/mysql start

slaveが機能しているかを確認します。

mysql-slave# mysql -uroot
mysql> show slave status \G
*************************** 1. row ***************************
             Slave_IO_State: Waiting for master to send event
                Master_Host: 10.254.183.243
                Master_User: wakame-repl
                Master_Port: 3306
              Connect_Retry: 60
            Master_Log_File: mysql-bin.000001
        Read_Master_Log_Pos: 340
             Relay_Log_File: mysqld-relay-bin.000002
              Relay_Log_Pos: 235
      Relay_Master_Log_File: mysql-bin.000001
           Slave_IO_Running: Yes
          Slave_SQL_Running: Yes
            Replicate_Do_DB:
        Replicate_Ignore_DB:
         Replicate_Do_Table:
     Replicate_Ignore_Table:
    Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
                 Last_Errno: 0
                 Last_Error:
               Skip_Counter: 0
        Exec_Master_Log_Pos: 340
            Relay_Log_Space: 235
            Until_Condition: None
             Until_Log_File:
              Until_Log_Pos: 0
         Master_SSL_Allowed: No
         Master_SSL_CA_File:
         Master_SSL_CA_Path:
            Master_SSL_Cert:
          Master_SSL_Cipher:
             Master_SSL_Key:
      Seconds_Behind_Master: 0
1 row in set (0.00 sec)

MySQLレプリケーション構成にMySQL Slaveが追加され、手順が正しく動いていることを確認出来ました。必要な処理の流れはご理解いただけたのではないでしょうか。

これらのスクリプトは便利なのですが、Wakameの上で実行すればコマンド一発で指示出来るようになるほか、サーバ間の処理タイミングを同期してくれるため、人間の目で確認する作業を全て機械化することが出来ます。

それでは、スクリプトの処理をWakameに記述して行くことにしましょう。

Wakameを使って、MySQLレプリケーションを自動化する

今回は、WakameのVersion 0.3.1を利用します。

WakameにMySQL Slaveの手順を記述する

MySQL SlaveをWakameのサービスとして追加するために、3つのファイルを修正・作成しました。サービスの性質によって対象ファイルが異なります。

  1. /home/wakame/corelib/lib/wakame/service.rb
  2. /home/wakame/corelib/lib/wakame/configuration_template.rb
  3. /home/wakame/corelib/config/template/mysql-slave/my-slave.cnf
/home/wakame/corelib/lib/wakame/service.rb
  • Wakame MasterとWakame Agentに対する処理
/home/wakame/corelib/lib/wakame/configuration_template.rb
  • configファイル用テンプレートのパス、ファイル名を指定
/home/wakame/corelib/config/template/mysql-slave/my-slave.cnf
  • /home/wakame/corelib/lib/wakame/configuration_template.rbで指定されたconfig用テンプレートファイル

このうち、先ほど紹介したシェルスクリプトの手順を記述する対象ファイルは/home/wakame/corelib/lib/wakame/service.rbです。6つのファイルの内容をすべて記述している訳ではなく、MySQL Slaveに関する手順のみ記述しています。

リスト1 /home/wakame/corelib/lib/wakame/service.rb
    class MySQL_Slave < Property
      attr_reader :basedir, :mysqld_datadir, :mysqld_port, :mysqld_server_id, :mysqld_log_bin, :ebs_volume, :ebs_device

      def initialize
        super()
        @max_instances = 5

        # ▽ _wakame-common.sh ここから
        @template = ConfigurationTemplate::MySQLSlaveTemplate.new()
        @basedir = '/home/wakame/mysql'

        @mysqld_server_id = nil

        @mysqld_port = 3307
        @mysqld_datadir = File.expand_path('data-slave', @basedir)

        @ebs_volume = 'vol-12ac437b'  # master volume_id
        @ebs_device = '/dev/sdn'      # slave mount point
        @ebs_mount_option = 'noatime'

        @mysqld_master_host = nil
        @mysqld_master_user = 'wakame-repl'
        @mysqld_master_pass = 'wakame-slave'
        @mysqld_master_port = 3306
        @mysqld_master_datadir = File.expand_path('data', @basedir)
        # △ _wakame-common.sh ここまで

        @duplicable = true
      end

      # in wakame-master
      def before_start(svc, action)
        agent_ip = svc.agent.agent_ip
        Wakame.log.debug("#{svc.agent}")
        Wakame.log.debug("mysql_slave.agent.agent_ip = #{agent_ip}")

        # ▽wakame-ebs-mysql-master-make-snapshot.sh ここから
        # directory
        command = "ssh -i #{Wakame.config.ssh_private_key} -o \"StrictHostKeyChecking no\" -o \"UserKnownHostsFile #{Wakame.config.ssh_known_hosts}\" root@#{agent_ip} \"[ -d #{@mysqld_datadir} ] || mkdir -p #{@mys
qld_datadir}\""
        Wakame.log.debug(command)
        system(command)

        # /dev/ebs_device
        command = "ssh -i #{Wakame.config.ssh_private_key} -o \"StrictHostKeyChecking no\" -o \"UserKnownHostsFile #{Wakame.config.ssh_known_hosts}\" root@#{agent_ip} \"[ -b #{@ebs_device} ]\""
        Wakame.log.debug(command)
        system(command)
        if $? == 0
          Wakame.log.debug("The EBS volume(slave) device is not ready to attach: #{@ebs_device}")
          return
        end

        vm_manipulator = VmManipulator.create
        volume_map = vm_manipulator.describe_volume(@ebs_volume)
        Wakame.log.debug("describe_volume(#{@ebs_volume}): #{volume_map.inspect}")
        if volume_map['status'] == 'in-use'
          # Nothin to be done
        else
          Wakame.log.debug("The EBS volume(slave) is not ready to attach: #{@ebs_volume}")
          return
        end


        @mysqld_master_host = svc.cluster.fetch_mysql_master_ip
        Wakame.log.debug("mysqld_master_host = #{@mysqld_master_host}")
        system("echo show master status | /usr/bin/mysql -h#{@mysqld_master_host} -P#{@mysqld_master_port} -u#{@mysqld_master_user}  -p#{@mysqld_master_pass}")
        if $? != 0
          raise "Can't connect mysql master: #{@mysqld_master_host}:#{@mysqld_master_port}"
        end

        system("echo 'FLUSH TABLES WITH READ LOCK;' | /usr/bin/mysql -h#{@mysqld_master_host} -P#{@mysqld_master_port} -u#{@mysqld_master_user}  -p#{@mysqld_master_pass} -s")
        master_status = `echo show master status | /usr/bin/mysql -h#{@mysqld_master_host} -P#{@mysqld_master_port} -u#{@mysqld_master_user}  -p#{@mysqld_master_pass} -s`.to_s.split(/\t/)[0..1]

        # mysql/data/master.info
        master_infos = []
        master_infos << 14
        master_infos << master_status[0]
        master_infos << master_status[1]
        master_infos << @mysqld_master_host
        master_infos << @mysqld_master_user
        master_infos << @mysqld_master_pass
        master_infos << @mysqld_master_port
        master_infos << 60
        master_infos << 0
        master_infos << ""
        master_infos << ""
        master_infos << ""
        master_infos << ""
        master_infos << ""
        master_infos << ""

        tmp_output_basedir = File.expand_path(Wakame.gen_id, "/tmp")
        FileUtils.mkdir_p tmp_output_basedir
        master_info = File.expand_path('master.info', tmp_output_basedir)
        file = File.new(master_info, "w")
        file.puts(master_infos.join("\n"))
        file.chmod(0664)
        file.close

        3.times do |i|
          system("/bin/sync")
          sleep 1.0
        end

        # scp master:$datadir/master.info slave:$datadir/master.info
        command = "scp -i #{Wakame.config.ssh_private_key} -o \"StrictHostKeyChecking no\" -o \"UserKnownHostsFile #{Wakame.config.ssh_known_hosts}\" #{master_info} root@#{@mysqld_master_host}:#{@mysqld_master_dat
adir}/"
        Wakame.log.debug(command)
        system(command)

        # ssh slave chown mysql:mysql $datadir/master.info
        command = "ssh -i #{Wakame.config.ssh_private_key} -o \"StrictHostKeyChecking no\" -o \"UserKnownHostsFile #{Wakame.config.ssh_known_hosts}\" root@#{@mysqld_master_host} chown mysql:mysql #{@mysqld_master_
datadir}/master.info"
        Wakame.log.debug(command)
        system(command)

        # sync
        3.times do |i|
          command = "ssh -i #{Wakame.config.ssh_private_key} -o \"StrictHostKeyChecking no\" -o \"UserKnownHostsFile #{Wakame.config.ssh_known_hosts}\" root@#{@mysqld_master_host} /bin/sync"
          system(command)
          sleep 1.0
        end

        FileUtils.rm_rf tmp_output_basedir

        # 2. snapshot
        Wakame.log.debug("create_snapshot (#{@ebs_volume})")
        snapshot_map = vm_manipulator.create_snapshot(@ebs_volume)
        16.times do |i|
          Wakame.log.debug("describe_snapshot(#{snapshot_map.snapshotId}) ... #{i}")
          snapshot_map = vm_manipulator.describe_snapshot(snapshot_map["snapshotId"])
          if snapshot_map["status"] == "completed"
            break
          end
          sleep 1.0
        end
        if snapshot_map["status"] != "completed"
          raise "#{snapshot_map.snapshotId} status is #{snapshot_map.status}"
        end

        # 3. unlock mysql-master
        system("echo 'UNLOCK TABLES;' | /usr/bin/mysql -h#{@mysqld_master_host} -P#{@mysqld_master_port} -u#{@mysqld_master_user}  -p#{@mysqld_master_pass}")

        # create volume /dev/xxxx
        Wakame.log.debug("create_volume_from_snapshot(#{volume_map.availabilityZone}, #{snapshot_map.snapshotId})")
        created_volume_from_snapshot_map = vm_manipulator.create_volume_from_snapshot(volume_map["availabilityZone"], snapshot_map["snapshotId"])
        volume_from_snapshot_map = created_volume_from_snapshot_map
        16.times do |i|
          Wakame.log.debug("describe_snapshot(#{snapshot_map.snapshotId}) ... #{i}")
          volume_from_snapshot_map = vm_manipulator.describe_snapshot(snapshot_map["snapshotId"])
          if volume_from_snapshot_map["status"] == "completed"
            break
          end
          sleep 1.0
        end
        if volume_from_snapshot_map["status"] != "completed"
          raise "#{volume_from_snapshot_map.snapshotId} status is #{volume_from_snapshot_map.status}"
        end
        # △wakame-ebs-mysql-master-make-snapshot.sh ここまで

        # ▽wakame-ebs-mysql-slave-restore.sh(1) ここから
        # attach volume
        attach_volume_map = vm_manipulator.attach_volume(svc.agent.agent_id, created_volume_from_snapshot_map["volumeId"], @ebs_device)
        16.times do |i|
          Wakame.log.debug("describe_volume(#{attach_volume_map.volumeId}) ... #{i}")
          attach_volume_map = vm_manipulator.describe_volume(created_volume_from_snapshot_map["volumeId"])
          if attach_volume_map["status"] == "in-use"
            break
          end
          sleep 1.0
        end
        if attach_volume_map["status"] != "in-use"
          raise "#{attach_volume_map.volumeId} status is #{attach_volume_map.status}"
        end
        # △wakame-ebs-mysql-slave-restore.sh(1) ここまで
      end

      # in wakame-agent
      def start
        # ▽wakame-ebs-mysql-slave-restore.sh(2) ここから
        mount_point_dev=`df "#{@mysqld_datadir}" | awk 'NR==2 {print $1}'`
        if mount_point_dev != @ebs_device
          # sync
          3.times do |i|
            system("/bin/sync")
            sleep 1.0
          end

          Wakame.log.debug("Mounting EBS volume: #{@ebs_device} as #{@mysqld_datadir} (with option: #{@ebs_mount_option})")
          system("/bin/mount -o #{@ebs_mount_option} #{@ebs_device} #{@mysqld_datadir}")
          # sync
          3.times do |i|
            system("/bin/sync")
            sleep 1.0
          end
        end
        system(Wakame.config.root + "/config/init.d/mysql-slave start")
        # △wakame-ebs-mysql-slave-restore.sh(2) ここまで
      end

    end

これで、手順の記述が終わりました。

MySQL_Slaveをサービスに追加

MySQL Slaveの手順を記述するだけではWakameサービス対象には含まれません。Clusterにサービスに追加する必要があります。MySQL_Slaveが起動するよりも前に、MySQL_Masterを起動させる必要があるので、依存関係を指定します。

リスト2 /home/wakame/corelib/lib/wakame/service.rb
    class WebCluster < ServiceCluster
      def initialize(master, &blk)
        super(master) {
        # サービスの定義を追加
        add_service(Apache_WWW.new)
        add_service(Apache_APP.new)
        add_service(Apache_LB.new)
        add_service(MySQL_Master.new)
        add_service(MySQL_Slave.new)

        # 依存関係の指定
        set_dependency(Apache_WWW, Apache_LB)
        set_dependency(Apache_APP, Apache_LB)
        set_dependency(MySQL_Master, Apache_APP)
        set_dependency(MySQL_Master, MySQL_Slave)
      end
    end

いよいよMySQL_Slaveを起動する準備が整いました。

wakameadmから実行してみる

これ以降、MySQL Slaveの追加作業を以下のコマンド1つで行えるようになります。

MySQL Slaveを増やす

以下のコマンドを入力するだけで、しばらく待てばSlaveが増えるはずです。

# wakameadm propagate_service Wakame::Service::MySQL_Slave
MySQL Slaveを移動する

以下のように、移動もできます。単一インスタンスに同居しているSlaveを追い出したりするのに便利です。

# wakameadm migrate_service {Wakame-Instance-ID}

Wakame 0.4のリリース

6月26日(金)Wakameは0.4をリリースしました。大きく変更があった点は以下の通りです。

  • ソースコードが書き直されて整理された
  • Wakame Agentの設計が変更され、機能追加と呼び出しが整理された
  • gem化された

特にソースコードの整理の意義ですが、世の中には、PostgreSQLなど、MySQL以外にも優れたオープンソースがたくさんあります。こうしたデータベースを愛用なさっている方も多いことでしょう。これらを1つ1つ独立して管理するために、ディレクトリ構造を見直し、それに合わせてソースコードを大幅に変更しました。

現在は、そのディレクトリ構造でMySQLはもちろんのこと、PostgreSQLのpgpool-IIを用いてスケールアウトすることも検証中です。原理的には上記の方法で、スクリプトを作って手順の動作確認をした後に、Wakameの上に新たなクラスを追加して実行をするだけです。

なお、この特集にこれまで掲載してきた内容は、今回のデータベースをスケールさせる話以外は基本的に最新である0.4のアーキテクチャをベースとしたものになっております。

今後あくしゅでは、こうしたDBMSの対応を増やしていく他にも、Webサーバやキャッシュサーバなどにも複数標準で対応して行きたいと考えています。誰もがコマンド一発でスケールアウトできる世界を目指していきますので、この辺に興味があってお手伝いをしてくださる方も募集をしております。

おすすめ記事

記事・ニュース一覧