MySQLを1〜2時間でスケールアウトする

最近、Elastic BeanstalkやECSと戦っているSREチームの菅原です。 P5をやりたいのにPS3もPS4も持っていないので指をくわえて羨ましがっている毎日です。

この記事では、突然のアクセス増に備えるために、MySQLのスレーブを1〜2時間でスケールアウトできるようにした話を書きます。

MySQL on EC2

クックパッドは周知の通りAWSを利用していますが、主要なデーターベースについてはAmazon RDSではなくMySQL on EC2を使っています。 これは以下のような理由によるものです。

  • 歴史的な経緯: AWS移行当時、RDSが無かった。また、移行後もしばらくはTritonnを使っていたため、RDSを使うことができなかった
  • オンラインメンテナンスの実現: VPCルートテーブルを使った仮想IPとMHA for MySQLを使ってダウンタイムゼロのマスタDBの切り替えを実現しています。 RDSによるDNSベースの切り替えでは、どうしてもダウンタイムが発生してしまいます。
  • 複雑なレプリケーション構成: 主要DBは内外様々なサービスが利用しているため、レプリケーションの構成が複雑になっています。あるスレーブでは特定のテーブルをレプリケーションしていなかったり、またあるスレーブでは別のDBと同居していたりなど。RDSでこのような複雑な構成に対応することは難しいです

スケールアウトと暖機

TV放映など突発的なアクセス増があった場合、DBの負荷も増大するためスケールアウトが必要になることがあります。クックパッドの場合、サービスの特性としてリードのアクセスが圧倒的に多いため、DBをスケールアウトする場合には主にMySQLのスレーブを増やして、サービスに追加することになります。

DBのデータはインスタンスにアタッチされているEBSのスナップショットとして、定期的にバックアップが取られています。新規にスレーブを作成する場合は以下のような手順になります。

  1. MySQL on EC2のインスタンスを立てる
  2. スナップショットからEBSを復元してインスタンスにアタッチ
  3. レプリケーションが追いつくのを待つ

これで新しいスレーブができました。「早速サービスに入れよう」…とはいきません。 作ったばかりのMySQLはデータがメモリにキャッシュされていないため、クエリが投げられるとディスクへの読み書きが発生し、処理に時間がかかってしまいます。 またスナップショットから復元されたEBSは、最初にブロックにアクセスしたときにはS3からデータをダウンロードしてくるため、その後のアクセスよりもレイテンシが増加します。

このように暖機の行われていないスレーブをサービスに投入すると、サービスの応答速度の低下を招き、障害にもつながります。

EBSの暖機

gp2/1000GBのEBSをfioで暖機してみたところ、約19時間ほどかかりました。

さすがに時間がかかりすぎるので「サービスに即時投入できるようにレプリケーションし続ける小さいインスタンスを用意しておく」とか「バックアップ用のスレーブのEBSを使って新しいスレーブを作る」などいくつか対策を考えてみたのですが、同じ部の先輩が「I2インスタンスを使うとよいのでは?」とアドバイスをくれました。

I2インスタンスは大容量で高速なインスタンスストアが使えるインスタンスタイプです。 DBで使っているファイルの総容量はEBSのサイズよりも小さいので、EBSからインスタンスストにコピーする方がEBS全体を暖機するより処理時間は速くなります。 また、インスタンスストアはインスタンスに物理的にアタッチされるボリュームなので、暖機などしなくても高速に使えます。

なるほどと思って、総容量300〜400GBのDBのファイル群をcpを使って8並列でコピーしてみたところ、だいたい3時間ほどかかりました。EBSを暖機するよりはずっと短くなったのですが、それでもまだ時間がかかります。 並列でcpを走らせるとある程度はスループットが出るのですが、ファイルごとの処理は直列なためサイズの大きいファイルがあるとそれに引きずられてスループットが下がってしまいます。

そこで一つのファイルをチャンクに分けてddでコピーするツールを作り、それを使ってコピーしてみたところ、3時間かかっていた処理を1時間程度まで短縮することができました。

MySQLの暖機

MySQLのデータをメモリに乗せる作業は、以前はMySQL::WarmerをRubyにポートした自作ツールを作って、手作業で行っていました。

サーバ上でメモリ使用量を見ながらウォームアップツールで主要なテーブルの暖機を行い、キャッシュが飽和したらサービスに少し入れてみて、スロークエリの出たテーブルをまた暖機…と悪い意味で職人的な作業であり、大量のMySQLに対して行うには非常に手間がかかりました。

そこでMySQL 5.6のInnoDBバッファープールのプリロード機能を使って、暖機作業を高速化しました。 InnoDBバッファープールのプリロード機能は、稼働しているMySQLのバッファプールの状態をファイルに出力しそれを起動時に読み込むことで、暖機の手間を省いてくれるものです。 基本的には同じサーバ上のMySQLでの利用が想定されていると思うのですが、今回は稼働中のスレーブのバッファプールをダンプしてそれを新しく作ったスレーブに読み込ませることで、暖機作業を機械的に行えるようにしました。

具体的には以下のような手順になります。

  1. 稼働中のスレーブの1台で定期的にバッファプールのダンプを行うcronを設定する
  2. ダンプファイルは圧縮してS3に保存しておく
  3. 新規に作成したスレーブはS3から最新のダンプファイルをダウンロードして、起動時に読み込む

ディスク上のデータの物理的な配置が完全に一致しているかはやや疑問でもあるのですが、手作業よりも圧倒的に手間が少なく、また十分な効果も得られているのでこの方法をとっています。

スケールアウトの手順

現在、MySQLのスケールアウトが必要な場合、以下のような手順で行っています。

  1. Kumogataを使ったCloudFormationのテンプレートを準備しておく
  2. 環境変数でサーバ台数を指定できるようにしておき、CloudFormationで必要な台数のインスタンスを起動する
  3. CloudFormationによって、起動したインスタンスには最新のバックアップから作成されたEBSがアタッチされる
  4. インスタンスが起動するとcloud-initの起動時スクリプトによって以下の作業が行われる
    • MySQLのセットアップ
    • EBSからインスタンスストアへのデータのコピー
    • S3からバッファプールのダンプファイルをダウンロード
  5. MySQLが起動してレプリケーションの再開とバッファプールのロードを行う
  6. レプリケーションが追いついてバッファプールのロードが終わると、Slackに通知が来る
  7. 新しいスレーブを人間がサービスに追加する

まとめ

この仕組みを導入することで、MySQLのスケールアウトのために「TV放映の前日、前々日から準備を始めて」「19時間ちかくを暖機に費やして」「職人芸でサービスに追加」していた作業が、「TV放映の当日、1〜2時間前にコマンドをたたいて」「Slackに通知が来るまで放置して」「通知来たらおもむろにサービスイン」といった具合に、大幅に省力化・短時間化できました。

オペレーション作業に特有の「長い時間かかって手持ちぶさたな割に目を離すことができないから他の作業がしにくい時間」って、ほんと嫌ですよね… 今後もそんな作業があればさっさと自動化して、QoLの向上を図っていきたいものです。