スプーキーズのちょっとTech。

SPOOKIES社内のより技工的な、専門的なブログページです。

【ハンズオンあり】オンラインマイグレーションツール gh-ost を導入した話

最近、一気に寒くなったせいで、

寒暖差疲労なるものに襲われ

調子が悪いアルバイトの本田です😷


それはさておき、

先日のブログご覧いただけたでしょうか?

エイチームさん主催の勉強会

オンラインマイグレーションツール gh-ost について紹介したと思います。

labs.spookies.co.jp

すると、弊社CEOとアンバサダー @masayuki14 より ↓こんなメッセージが!

f:id:spookies_honda:20181218102507p:plain:w400:left










ということで、

今回は、実際にスプーキーズの開発に導入してみた話です。

(ローカル環境で動作確認したところまでのお届け🎅🤶🦌)

gh-ost とは

先日のブログを読んでいない方もおられると思うので(ぜひ読みに行ってください!→先日のブログ

まずは、簡単なgh-ostの紹介↓

  • オンラインマイグレーションツール
  • 正式名称:GitHub's Online Schema Migrations for MySQL
    • 名前から見て分かる通り、GitHub製です
  • OSSとして開発が進む
  • 本番で動いているテーブルのコピー(ゴーストテーブル)を作成し、そっちにmigrateかけて、できあがったら本番のものと入れ替え
  • 本番用のテーブル内のデータが更新されたときは、非同期でゴーストテーブルも更新
    • Slave DBのbinary log を監視することにより実現

ローカル環境で動かしてみた

導入にあたり、まずは、ローカル環境でgh-ostの動作を試してみました。

ここからの作業は

エイチームさんの勉強会で、gh-ostについてお話されていた

s2terminal さんが書かれたQiita記事↓をもとに進めていきます。

qiita.com

  • テスト環境
    • macOS(Ver. 10.13.4)
    • Docker for Mac(Ver. 2.0.0.0-mac78 (28905))
    • gh-ost(Ver. 1.0.47)

1. gh-ostインストール

  1. GitHubからgh-ostをダウンロード
  2. ダウンロードした圧縮ファイルを解凍し、実行ファイルgh-ostを取り出す
  3. gh-ostの実行ファイルをパスが通っているところに移動
  4. 動作チェック
$ gh-ost -version
1.0.47

OK!

2. Docker上でMySQL 環境を構築

  1. Docker公式サイトからDockerをダウンロード(アカウント作成必須)
    • 詳細は割愛
  2. Dockerfile一式をクローン $ git clone https://github.com/s2terminal/mysql-repl.git
    • mysql-slave.cnfの記述を下記のとおり修正
[mysqld]
server-id=2
log-bin
log_slave_updates

3.docker-compose up で起動

3. DB起動確認

以下コマンド実行してMySQLに入れたらOK

$ mysql -u root -h 127.0.0.1 -P 13306

$ mysql -u root -h 127.0.0.1 -P 23306

4. Master/Slave の設定

  1. Master側での作業
$ mysql -u root -h 127.0.0.1 -P 13306 -e 'FLUSH TABLES WITH READ LOCK'
$ mysqldump --all-databases -u root -h 127.0.0.1 -P 13306 --master-data --single-transaction --order-by-primary -r backup.sql
$ MASTER_LOG_FILE=`mysql -u root -h 127.0.0.1 -P 13306 -e 'SHOW MASTER STATUS\G' | grep File | awk '{ print $2 }'`
$ MASTER_LOG_POSITION=`mysql -u root -h 127.0.0.1 -P 13306 -e 'SHOW MASTER STATUS\G' | grep Position | awk '{ print $2 }'`
$ mysql -u root -h 127.0.0.1 -P 13306 -e 'UNLOCK TABLES'

2.Slave側でレプリケーションスタート

$ mysql -u root -h 127.0.0.1 -P 23306 -e 'STOP SLAVE'
$ mysql -u root -h 127.0.0.1 -P 23306 -e 'SOURCE backup.sql'
$ mysql -u root -h 127.0.0.1 -P 23306 -e "CHANGE MASTER TO MASTER_HOST='db-master', MASTER_PORT=3306, MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='${MASTER_LOG_FILE}', MASTER_LOG_POS=${MASTER_LOG_POSITION};"
$ mysql -u root -h 127.0.0.1 -P 23306 -e 'START SLAVE'
$ mysql -u root -h 127.0.0.1 -P 23306 -e 'SHOW SLAVE STATUS\G'
$ mysql -u root -h 127.0.0.1 -P 23306 -e "SET GLOBAL binlog_format = 'ROW'"

これで Master/Slaveの設定は完了です。 実際にMasterにDBを作成して確認してみます。

【Master DBで実行するSQL例】
CREATE DATABASE test_db;
USE test_db;

CREATE TABLE books(
  id   INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  INDEX(id)
);

INSERT into test_db.books (name) VALUES ('Spookies News');
INSERT into test_db.books (name) VALUES ('Story of Spookies');

上記のクエリで適当なDBとテーブル、テストデータをMasterに作成します。

【Master DBで実行】
mysql> select * from books;
+----+-------------------+
| id | name              |
+----+-------------------+
|  1 | Spookies News     |
|  2 | Story of Spookies |
+----+-------------------+
2 rows in set (0.00 sec)

ちゃんとMasterにデータが入っていますね。

次にSlaveを確認します。

【Slave DBで実行】
mysql> select * from books;
+----+-------------------+
| id | name              |
+----+-------------------+
|  1 | Spookies News     |
|  2 | Story of Spookies |
+----+-------------------+
2 rows in set (0.00 sec)

MasterのデータがSlaveにレプリケーションされていますね!

4. 準備は整った。いざ、gh-ost始動!

今回は

Slaveのbooksテーブルに新しいカラムを追加してみます。

gh-ostの動作モードは3つあります。→ 参照

今回は、上記参照のa. Connect to replicac. test-on-replica(テスト)のモードで動かします。

まずは、テストモードで実行します。

テストモードでは、ゴーストテーブルが作成されるだけで、

MasterおよびSlaveの内容が変更されることはありません。

4.1. テストモードで動かしてみる

--test-on-replica がテストモードであることを示します。

$ gh-ost --user="root" --password="" --host="127.0.0.1" --port="23306" --database="test_db" \
  --table="books" \
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" \
  --test-on-replica \
  --gcp \
  --assume-master-host="127.0.0.1:13306" \
  --execute

上記コマンドを実行すると、

_books_ghoテーブルが作られています。

中身を確認すると

【Slaveで実行】
mysql> select * from _books_gho;
+----+-------------------+-------+------------+
| id | name              | price | created_at |
+----+-------------------+-------+------------+
|  1 | Spookies News     |   100 | NULL       |
|  2 | Story of Spookies |   100 | NULL       |
+----+-------------------+-------+------------+
2 rows in set (0.00 sec)

確かに、pricecreated_atカラムが追加されています!

では、次は実際にSlaveに反映→Masterに反映というプロセスをやっていきます。

さきほどお作成したゴーストテーブルは削除しておきましょう。

mysql> DROP TABLE _books_gho;
Query OK, 0 rows affected (0.00 sec)

4.2. 「a. Connect to replica」モードで動かしてみる

1. Migration実行後に自動でMasterに反映するバージョン

gh-ost --user="root" --password="" --host="127.0.0.1" --port="23306" --database="test_db" \
  --table="books" \
  --alter="ADD COLUMN price INT DEFAULT 100, ADD COLUMN created_at DATETIME" \
  --gcp \
  --assume-master-host="127.0.0.1:13306" \
  --execute

上記コマンドを実行後にMaster DBを確認すると…

【Masterで実行】
mysql> show tables;
+-------------------+
| Tables_in_test_db |
+-------------------+
| _books_del        |
| books             |
+-------------------+
2 rows in set (0.00 sec)

mysql> select * from books;
+----+-------------------+-------+------------+
| id | name              | price | created_at |
+----+-------------------+-------+------------+
|  1 | Spookies News     |   100 | NULL       |
|  2 | Story of Spookies |   100 | NULL       |
+----+-------------------+-------+------------+
2 rows in set (0.00 sec)

できてますね!

先述しておりますが、

gh-ostでは、Masterテーブルの複製(ゴーストテーブル)を作成し、

そのゴーストテーブルに対してMigrationを行います。

そして、Migrationが完了次第、

本番稼働してるテーブルと名称を入れ替える形で切り替えを行います。

したがって、_books_delテーブルは置き換える前に本番で動いていたテーブルです。


2. Migration実行後に手動でMasterに反映するバージョン

Masterに先程作成されたテーブルは消しておきましょう。

【Master DBで実行】
mysql> drop tables _books_del;
Query OK, 0 rows affected (0.01 sec)

まずは、手動切り替える用のフラグファイルを作成します。

$ touch /tmp/ghost.postpone.flag

(↑gh-ostコマンドを実行するPC上での実行)

それから、

--postpone-cut-over-flag-fileオプションにより

切り替え用フラグファイルを指定。

$ gh-ost --user="root" --password="" --host="127.0.0.1" --port="23306" --database="test_db" \
  --table="books" \
  --alter="ADD COLUMN updated_at DATETIME" \
  --gcp \
  --assume-master-host="127.0.0.1:13306" \
  --postpone-cut-over-flag-file=/tmp/ghost.postpone.flag \
  --execute

上記コマンドを実行すると、

2018/12/18 12:09:19 binlogsyncer.go:79: [info] create BinlogSyncer with config {99999 mysql 127.0.0.1 23306 root   false false <nil>}
2018/12/18 12:09:19 binlogsyncer.go:246: [info] begin to sync binlog from position (9ef9788f80f5-bin.000004, 3333278)
2018/12/18 12:09:19 binlogsyncer.go:139: [info] register slave for master server 127.0.0.1:23306
2018/12/18 12:09:19 binlogsyncer.go:573: [info] rotate to (9ef9788f80f5-bin.000004, 3333278)

このようなログが出力され続けると思います。これでOKです!

Migrationが終わり、いつでも切り替えられる状況です。

この状況のままで Master を確認します。

【Masterで実行】
mysql> show tables;
+-------------------+
| Tables_in_test_db |
+-------------------+
| _books_ghc        |
| _books_gho        |
| books             |
+-------------------+
3 rows in set (0.01 sec)

mysql> describe books;
+-------+-------------+------+-----+---------+----------------+
| Field | Type        | Null | Key | Default | Extra          |
+-------+-------------+------+-----+---------+----------------+
| id    | int(11)     | NO   | PRI | NULL    | auto_increment |
| name  | varchar(20) | YES  |     | NULL    |                |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

mysql> describe _books_gho;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| name       | varchar(20) | YES  |     | NULL    |                |
| price      | int(11)     | YES  |     | 100     |                |
| created_at | datetime    | YES  |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

確かに、booksテーブルは変化なしですが、

_books_ghoテーブル(ゴーストテーブル)にはカラムが追加されている!

では、実際に本番DBに反映します。


本番DBに反映するための切り替え作業はフラグファイルを消してあげるだけです。

$ mv /tmp/ghost.postpone.flag /tmp/ghost.postpone.flag.unpostpone

(↑gh-ostコマンドを実行するPC上での実行)

実行すると、

ログ出力が止まります。

では、Master DBを見てみましょう。

【Masterで実行】
mysql> show tables;
+-------------------+
| Tables_in_test_db |
+-------------------+
| _books_ghc        |
| _books_gho        |
| books             |
+-------------------+
3 rows in set (0.00 sec)

mysql> select * from books;
+----+-------------------+-------+------------+------------+
| id | name              | price | created_at | updated_at |
+----+-------------------+-------+------------+------------+
|  1 | Spookies News     |   100 | NULL       | NULL       |
|  2 | Story of Spookies |   100 | NULL       | NULL       |
+----+-------------------+-------+------------+------------+
2 rows in set (0.00 sec)

反映できてますね!

すごい!


ちなみに、

手動切替を待つ状態で

Masterのデータに変更があった場合を試してみましょう。

切り替え用フラグをまた作成し、

$ touch /tmp/ghost.postpone.flag

下記コマンド実行

$ gh-ost --user="root" --password="" --host="127.0.0.1" --port="23306" --database="test_db" \
  --table="books" \
  --alter="ADD COLUMN favorite B DEFAULT 0, ADD COLUMN created_at DATETIME" \
  --gcp \
  --assume-master-host="127.0.0.1:13306" \
  --execute

また、ログが出力され続ける状態になったと思います。

では、この状態でMaster DBの方にデータを追加してみましょう。

【Masterで実行】
mysql> insert into books (name, price) values ('Spo Paper', 200);
Query OK, 1 row affected (0.01 sec)

mysql> select * from books;                                                              
+----+-------------------+-------+------------+------------+
| id | name              | price | created_at | updated_at |
+----+-------------------+-------+------------+------------+
|  1 | Spookies News     |   100 | NULL       | NULL       |
|  2 | Story of Spookies |   100 | NULL       | NULL       |
|  3 | Spo Paper         |   200 | NULL       | NULL       |
+----+-------------------+-------+------------+------------+
3 rows in set (0.00 sec)

この変更はもちろんSlaveに反映されています。

【Slaveで実行】
mysql> select * from books;
+----+-------------------+-------+------------+------------+
| id | name              | price | created_at | updated_at |
+----+-------------------+-------+------------+------------+
|  1 | Spookies News     |   100 | NULL       | NULL       |
|  2 | Story of Spookies |   100 | NULL       | NULL       |
|  3 | Spo Paper         |   200 | NULL       | NULL       |
+----+-------------------+-------+------------+------------+
3 rows in set (0.00 sec)

問題はMasterに作成されいるゴーストテーブルにも反映されているのか否か…。

では、手動切替を行った後にMasterを確認した結果がこれです↓

【Masterで実行】
mysql> select * from books;
+----+-------------------+-------+------------+------------+----------+
| id | name              | price | created_at | updated_at | favorite |
+----+-------------------+-------+------------+------------+----------+
|  1 | Spookies News     |   100 | NULL       | NULL       |        0 |
|  2 | Story of Spookies |   100 | NULL       | NULL       |        0 |
|  3 | Spo Paper         |   200 | NULL       | NULL       |        0 |
+----+-------------------+-------+------------+------------+----------+
3 rows in set (0.00 sec)

反映されています!

こちらも先述したとおり、

gh-ostではSlaveのバイナリログを見て、

非同期でデータを同期しています。

したがって、常に本番で動作しているMaster DBとの同期が取れています

最後に

このようにオンラインマイグレーションツール gh-ostを使用することで

サービスを止めることなくマイグレーションが

安全(サービス的にも肉体的にも精神的にも!)に行えることができます!

Spookiesでも早速導入しました。

みなさんも機会があればぜひ使ってみてください👩‍💻👨‍💻