Edited at

TD APIをつかったアプリケーション構築の考え方

More than 3 years have passed since last update.

こんにちは。ちょびえです。TreasureData AdventCalendarも19日目となり、ゆるふわな感じで進行しております。今日はTD APIをつかったアプリケーション構築について書いてみようかと思います。


モチベーション

最近のウェブゲーム(広義の意味としてなんかしらHTTPを使ってるもの、という定義としておきます)では大規模なバッチ実行をしつつユーザー間のマッチングを行ったりする例「Cross2014 ドリコムの データマイニング活⽤用事例例 -‐‑‒ リアルタイムギルドバトルのマッチング最適化 -‐‑‒」等の事例が公開され、カジュアルにビッグデータ処理をアプリケーションから利用するという事例があがってきているのが記憶に新しいと思います。

私共のウェブゲーム業界のよくある例としては、勝敗履歴やユーザーのアクティビティ等から適切なマッチングを行いたい、よりアクティビティが高い人と遊べるようにSuggestionしたい、といったようにこういった部分で競合ゲームとの差別化を図り、ゲームがもっと楽しく遊べるようにしたい、という背景があります。

しかしながら、一般的なアプリケーション側で用意しているようなMySQL等の構成を流用しつつ、こういった仕組みを作ると色々な課題に遭遇します。


  • データベースリソースの問題


    • 計算過程や計算結果の挿入で高負荷にしてはいけない



  • バッチ実行環境のリソースの問題


    • メモリが大量に必要になったり、計算に時間がかかりすぎてはいけない

    • 既存のバッチ系に影響を与えてはいけない



  • ネットワークリソースの問題


    • バッチサーバーからデータベースとのトラフィックは適度に抑えないといけない



いずれにしても、専用のマシンやネットワークを準備すれば解決出来る問題ではありますが、Webサービスは基本的にあるものでなんとかやりくりしていかなければならないので理想を言っても仕方がありません。

Treasure Dataのサービスのことをよくよく考えてみると、裏側はHadoopなわけでして。であればTDを大規模なバッチデータ実行環境としても使うことができますね。

ゲーム観点で言うと分析だけにつかうのはもったいないので、TDのAPIをつかってアプリケーション構築の考え方について考察を深めていきます。

少し本題から離れると、最近の傾向としてはContainerでの仮想化が盛んですので将来的には隔離した環境でバッチ処理をばんばんクラウド側になげれるような将来も遠くないのかもしれませんが……


おおまかな流れ

それではおおまかな流れを確認したいと思います。アプリケーションはさておき


  • 1) TD上にデータが有る


    • 1) そのままQueryを投げる



  • 2) TD上にデータがない


    • 1) 小規模のデータセットならBulk Importしても許容できる。大規模なデータセットだと時間がかかるので用途に応じて許容範囲内ならImportしてQueryを投げる



と、データセットがクエリを実行したいときにすでにあるか、ないかでImportするしないが当然変わります。当たり前っちゃ当たり前なんですが、これに加えてクエリの計算結果をどれくらいの時間で結果を取得したいのか、遅くとも数十分以内には結果を取得したいのか、時間かかっても問題ないのか、等の要件によりどういうふうにアプローチしていくかが変わっていきます。

参考までに1−1のTD上にデータが有る場合で考えてみます。


アプリケーション ー> バッチサーバー <ー> Treasure Data
        <ー> データストレージ

バッチサーバーからTD宛にQueryを投げて、終わり次第結果を取得しつつ、データストレージに突っ込んでいけばOKです。簡単ですね!

Treasure Dataから直接指定のMySQLに書き出す機能もありますが、一般的なウェブゲームサービスであればユーザー管理やFirewall管理などの運用上手間を省きたい、ということがあったり。はたまた負荷のコントロールを行いたい、結果セットに対して後処理を入れたい、といったもろもろの事情から自分たちでコントロールしやすいようにバッチサーバーからREST APIをコールして結果セットを取得したほうがなにかと都合が良かったりします。

そんなことは置いておいて、よくある例で考えてみましょう。


結果を得るまでどれくらいの許容時間があるか

最近のウェブゲームでよくあるGvG形式(要はプレイヤー同士で何十人と集まって競い合うゲームですね)のマッチング開始時には編成や装備状況のロック期間を設けている事が多くあります。

このロック期間がある背景的には直前までチーム構成、装備状況などを弱い状態にしてマッチング時のズルが出来ないようにしています。仮にマッチングが終わった直後に編成などが変えられたらアンフェアなマッチングになってしまうのでそういうのは避けたいのでロックしているというのが多いです。あとは、何かしらの理由によりマッチングの計算が出来なかった時の時間のバッファです。

例えばロック期間が30分と考えると、計算結果をインサートするまでに30分のバッファがあるということです。気合入れてインサートする場合は2分、安全にインサートするのに5分かかるというのであれば計算に使える時間は少なくとも25分程度ある。そして、計算1回に10分かかるのであれば最悪1回は失敗できる。ということですね。

……

数秒で終わる処理ならまだしも、こういった大規模な演算は複雑になりがちなので時間が結構かかってしまうので、、、そう考えるとバッファ時間はあれど結構シビアですね。

最終的にはどれぐらいのデータセットを使ってどんだけ複雑な計算を使うか、で変わってくるので杞憂かもしれませんが。逆にTD場で平均1分で終わるQueryだったら20回ぐらい失敗できるので楽勝ですね!なんて考え方もできるかと思います。


クエリ実行を考える

特にクエリを投げる部分で考えるべき事はないので、Query実行したらIDをメモっておいてQuery実行状況をポーリングして結果セットを自分で取得するのでもいいですし、そのまま結果セットをHTTP経由で受け取れるようにしても良いと思います。

後者の場合アプリケーションのエンドポイントとは別のエンドポイントを用意したくなると思うのでそれはそれで可用性考えたりするのは面倒なんで、僕は前者の自分でFetchするほうが多いです。

クエリの実行に使うAPIは下記となります。

http://docs.treasuredata.com/articles/rest-api#post-v3jobissuetypedatabase

上記には書いてありませんが、結果セットのurlの指定はここらへんにかいてあります。

http://docs.treasuredata.com/categories/result

https://github.com/treasure-data/td-client-ruby/blob/1fddf92c301800f10af25a8f0c8dacb5245c37fc/lib/td/client/api.rb#L645


結果取得からインサートまでを考える

http://docs.treasuredata.com/articles/rest-api#get-v3jobresultjobidformatmsgpackgz

結果セットを取得する場合は、まずどれくらいのサイズのデータが来るかを事前に検討しておいてください。HTTPクライアントによっては全てのデータをメモリ上に展開してしまう物もありますので、想像以上にメモリを使ってしまう事もあるかもしれません。事前に自分たちが使っているHTTPクライアントがどういう挙動をするかは確認をしておきましょう。

私がつくったProduction環境で動作している機能ではQueryのIDを記録し、実行状況をポーリングするようにしています。

これは現状のアプリケーション構成を変更せずにできるのと、インサート時の負荷コントロールが容易だからです。Production環境で使っているDBとなるとその他のクエリの処理などで常時負荷がかかっていることがあると思います。特に該当データベースが他の機能と相乗りをしている場合は自分たちでQPSを調整してInsertを行ったほうが負荷のコントロールが行いやすいです。

一般的な話になってしまいますがどれくらいのQPSに調整すればいいかというのはアプリケーション側からデータベースをどういうふうに使っているかにより変わりますので導入前の負荷テストや、現状運用している中で数値を見極めていくしかありません。

よくわからなければInsertの許容時間から1Insertあたりのwait時間を逆算して安全路線でやってみる、それで大丈夫そうならもうちょっと速くしてみるというふうに日々調整していくのがいいかと思います。


結果の代替案を考える

何かしらの理由によりQueryが失敗した場合、というのも考えておきます。

自分たちが設定したバッファの時間内にリトライができるのであればそのままリトライをすればいいのですが、何かしらの理由によって計算のバッファ時間を超えてしまう時はどうすればいいのでしょうか。

Queryが実行出来なかった場合にサービスが提供できなくなってしまうと死活問題です。どうしてもインターネット経由の実行となりますので何かしらのネットワーク障害に巻き込まれて使えない状態になってしまったら困ってしまいます。基本的にそういった障害に遭遇することはありませんが、何か問題が発生した時でも問題が出ないようにつくっておくのはとても大事です。

なにかしらの理由により使えなかった場合はそれはどうしょうもないので代替案が使えないかを検討します。

先のマッチングでいえば、最悪やや精度の低いマッチング計算を内部で行ったり、前回のマッチングデータが残っていればそこから似たようなマッチングデータを生成すること等も場合によっては可能かと思います。

最適な結果セットが得られなくても別アルゴリズムで代替するなどは可能ですので、外部に計算を託す場合はベストな手段、ベターな手段などいくつかの計算のしくみを用意できていると確実です。


日々の運用を考える

運用している時によくある問題ですがデータセットの量が一定量であれば問題ないのですが、増えていくようなタイプだとクエリの実行時間が少しずつ長くなっていくことが有ります。そういった場合に気づけるように、必ずクエリからInsertまでの実行時間をグラフ化できるようにしたり、実行時間が一定を超えたらアラートを流せるようにしておくと安全です。

ちょっと話は戻って、自分たちでゴリゴリプログラムを書いてマッチング処理をさせるケースについて考察を深めてみましょう。

複雑なアルゴリズムで書いた処理はその後メンテナンスを行うチームメンバーが管理できなくなって凶悪な技術的負債になる可能性があります。プログラミング言語で処理を書いた場合はもはやプログラマしか変更することができませんが、最近のゲームデザイナであればこういったビッグデータ処理もできるので、変更が気軽にできるようになります。とはいえ、ビッグデータ処理でよくある多段で結果をどんどん計算していく場合はそれはそれで負債っぽくなりがちなので、どのレベルの人であれば運用をまわせるかは考えておきましょう。

またプログラム側で書かない場合は、一般的なSQLやpigで処理を書くことになるので他のアプリケーションへの導入も簡単になります。TDへのバッチ実行システムと、結果取得や後処理を行ってDBに手加減しつつInsertできる機能を一個つくっとけばあとは使い回しが聞くので楽ですねー。


まとめ

TDをバッチシステムとして利用する場合の考え方をまとめると


  • データセットがTD上にあるか、ないか。ない場合はBulk Importの時間は現実的なのか

  • 負荷のコントロールや後処理をする必要があれば自分たちで結果セットを取得する

  • 計算に使える最大時間を決める(Import,Query+Buffer,Fetch,Insertまで。何度クエリが失敗できるかも考える)

  • 最悪ダメだった時の代替処理が自動で使えるように準備する(精度の低いマッチング、前回の結果の流用等)

  • 運用面でまわせる技術を使っている事を確認する(オーバーエンジニアリング(やりすぎたプログラム)は後に負債となる)

  • 実行時間のモニタリングはきちんと行う。アラートまで作りこむ

それでは、TD APIを駆使して運用面で楽ができるようにチャレンジしてみてください