せじまくんの刺さらない話(MySQL Slave増設編)

はじめまして。プラットフォーム開発本部のせじまです。好きなものはDisk I/Oです。
今回はMySQL(on Linux)のレプリケーションにまつわる、ちょっとしたお話をさせていただきたいと思います。

はじめに

MySQL4.0以降のレプリケーションは、

  1. Masterのmysqldが、INSERT/UPDATE/DELETEなどの更新情報を、バイナリログに記録する。
  2. Slaveのmysqld(IOスレッド)は、masterのmysqldに接続し、バイナリログを転送する。
  3. Slaveのmysqld(IOスレッド)は、受信したバイナリログ内容を、リレーログに記録する。
  4. Slaveのmysqld(SQLスレッド)は、リレーログを読み込み、更新内容をslaveのDBに反映する。

といった仕組みになっています。図にすると次の通りです(*1)。

MySQLのレプリケーションはとても良くできた仕組みなのですが、グリーではある問題を抱えていました。稼働中のMasterにSlave を増設するとき、Masterが刺さってしまう(*2)ことがあったのです。問題は、Masterのバイナリログの転送にありました。

Masterを停止させずにSlaveを増設する場合、

  • ファイルシステム等の機能を利用して、既存のDBのスナップショットをとる
  • 既存のSlaveを一時停止して、新しいSlaveに、データをまるまるコピーする

いずれかの方法で増設するSlaveにデータをコピーして、レプリケーションを開始することが一般的だと思います。ただ、スナップショットをとったりデータをコピーしている間もMasterのバイナリログは更新され続けています。更新頻度が高い == バイナリログを大量に吐くMasterに対してSlaveを増設したとき、

  • Masterの古いバイナリログがページキャッシュ上に載っておらず、Slaveに転送する際、Disk Readが多発してしまう。
  • バイナリログの転送で、ネットワークの帯域を食われてしまう。

といった問題が起こりえます。
特に、バイナリログの転送に伴う突発的なDisk Readは、致命的な問題といえます。

基本的に、Diskへのwriteはバッファリングないしスケジューリングの余地があります。しかし、(キャッシュに載ってない)readは、Diskからの応答を待つしかありません。read の IOPS が高くなると MySQL はわりと容易に刺さるので、私はMySQLの負荷を見るとき、write の IOPS よりも read の IOPS を気にすることが多いです。(*3)

どうするか

今回は次のような対処を行いました。

  • max_binlog_size のデフォルト(1GB)は大きすぎるので、小さくする。
    • バイナリログがページキャッシュを何GBも使わないでほしい
    • バイナリログの読み込みは sequential read になるとはいえ、一度に1GBもreadしたくない
  • 増設対象のSlaveで実行するスクリプトを作成。疑似コードは次の通り。

ionice は I/O scheduler が cfq じゃないと効果ないです。

さいごに

つまるところ、

  1. ionice cat ${binlog}
  2. バイナリログをちょっとずつ転送
  3. バイナリログ一個分転送できたら、IOスレッド止めて、転送したバイナリログのクエリを実行
  4. 1. にもどる

を繰り返すだけのお粗末な対応ですが、これがなかなか効き目ありました。
かつては、vmstat等でMasterの負荷を見ながら手動でSTOP SLAVE && START SLAVE を叩いて、少しずつSlave増設していたこともありましたが、いまではピークタイムのSlave増設が危なげなく行えるようになりました。また、一台のMasterに対して、複数台のSlaveを同時に追加したりもできています。

今回のお話が皆様の参考になればさいわいです。
これからもGREE Engineers’ Blogをよろしくおねがいします!

*1: 奥野幹也 氏の『エキスパートのためのMySQL[運用+管理]トラブルシューティングガイド』P.321の図を参考にさせていただきました

*2: ささる(動詞)「コマンドやネットワークが過負荷等の要因で無反応状態になること。」~グリー社内用語集より

*3: 特に、バッテリーバックアップ付きのRAID装置を使っている場合、writeのIOPSは目覚ましい向上を見せるので、readの方が先に性能限界に達してしまう印象があります。