EVENT REPORTイベントレポート
ヒカラボレポートとは、開催されたヒカラボにおいて、登壇者が伝えたい講演内容を記事としてまとめたものです。ご参加された方はもちろん、ヒカラボに興味があるという方も是非ご覧ください。
ヒカラボレポートとは、開催されたヒカラボにおいて、登壇者が伝えたい講演内容を記事としてまとめたものです。ご参加された方はもちろん、ヒカラボに興味があるという方も是非ご覧ください。
ヒットすればするほど安定したインフラ基盤を求められるスマホゲーム業界。守られない規約、成功しないビルド、起動しないアプリ……開発の壁は尽きることはありません。 今回は、そんなスマホゲーム業界の最前線を走る株式会社アカツキのエンジニア3名をお迎えしたイベントの内容をレポート。大ヒットアプリの開発を支えるノウハウを大公開します! 大トラフィックのサーバーサイド、ネイティブアプリのゲーム開発やNode.jsに興味のある方、必見です!
講演者プロフィール
株式会社アカツキのCTO、田中勇輔です。
今日は、Ruby on Rails(RoR)とAWSで1分間で10万リクエストを処理するにはどうすればいいか?ということについてお話しさせていただきます。
RESTなAPIサーバであるRoRを選んだきっかけ
そもそも「なぜRoRを選んだのか?」という理由は、ずばりRoRなら社内の既存資産を活かすことを考えたからです。既存資産というのは、認証や課金のライブラリ、環境構築やデプロイの仕組みといったものです。学習コストはかかるものの、RoRがあれば大抵のことができるので、企業としてはとても楽です。
RoR、AWSで1分間に10万リクエストを処理するために必要な3つのこと
RoR、AWSで1分間に10リクエストを処理しようとした場合、3つのポイントを押さえておく必要があります。
その3つとは
・スケールアウト戦略
・負荷テスト
・地雷をうまく避ける
です。
大量のリクエストがあっても、スケールアウトしておけば怖くない
前もってスケールアウト構築しておけば、急激にリクエストが増加してもしっかりと対応することが可能です。もちろん、スケールアウトしたアプリケーションサーバの構成管理やデプロイ方法についてはきちんと考えておく必要があります。基本的には「アプリケーション構成+デプロイ」で対応するということです。
DBはRDBを用いました。一般的なWebサービスであれば、DBパラメータはRDSデフォルトでほぼ問題ありません。この辺りのデフォルト値のチューニングはうまくバランスを取っていてAWSはすごいと思います。
RoRのShardingはOctopusを使用しました。構成は、ゲーム共通のデータを管理する単一の共通DBと、ユーザの資産を管理する複数のユーザDBで分かれています。ユーザDBのShardingは、config/shards.yml を用意して、リクエスト単位(コントローラーレベル)にOctopus.usingで接続先を指定しています。
そうすると、共通DBへのアクセスは、ActiveRecord レベルでusing(:master)を指定することになります。ここで毎回接続先を指定するのは辛いので、共通データ系のモデルにはusing(:master)を指定したクラスから継承させたいという発想が出てくると思います。が、ここにはOctopusの難点があって、「継承したクラスでoctopus_establish_connection がうまくいかない」という問題があります。
最終的には「毎回using(:master)を書く」という対処に落ち着きました。それは、ゲーム共通データもUser Shardに置いてしまうと、データ更新のトランザクションが完了するタイミングがズレてしまうからです。また、一部のDBのみ失敗してしまう可能性もあります。
しかし、実際やってみて、ゲーム共通データもUser Shardに置く方が辛くなかったと思います。データ更新のズレはなるべくコミットするタイミングをあわせることで少なくできます。DB単位の整合性はとれるので、データ更新の失敗もすぐに対応すれば大きな問題にはならないはずです。その対応コストよりも、毎回using(:master)を指定するコストの方が大きかったかなと今は考えています。
スケールアウト戦略は、やはり設計段階が重要です。その理由は、例えば、今お話ししたようなOctopusを後で導入しようと思っても、全てのDB接続に関するコードを正しく書き換えるのは正直辛いものがあるからです。ただ、全てにおいてスケールアウト戦略を立てる必要はなく、2万Req/minを越えた辺りから検討しても良いのでは?と思っています。
負荷テスト
負荷テストで一番使われているものといえばJMeterですね。ただ、JMeterのjmxスクリプトを作るのは中々大変な作業です。そのような時はジェネレータを作ってしまえばいいのですが、Rubyの世界にはruby-jmeterという便利なGemが存在します。ruby-jmeterで、jmxスクリプトが簡単に作れるようになり、負荷テストスクリプトのメンテナンスもすごく楽になります。
例えば、スレッド数の指定やリクエストヘッダの指定、リクエストbodyの指定などがスムーズにできるようになるのが良いです。負荷テストは運用中でも役に立つものなので、ぜひ継続的に開発できる仕組みを整えていきましょう。
RoR、Radisに関する失敗談から学ぶ「地雷をうまく避ける」方法
地雷をうまく避けるにはどうすればよいか?一番よいのは、他者の失敗から学ぶことです。ですから、ここでは自分達が失敗したことと、その対策についてお話したいと思います。
<失敗1>
RoR 4.1/Arel 5.0 でコネクション切断時にスキーマキャッシュが使われない
Railsのコネクションプーリングをそのまま使うと、UserShardの数 * プロセス数 * ConnectionPool数のコネクションが張りっぱなしになって、使いまわされます。そのリソースを節約するために、sonots/activerecord-refresh_connection を利用していたのですが、リクエストの度に、SHOW FULL FIELDS FROM ~~ が発行されていました。
そのため、急激な負荷が発生した時に、共通データDBのCPU負荷が高くなりすぎるという問題がありました。当時は「SHOW FULL FIELDS FROM ~~」の発生原因を潰していく時間的な余裕がなかったので、sonots/activerecord-refresh_connectionを止まるという選択をしました。
これから踏む人は少ないと思いますが、対処法としては、「winebarrelさんのarel_columns_hash を使う」もしくは,「Arel 7.0のバージョンを使う」です。
<失敗2>
Redis: KEYS patternを使っていた
RedisではO(N)以上の計算量のコマンドを使うときは十分注意する必要があります。KEYS patternの計算量は、O(N)なのですが、もっと悪いことに、全てのマッチするKEYを取得しなければならないので、全てのShardにリクエストが送られます。この負荷に対しては、サーバを増やしても意味がありません。
当時原因をきちんと特定せずに急遽サーバを増やすということをやってしまい、64台にもなってしまいました。本当は8台で十分な負荷量です。コードレビューでの漏れや、負荷テストにおけるRedisのデータ量チェック漏れ、深夜の(思考能力が低下している状態での)緊急対応ということが重なった結果の悲劇です。
データ量が多い前提の負荷テストはやると思いますが、キャッシュサーバは漏れがちだと思いますので、特にRedisには注意してもらうのが良いと思います。
最後に
「RoRとAWSで1分間で10万リクエストを処理するために必要なこと」として、「スケールアウト戦略」「負荷テスト」「地雷をうまく避ける」の3つを紹介しました。特に、「負荷テスト」は「地雷をうまく避ける」ことにも通じてきますので、ぜひ押さえていただければと思います。今日はありがとうございました。
今日のタイトルは「知っていると苦労しない、ネイティブアプリ開発でコケるポイント対策」です。要は、「汚いコードとどう戦っていくか」っていう話です。
ワークフローを見直し、ネイティブアプリ開発時の苦労を軽減
ネイティブアプリ開発時に苦労する点と言えば、
・行数が多い
・クライアント、サーバ、DB、リソースなど、ステートが多い
・自動テストがしにくい
・ビルドが難しい、長い
・プラットフォーム依存症
・「出したら終わり」ではない
などが挙げられると思います。
皆さんも思い当たる点があるのではないでしょうか?
このような「苦労」を克服するためには、開発のワークフローをうまく回すことが必要です。弊社では開発の段階を「コーディング→レビュー→ビルド→アプリ配信→実装・修正依頼」に分け、それぞれの段階に適したツールを用いることで、自動化を進めていくことにしました。
コーディングにはGitHubを導入
弊社では、コーディングの部分にGitHubを導入しています。「これで自動化ができる」と言うほどではないですけど、Webのプロジェクトにはすごく合っています。
「GitHub自体が良い」というのはもちろん、オープンソースのように自由にアイデアを出せるのが個人的にはすごく好きです。容量が大きいバイナリファイルを取り扱うのが早いのも良い点です。
レビューにはオリジナルの「アカツキ版HoundCI」を使用
コーディングスタイルのチェックはHoundCIというツールを用いています。HoundCIとは、GitHub上でコーディング規約に違反しているところを自動で認識してくれるサービスで、Ruby, CoffeeScript, JavaScript, SCSSに対応しています。
このHoundCIをRoRのサーバ側のリポジトリに適用したところ、すごく好評で「クラウント側にも欲しい」と思うようになりました。でも、残念ながらHoundCIはC++に対応していません。
「それなら、自分で作っちゃえ」ということで、アカツキ版のHoundCIを作りました。
アカツキ版のHoundCIについては、弊社のブログ(http://hackerslab.aktsk.jp/)で紹介していますので、興味のある方はぜひ見ていただけると嬉しいです。
ちなみに、HoundCIをクライアントのリポジトリに導入した直後は1PRテストにつき50件以上の違反が出ることがありましたが、それも一ヶ月弱で定着しました。
ビルドの自動化は CircleCIとTravisCIをチョイス。自動化テストがスムーズに
ビルドの自動化はCircleCIとTravisCIを活用しています。GitHubとの連携が便利なのがいいですね。でも正直なところ、大事なのは「どのサービスを使うか」ではなく、「個人のビルド環境は信用しない」というところかなと思います。
アプリ配信はCrashLytics導入でさらに便利に
弊社では、GitHubにプルリクエストマージ後、CIサービスからクラウドへ自動アップロードしています。そうすることで、非エンジニアが最新のビルドをいつでも触ることができるのが魅力です。デバッガからのバグレポートにGitのコミットハッシュが記載され、そこからバグが見つけやすくなるというのもいいですね。
それから、CrashLytics を導入しています。クラッシュレポートを自動で送信してくれたり、アプリ配信機能を兼ねていたり点がメリットですね。
実装・修正依頼は、非エンジニアでも使いやすいJIRAを採用
実装・修正依頼では、チケット管理ツールのJIRAを使うことが多いです。エンジニアだけの観点から言うとGitHub+ZenHubがオススメですが、非エンジニアにも分かりやすいのはやはりJIRAだと思います。
最後に
今日は、弊社のネイティブアプリ効率化のためのノウハウを紹介させていただきました。基本的に「WEB界やサーバ界で当たり前のことをクライアントに拡張したものが多い」という気がしています。
改善したい点はまだまだあります。より良い方法があれば、シェアしましょう。私からは以上となります。ありがとうございました。
私の方からは、Node.jpの非同期処理についてお話をしたいと思います。
Nodeを使って2D横スクロールアクションゲームを作成
今は2D横スクロールのアクションゲームを制作しています。マルチプレイに対応しており、4人同時対戦が可能です。プロトコルはwebsocket、プラットフォームとしてはNode.jsを使っています。Node.jsはパフォーマンスを出しやすいのが魅力ですね。Non-blacking、EventDrivenの特性がwebsocketとの相性が良いです。
非同期処理のポイントはPromiseとGenerator
Nodeでの非同期処理でキーとなるのがPromiseとGeneratorです。
まずベースとして、ある非同期処理があればそれをPromise Objectでラップします。非同期処理が完了したタイミングでコールバックが来ますが、Promiseではそれをthenでチェインする形で解決することが出来ます。生のコールバックに比べてインターフェイスが綺麗になりますね。
Generatorは、luaのコルーチンと同様の仕組みです。yieldとnextで非同期処理の実行と制御のやりとりができます。Generatorを使うことでインターフェイスとして非同期処理を縦に書く事ができ、可読性が大きく向上します。
Generator利用時のデファクトスタンダードco
Generatorを用いる際は、coというライブラリを用いるのがおすすめです。生のGeneratorは非同期処理側と呼び出し側の世界の行き来を強く意識して書く必要がありますが、coを使うことでその部分の負担がだいぶ下がります。
また、Generatorのyieldに渡せるもの(yieldables)はobjectやarray、thunkなど様々ですが、そこにはPromiseオブジェクトも含まれています。個人的には、非同期処理はまずPromiseでくるんでおき、特に複雑性が増す場所ではGeneratorに渡すことで綺麗に書く、という方法が気に入っています。
まとめ
Node.jsは、特に双方向通信のようなパフォーマンスが求められる場面では強力な選択肢です。一方で、Node.jsには常にcallback地獄の話題がつきまといます。今回の紹介が非同期処理を気持ちよく行う一助になれば幸いです。本日はありがとうございました。