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の向上を図っていきたいものです。

Amazon Redshiftへ継続的にデータをロードする際に気をつけること

こんにちは、インフラ部データ基盤グループの小玉です。

データ基盤グループでは、Amazon Redshift(以下、Redshift)へ継続的にデータをロードする仕組みを、約半年に渡り構築・運用してきました。この記事では、その中で学んだことを共有させて頂きます。

弊社では情報系システムの一部に、AWSが提供するRedshiftという分散データベースを利用しています。情報系システムとは、データ分析を主な用途とするシステムのことです。なかでもRedshiftはSQLを使った大量データの高速な分析に最適化されているため、DWH(データウェアハウス)としての利用に適しています。

DWHの構築に必要なタスクとしては、データソースの特定、モデリング、データの抽出・変換・ロード(ETL)、クエリツールやBIツール導入、パフォーマンス・チューニング、メタデータの管理、バックアップ・リストアなど、がありますが、今回は「データの抽出・変換・ロード(ETL)」に関する話になります。

DWHへの一般的なデータロード

DWHへのデータのロードは、日ごと、週ごと、月ごとなどの決まったタイミングで、DWHユーザの分析クエリ(以下、ユーザクエリ)の流れない深夜から早朝に、バッチ処理でドカっと行うことが多いです。例えば、毎日早朝に最新のユーザマスタをロードしたり、月初に前月分の売上データをロードする、といった具合です。

このような処理が一般的である理由としては、「日中はユーザクエリのためにリソースを空けたい」、「業務が月末締めなのでそれに合わせたい」、「DWH用データベースは細かいINSERTが苦手なことが多い」といったものが挙げられます。

とはいえ、スピード命のこの業界では、「今朝デプロイした機能のログを午後には見たい」、「10分前のログを元に分析を実施し、その結果を本番システムに反映したい」といった要望も珍しくありません。

Redshiftへ継続的にデータをロードする仕組み

そこで弊社では、一部のログデータ(約1万レコード/秒)について、数分から数時間間隔でRedshiftへ継続的にロードする仕組みを構築し、運用を始めています。その仕組はさっくり以下の通りです。

  • アプリケーション(ウェブサーバ等)がFluentdへログを送る
  • FluendがS3にログをまとめて書き込む
  • 独自ロードシステムが...
    • S3上のログを読み込み、クレンジングや変換を行い、再びS3へ書き込む
    • S3上のクレンジング済ログをRedshiftへロード(COPY)する

このような仕組みを構築、運用する上で気をつけるべきこと(苦労したこと)の一つは、「ロード処理のリソース消費を最小限に抑えること」です。

なぜロード処理のリソース消費を最小限に抑える必要があるのか?

継続的にデータをロードするということは、日中、ユーザクエリが実行されている最中にデータのロードが行われるということです。この場合、ユーザクエリとロード処理は、CPUやI/Oなどのリソースを分け合って実行されることになります。よって、ユーザクエリへの影響を出来るだけ小さくするために、ロード処理のリソース消費は最小限に抑えるべきです。

それを踏まえ、我々は以下のような方針で上記のロードシステムを構築・運用しています。

  • ELTではなくETLを選択する
  • ロードシステムのバックエンドDBは分ける
  • 基本的なロードの最適化を怠らない

ELTではなくETLを選択する

DWHへデータをロードする工程は、一般的にETLと呼ばれています。ETLとは、ソースシステムからデータを抽出し(Extract)、加工・変換をした上で(Transform)・ロードする(Load)する処理のことです。ただし、最近ではELT、つまりデータベースへロードした後に加工・変換をする流れも多く見られるようになってきており、ETLとELTのどちらを選択するかはDWH構築におけるデザイン・チョイスの一つになっています。

見出しにもある通り、Redshiftへ継続的にデータをロードする場合は、ELTではなくETL、つまりロード前に加工・変換処理を実施する方式を検討するべきです。なぜならETLの場合は、加工・変換処理のためのINSERT/SELECTをRedshift上で実行する必要が無く、リソース消費を抑えることが出来るからです。

弊社では独自システムを使い、Redshiftの外でデータの加工・変換を済ませた後で、ロードしています。このような仕組みを構築することで、ロード処理に関わるリソース消費を最小限に抑えることができました。また、このレイヤーを導入することで、SQLでは難しい加工・変換処理を行うこともできるようになりました。

ロードシステムのバックエンドDBは分ける

読み書きの多いウェブアプリケーションのバックエンドDBとしてRedshiftを使う方は居ないと思います。しかし、上記の独自ロードシステムのような比較的小さなシステムであれば、そのバックエンドDBとしてRedshiftを使っても良いと思うかもしれません。

実は、我々がそうでしたが、今はRDS上のPostgresSQLに移行しています。移行の理由は、ロードシステムからの書き込みの負荷が、無視できないレベルだったためです。

Redshiftを含むDWH用途の分散データベースは、MySQLやPostgresSQLとは異なったデータアクセスパスを持ち、またロックやトランザクションも分散システム用のアルゴリズムで実装されています。そのため、ロードシステムに限らず、アプリケーションのバックエンドDBとして利用する場合は、その仕組みを十分に理解しておく必要があります。

基本的なロードの最適化を怠らない

上記の独自ロードシステムには、まだ実装出来ていない部分もありますが、公式ドキュメントに記載されているような、基本的なロードの最適化も忘れずに行う必要があります。なかでも重要なのは「COPYの対象ファイル数を、Redshiftのスライス数の倍数にすること」です。

スライスとはRedshiftのノード上で処理の実行を担うプロセスのことで、専有のディスク、CPU、メモリを割り当てられています。ロードを含む多くの処理はスライス単位で並列実行されるため、Redshiftの能力を最大限引き出すには、全てのスライスに均等に負荷をかけることがとても重要です。

例えば、システム全体で4スライスある場合、500MBのファイルを2つロードするより、250MBのファイルを4つロードするほうが高速です。なぜなら、全てのスライスがロード処理を実行することになり、リソースを無駄なく活用出来るからです。なおこの場合、ファイルを分割するだけでなく、そのサイズを出来るだけ揃えることも大切です。

まとめ

以上、この記事では、Redshiftに継続的にデータをロードする際に気をつけるべきこととして、「ロード処理のリソース消費を最小限に抑えること」を挙げ、弊社での取組みについて紹介をさせて頂きました。

また、クックパッドでは、一緒にデータ基盤を作っていただけるエンジニアを募集しています。ご興味のある方は是非遊びにいらしてください。

iOSアプリの継続的デリバリーへの取り組みについての勉強会を開催しました

f:id:gigi-net:20160913200616j:plain

技術部モバイル基盤グループの三木(@)です。

去る9月13日、「Cookpad Tech Kitchen iOSアプリの継続的デリバリーへの取り組み」と題して、iOSエンジニア向けの技術交流イベントを行いました。

このイベントでは、iOS開発の中でも特に大規模アプリの開発フローや、品質改善を支えるための技術をテーマに、弊社のエンジニアから3つの発表をしました。

この記事では、その様子についてお伝えします。

クックパッドiOSアプリの品質管理のための取り組み

まず、技術部品質向上グループの松尾(@)から、クックパッドiOSアプリ開発体制について紹介しました。

この発表では

  • どのような組織構造を元にアプリ開発が行われているか
  • 月1のリリースサイクルを実現するためのリリースフロー
  • テストサイズとテスト戦略
  • UIテストを中心とした自動テスト手法

などについての概略を紹介しました。

サイズごとのテストについては以下の記事でも解説しています。

CDC Testing on iOS

次に、私、三木より、Consumer-Driven Contract Testing(CDC Testing)と呼ばれる、クライアントとサーバーサイドアプリケーション間の関係性に着目したテスト手法をiOSアプリに組み込んだ知見について発表しました。

CDC Testingは、クライアントからのAPIサーバーへのリクエストを記録し、継続的に検査するテスト手法です。 これにより、APIの変更による連携の崩れを検出することができます。

サーバーサイドアプリケーション間のCDC Testingについて、当技術ブログにいくつか記事がありますので、ご興味のある方はあわせてご覧ください。

また、今回紹介した、iOSのユニットテスト内でPactを用いてAPI仕様を作成するためのライブラリ、Phakchiも公開しています。

クックパッドiOSアプリの開発フロー

最後に、投稿開発部森川(@)から、サービス開発に携わるエンジニアの目線から、 開発フローの中でどのように品質を改善していくかという手法について発表しました。

コードレビューやKPT会など、開発速度を上げるために行っている工夫についていくつか紹介しています。

また、今回の発表でお伝えしきれなかった内容について、以下の記事もあわせてご覧ください

開発速度を上げるための Pull-Request のつくり方 - クックパッド開発者ブログ

懇親会

上記の発表の後、懇親会を行いました。

懇親会では、他の弊社iOSアプリエンジニアも参加しながら、参加者と様々な意見交換や談話をしました。

多くの皆様にご参加いただけたおかげで、より深掘りした話をしたり、各社のiOS開発の知見を共有するなど、活発な情報交換が行えたと思います。

また、今回はAppleになぞらえ、リンゴをテーマにした料理も振る舞われました

f:id:gigi-net:20160913202255j:plain

まとめ

いかがでしたでしょうか。

クックパッドでは、今後もiOS開発をテーマに同様のイベントを行っていきたいと考えています。 次回は11月頃の開催を予定しています。詳細が決まりましたら、当ブログなどでお知らせします。

また、クックパッドでは、一緒にiOSアプリ開発を行っていくエンジニアを募集しています。ご興味のある方は是非遊びにいらしてください。