トレタ開発者ブログ

飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。

EngineyardからAWSに移設してAuroraの運用を開始した

どうも佐野です。トレタのインフラはEngineyardワオワオやってたんですが、あの、なんていうのAurora?それを使いたかったわけです。 さて、私が書いた前々回の記事にて、「コア機能のAWS化」を今後のTODOとして挙げていましたが、5/9にEngineyardからAWSへの移設が完了していました。EngineyardはRailsやnodejsなどのWEB-DB環境を簡単に構築できるPaaSです。Herokuとの違いはサーバにsshしたり、chefを利用してFluentdなどのミドルウェアを入れたり、既存のコンフィグレーションをカスタマイズしたりすることができる点が挙げられます。比較的自由度の高いPaaSと言えるでしょう。トレタでは創業〜先月までお世話になりました。 今回は移設とAuroraの運用に関するTipsの紹介になります。なお、本記事に示す設計・運用方針はAWSのソリューションアーキテクトの支援を得て決定したものではなく、私の見解で決定したものなのでご了承ください。ベストプラクティスではないかもしれません。

以下、目次になります。

  1. 移設
  2. Auroraのコンフィグレーション
  3. 基本、マスターのみを使う
  4. スロークエリの検出
  5. フェイルオーバするとき
  6. スケールアップするとき
  7. Auroraのメンテナンスをするとき
  8. リストアするとき
  9. MySQL on EC2からレプリケーションを行う場合
  10. 失敗したこと

1. 移設

運用エンジニアを長らくやっていると移設は慣れっこなのですが、弊社エンジニアに移設経験者が私しかいませんでした。MTGで「なるほどー」という空気になったので書いてみます。システムの移設をやったことがない、これからやる、という方がいらっしゃいましたら参考にしてみてください。 移設は往々にしてデータをどのようにして新環境に移すか?というのが技術的な課題になります。トレタの場合、MySQL, Redisといったレプリケーション可能なよく知られたデータストアを使っていたので、技術的な難しさはほとんどなく、どちらかというと社外を含む関係各所へのメンテナンスの日程の調整の方が骨が折れました。このあたりはセールスチームなどの協力を得て今回はスムースに調整を進めることができました。 技術的な移設手順は下記の通り。

1.1. これを

Engineyardで稼働している状態です。AWSに新環境を作っておきます。

f:id:hiroakis:20160616113808p:plain

1.2.こうして

データストアは新環境からレプリケーションを張っておきます。Engineyardはsshポートがグローバルに開放されているので、sshトンネルを張ってトンネル経由でレプリケーションを行いました(Engineyardのサポートに連絡すれば3306ポートをIP制限掛けてグローバルに開放してくれたかもしれない)。またsshプロセスはmonitで監視し、もし落ちた際は自動再起動するようにしておきます。Redisについても同様、sshトンネル経由で6379ポートに接続してデータの同期を行います。 Auroraから外部のMySQLにレプリケーションを行う場合(Auroraがスレーブ、MySQLがマスタ)は、 call mysql.rds_set_external_master のようなストアドで行います。このあたりはRDSのマニュアルを参照してください。で、今回はEngineyardのMySQLにsshトンネル経由でレプリケーションを行う必要があるため、AWS上に踏み台のMySQLを用意し、カスケード接続でAuroraにデータを同期させます。図の通り、Engineyard MySQL <- (sshトンネル) <- EC2 MySQL <-(ストアド利用) <- Auroraというレプリケーションチェーンになります。

f:id:hiroakis:20160616113827p:plain

1.3. こうやって

これはメンテナンス時間中に行います。まずシステムをメンテナンス状態にします。そしてデータストアのレプリケーション遅延がゼロになっていることを確認して、ウェブアプリケーションのデータストアの接続先が新環境に向くようにします。終わったらメンテナンスを解除します。トレタではメンテナンス時間は1時間設けました(実作業時間は確認を含めて30分程度でした)。 蛇足ですが、Auroraなどのデータストアはパブリックアクセス可能な状態にし、旧環境であるEngineyardのAppサーバからのアクセスを許可しておく必要があります。

f:id:hiroakis:20160616113848p:plain

1.4. こう

あとは日中帯に落ち着いて新環境にデプロイを行い、DNSを新環境に向けて終わりです。

f:id:hiroakis:20160616113909p:plain

2. Auroraのコンフィグレーション

長らくMySQL運用おじさんをやっていた身としてはやはりコンフィグが気になります。私が気にした箇所についていくつか抜粋します。総括すると、大抵のものはデフォルトで良さそうな印象を受けました。

クラスタパラメータグループ

* 文字コード
character_set_client = utf8
character_set_server = utf8
character_set_connection = utf8
character_set_database = utf8
character_set_results  = utf8
skip-character-set-client-handshake = 1
* バイナリログ
binlog_format = MIXED
log_slave_updates = 1(デフォルト)
sync_bin_log = 1(デフォルト)
* InnoDB
innodb_commit_concurrency = 0(デフォルト)
innodb_purge_threads = 1(デフォルト)
innodb_file_per_table = 1(デフォルト)
innodb_flush_log_at_trx_commit = 1(デフォルト)
innodb_support_xa = ON(デフォルト)

文字コード周りとバイナリログの有効化を設定してあります。移設の段階でカスケード接続させるのでlog_slave_updatesは1にします。sync_bin_logは1で良いでしょう。またバイナリログの保持期間を設定する expire_logs_days は無く、 call mysql.rds_set_configuration('binlog retention hours', 24); などとしてストアドでバイナリログの保持期間を設定する必要があります。InnoDB関連でいくつか気になるものがありますが、デフォルトで納得できる値になっていました。

DBパラメータグループ

* 接続周り
max_connections = 自動計算(デフォルト)
thread_cache_size = 自動計算(デフォルト)
max_allowed_packet = 134217728(128M)
* スローログ
slow_query_log = 1
long_query_time = 2
log_queries_not_using_indexes = 1
log_output = TABLE(デフォルト)
* 各種バッファ
tmp_table_size = 67108864(64M)
max_heap_table_size = 67108864(64M)
sort_buffer_size = 8388608(8M)
join_buffer_size = 131072(128K)
innodb_sort_buffer_size = 1M(デフォルト)
innodb_log_buffer_size = 8M(デフォルト)
* InnoDB
innodb_buffer_pool_size = 自動計算(デフォルト)
innodb_flush_method = O_DIRECT(デフォルト)
innodb_buffer_pool_dump_at_shutdown = 1(デフォルト)
innodb_buffer_pool_load_at_startup = 1(デフォルト)
* 分離レベル
tx_isolation = REPEATABLE-READ(デフォルト)
* クエリキャッシュ
query_cache_size = 自動計算(デフォルト)
query_cache_type = 1(デフォルト)

接続周りの設定、スローログ、各種バッファ、InnoDB関連、トランザクション分離レベル、クエリキャッシュあたりをケアしました。

  • 接続周り

thread_cache_size、max_connectionsが自動計算されるようになっていて、計算後の値も適切だったのでAuroraの自動計算に任せました。max_allowed_packetは自分のMySQLの秘伝のタレの値をそのまま転用。

  • スローログ

スローログについては閾値を2秒とします。RDSの場合、log_outputはTABLEとなり、これはmysql.slow_logにログが蓄積されることを意味します。

  • 各種バッファ

バッファの数値はちょいちょいいじりました。とはいえこれらの数値の根拠はなくて、私の経験的にデフォルトではちょっと小さいのではないか?と思ってこのような数値にしてあります。

  • InnoDB

クラスタパラメータグループだけでなくDBパラメータグループにもInnoDB周りの設定があります。性能面において最も重要な設定の一つであるinnodb_buffer_pool_sizeはこちらにあり、これもAuroraによって自動計算されるようになっています。innodb_flush_methodもO_DIRECTになっており、これらは適切な値だったのでとくにいじりませんでした。

  • トランザクション分離レベル

確認したところ、tx_isolation = REPEATABLE-READがデフォルトで設定されていたので特に変更せず。

  • クエリキャッシュ

デフォルトで有効になっており、キャッシュサイズは自動計算されていました。実はこれが個人的にちょっと悩ましくて、私の経験上、過去にクエリキャッシュが効果を発揮したことはあまりなかったです(根拠のあるデータを出すことができずすみませんが...)。インデックスが効いていれば十分高速ですし。無効にしたいところだが、Auroraの推奨?と判断して特に変更せずこのままとしました。

3. 基本、マスターのみを使う

現状のトレタのトラフィックでは1DBで十分です。1DBでの運用を行います。(スレーブは深夜のバッチ処理で使うようにしてありますが)基本的にスレーブは昇格用のスタンバイです。

4. スロークエリの検出

上の方に書いた通り log_output = TABLE のためスロークエリは mysql.slow_log テーブルに出力されるようになります。監視用に立てたインスタンスから次のようなクエリを定期的に投げて、スロークエリを検出しています。

select * from mysql.slow_log where start_time > '%s' order by start_time

トレタでは次の様にしてSQL警察がslackに通知するようにしています。

f:id:hiroakis:20160614013936p:plain

5. フェイルオーバするとき

フェイルオーバーボタンをポチります。数十秒〜1分程度です。

6. スケールアップするとき

インスタンスサイズの大きいreader*1を優先度tier-0(優先度-0)*2で作成し、フェイルオーバーを行います。

7. Auroraのメンテナンスをするとき

Auroraも他のRDSがサポートしているエンジン同様、Aurora自体のメンテナンス(OSアップグレード、バージョンアップetc)が必要なケースがあります。トレタではAuroraのメンテナンスはフェイルオーバーで行う方針です。つまり先にスレーブをupgradeしてそれをマスタに昇格させます。このリンクのようにAuroraのメンテナンス時はフェイルオーバーじゃなくて再起動の方が早い、つまり無下に「upgrade now」を実行してもよいと言及されている場合もありますが、私が実験した結果では、upgrade nowの停止時間は短い時は短いが遅長い時もある、フェイルオーバは安定して1分程度の停止時間、という結果になったので停止時間が安定している方を採択する方針です。upgrade nowの停止時間はAuroraのメンテナンスの内容、インスタンスサイズ、実施時の負荷などに依存するのかもしれません。

  • db.r3.largeをupgrade now: 16秒停止
  • db.r3.largeをフェイルオーバ: 60秒停止
  • db.r3.xlargeをupgrade now: 1分59秒停止
  • db.r3.xlargeをフェイルオーバ: 55秒停止

なお、ここでいう停止時間とは、インターネット経由でシステムのエンドポイントを叩き、オフラインになった時間を計測しています。なのでAurora自体の停止時間に加えて、ヘルスチェックなどで切り離されてしまった時間から復活する時間も含まれています。

8. リストアするとき

任意の時刻でのpoint-in-timeリカバリが可能です。もし時刻ではなく、たとえばオペミスでdrop tableしちゃってdrop tableの直前まで戻したい場合などは、バイナリログを活用することになるでしょう。

9. MySQL on EC2からレプリケーションを行う場合

AuroraをマスタとしてEC2など外部のMySQLをスレーブにするパターンです。移設のときと逆ですね。こちらについてはいつもどおりAuroraにレプリ用のユーザを作成して 外部のMySQLから change master すればよいです。

10. 失敗したこと

リザーブドインスタンスにし忘れた...:(;゙゚'ω゚'): うーむ...。RDSってあとからリザーブドにできなかったよね...??? (追記)各所よりあとからリザーブにできると教えていただきました

おわり

*1:readerというのはいわゆるスレーブ。Aurora用語かな?マスタはwriterと呼ぶ。

*2:昇格の優先度です。後ろの数字が若いものが優先的に昇格されます。

Shoryukenでつくるバッチ処理基盤

トレタのAPI開発を担当している芹沢です。

トレタでは、長時間かかるバッチ処理を複数台のサーバ上で処理させて短時間で処理できるバッチ処理基盤をAWS上で構築しました。この仕組みについて説明します。

目的

短期的には以下の課題を解決するため、長期的には似たような要件が再度発生した時に、同じ手法で解決できることを目的に作りました。

  • 非同期でDBをデータソースとしたデータを加工してCSVファイルとして出力してS3にputしたい
    • データソースはDBに入っているリアルタイムのデータであることが求められる
    • CSVファイルの作成は決められた時間内に完了する必要がある
    • 対象となるデータソースの量は日々増加し続けるが、常に決められた時間内にCSV作成が完了している必要がある

難点

今回の要件で技術的に難しい点は以下の2点です。

DBを直接参照しながら大量のデータを処理する

例えば、データソースとしてDBからHDFSやRedshiftに同期されたデータを使えるのならば、AWS EMRなどのサービスを活用することで大量のデータを処理することは比較的容易です。しかし、今回の要件は、更新頻度が高いデータソースをできるだけリアルタイムに近い状態で取得してCSVを作成する必要がありました。

一定のスループットを担保し続ける

今回の要件上、1回のCSV処理作成にかかる時間を15分以内に抑える必要がありました。inputとなるデータ量が常に一定であれば一度15分以内に終わるように構築すれば、あとはそれを延々と動かし続ければ良いのですが、データ量が増加し続けるという事情があるため、データ量が増えても一定のスループットを担保し続ける仕組みを用意する必要がありました。

バッチ処理専用の環境を作ることにしました

これらの課題を解決するために以下の様な基盤を構築し、その上にデータ処理用のバッチを実装しました。

f:id:serihiro:20160608182432p:plain

処理の流れは図の通りです。

バッチの実装について

いわゆるJob Queueですが、以下2つのJobが介在します。前者はSidekiq、後者はShoryukenを使って実装されています。

IDをグルーピングしてCSV作成Jobを作るJob

  1. sidekiq-cronによって15分に1回起動します
  2. 処理対象のレコードのIDを取得し、100IDずつにグルーピングします
  3. グルーピングしたIDを引数としたCSV作成Jobを処理対象のレコード数分だけ作成します

CSVを作成するJob

  1. Jobに含まれるIDを元にCSVを作成します
  2. 作成したCSVをS3にpushします

1st release時点では、処理対象のデータのIDが800個ほどあったので、1回の処理につき8個のCSV作成Jobが生成され、8個のShoryuken workerが8並列で処理します。

個々のCSV作成Jobの処理時間は概ね10分〜11分に収まっているので、結果的に15分以内に処理を完了させるという目標を達成できました。処理対象のIDが増加してもShoryuken orkerの数を増やすことでスループットを増加させることが可能になっており、スケールアウトしやすい構造になっています。

Shoryukenについて

あまり日本語の記事を見かけなかったので簡単に紹介します。

Sidekiqとほぼ同じような使い方で使えるworker gemです。Sidekiqと異なる点として、Queue storeにAWS SQSを使います。元々トレタでは非同期処理にはSidekiq + redisを使っていましたが、今回のようにJobの数がスケールしていくことが想定されるケースにおいてはQueueのサイズや数に融通が効くSQSの方が適しているという判断の元、SQSを使うためにShoryukenを採用しました。

Tips: 各Jobのロギングについて

ShoryukenにはJobの実行時に前処理・後処理を挟むなどの目的に使えるMiddleware機構があります。 今回はJob実行時のパフォーマンス計測とエラー時の原因調査のために各Jobのログをfluentdで拾ってBigQueryに入れておきたかったので、以下のようなMiddlewareを自作してログをテキストファイルで残すようにしました。

module Shoryuken
  module Middleware
    module CustomMiddleware
      module Server
        class ProcessingTimeMeasure
          def call(worker, queue, sqs_msg, body)
            shoryuken_log_path = Rails.root.join('log', 'shoryuken_worker.log')
            @logger = ::Logger.new(shoryuken_log_path)
            @logger.formatter = proc do |_, _, _, message|
              message.to_json + "\n"
            end

            @success = true
            @messages = []

            started_at = Time.zone.now

            # ここでJob本体の処理が実行されます
            yield

            ended_at = Time.zone.now

            elapsed_time = (ended_at - started_at) * 1000
          rescue => e
            @success = false
            @messages << e.to_s
            Bugsnag.auto_notify(e)
          ensure
            body = {
              timestamp: Time.zone.now.to_i,
              started_at: started_at,
              ended_at: ended_at,
              elapsed_time: elapsed_time,
              success: @success,
              messages: @messages,
              worker: worker.class.to_s,
            }
            @logger.info(body)
          end
        end
      end
    end
  end
end

このMiddlewareをShoryukenに積むにはconfig/initializes配下に適当なファイルを置いて以下のように追記します。

Shoryuken.configure_server do |config|
  config.server_middleware do |chain|
    chain.add Shoryuken::Middleware::CustomMiddleware::Server::ProcessingTimeMeasure
  end
end

まとめ

今のところ安定して稼働していますが、基盤としてはまだ汎用的なつくりにはなっていないので、汎用基盤として稼働させていくためには以下のような機能が必要になりそうだと考えています。

  • 処理時間に制限があるJobの時間が経過した場合に通知する(Shoryuken workerの数を増やすタイミングを見極めるための何か)
  • Job管理(workflow系ツールの導入など?)
  • Shoryukenのプロセス監視と自動復旧

今後も開発を続けながら、何かシェアできる知見が得られたらまた開発者ブログに書きたいと思います。

お約束

トレタはエンジニアを募集しています。iPadアプリやAPIの開発以外に、こういった基盤開発にも興味があるエンジニアはぜひご応募ください。

www.wantedly.com

www.wantedly.com

テストデータ生成に欠かせない便利な◯◯kitの勧め

iOSを担当している高です。

開発しているとそれっぽいテストデータが欲しいってことが結構あります。今までは各々がローカルで都度スクリプト書いてるような状態で、僕の場合はトレタアプリの中に直接書いて都度書き捨ててる感じでした。 これは相当効率が悪いですし、自分用に書いたものは人にも共有しづらいという問題があったのでこれを機にツールを作ってみることにしました。

開発上の課題

iOS開発をする上でこの様な課題がありました。

  • パフォーマンス確認のために大量データが欲しい
  • テーブル数などの設定をかんたんに変えたい
  • 外部連携が必要なテストデータが欲しい。けれどE2Eで作るのはちょっと大変

大量データは文字のごとくです。通信時間やアプリ上での描画パフォーマンスを確認するために想定される最大のデータを用意したい。データ作成が1回だけで良ければ手で作ってもいいのですが、トレタの場合日付が軸になるので次の日には過去データになってしまいます。そうならないように未来データとして作ると今度は毎回そこまで行くのが面倒という本末転倒なことになります。

テーブル数も現状アプリからは1テーブルずつでしか追加と削除が出来ません。これはユーザが使う分には問題無いのですが、テストで多様なデータを用意する場合には結構手間な作業になります。消すのも同様です。

外部連携が必要なテストデータに関してはアプリ上ではまずは表示の確認がしたいだけのことが多いです。charlesでレスポンスを書き換えたりしていたのですが、結構手間なこともあり、あまり実用的ではありませんでした。

octokit

言わずと知れたGitHubのAPIツールです。 https://octokit.github.io

コマンドラインからちょっとしたことをやりたい時なんかにoctokit.rbを使ったりしますよね。こんな感じでトレタのAPIも触れるといいなぁと思いました。

toretakit

ということでtoretakit inspired by octokitを作ってみました。 f:id:y_koh:20160428113221p:plain

octokit.rbと同じようにgemとして作っています。

予約データを作る

トレタで予約を作るために必要な最低限の情報はこれらになります。

  • 予約日時
  • 氏名(漢字)
  • 氏名(よみがな)
  • 電話番号
  • 人数

氏名や電話番号にはfakerというgemを使いました。結構有名なgemなのでご存じの方も多いと思います。ただ、氏名に関してはfakerはふりがなに対応していないので、gimeiというgemを使いました。 gimeiの説明をREADMEから引用します。

gimei は、日本人の名前や、日本の住所をランダムに返すライブラリです。テストの時などに使います。似たようなライブラリにfakerがあります。fakerはとても優れたライブラリで、多言語対応もしていますが、ふりがな(フリガナ)は流石に対応していません。gimei はふりがな(及びフリガナ)に対応しています。

出現率の異なるランダム値が欲しい

テストデータを作るときに同じデータばかり作ってもしかたがないのである程度ランダムで作りたいことがあります。

例えばトレタの場合だと予約するときの人数はランダムで決めたい、みたいな感じです。ただこの時に本当に単純なランダムにしてしまうと2人の予約と10人の予約が同じ回数だけ出てくるという現実離れしたデータになってしまいます。

実際のデータではところどころ山があると思いますが、そこまで忠実に再現するのは既存データを洗ったりしないといけなくてちょっと大変なので簡単に大きい数字の方が出にくいようにしたいと思いました。

なんか良いアルゴリズムが無いかなと探したのですが見つからず、とりいそぎベタにこんな感じで対応してみました。数字の間隔はなんとなくです。

def rand_count()
  r = rand(100)
  case r
  when 99..100 then 10
  when 96..99 then 9
  when 93..96 then 8
  when 90..93 then 7
  when 85..90 then 6
  when 80..85 then 5
  when 70..80 then 4
  when 60..70 then 3
  when 50..60 then 2
  when 0..50 then 1
  end
end

何か良いアルゴリズムがあったら是非教えてほしいです!

どんなことが出来るようになったか

こんな感じでテーブル数が100の設定で、2回転しているデータ(かなりの繁盛店!)をそれっぽく作る、みたいなことが簡単にできるようになりました。 f:id:y_koh:20160428113031p:plain

(人数が固定になってますが、先ほどのランダムロジックはここではまだ使ってなくて他のところで使ってます)

また、他のPJでも使ってもらえるようになりました。 f:id:y_koh:20160428113112p:plain

今後の展望

現状toretakitはAPI通信とデータ生成が一緒くたになっているのでのちのち別gemとして切り出したいなと思ってます。

将来的には複数店舗さんのシミュレーションデータ生成ロジックを用意して、コマンド一発でそれっぽいデータが用意できるようにしたいなと思っています。

まとめ

僕は普段はSwift、たまにObjective-C(既存コード)という生活を送ってるのですが、こういったツール系で普段と違う言語・環境で開発するのも楽しいものですね。

引き続きエンジニアは募集中ですので興味のある方はチェックしてみてください!

© Toreta, Inc.

Powered by Hatena Blog