火曜日, 10月 13, 2009

【Google App Engine】 TaskQueueはスケールしない!? このエントリーを含むはてなブックマーク


 TaskQueueによって、何がどう改善されるかについて、Reflex iTextを使って具体的に調べてみたので報告したい。特に、パフォーマンスについて、先日の記事(ココ)と比較しながら説明する。

 やりたいことは単純で、大量のPDFをいかに短時間で生成するかである。前回のテストでは、クライアントから大量のリクエストを投げることでこれを実現しようとしたが、リトライ処理が多発してスケールしなかった。では、TaskQueueを使うとどうなるのだろうか。今回はそこにポイントを絞って調べてみた。

TaskQueue概要

 まず、TaskQueueであるが、これはWebHook型のキューシステムで、Task自体を通常のServletとして書くことができる。Googleのサンプルを見ればわかるように、QueueFactory.getDefaultQueue().add()を使って、URLとRequestParameterを与えるだけで、Worker Servletが呼ばれる仕組みになっている。このシンプルさはとても驚きである。
 ただし、WorkerのURLは、呼び出し元のServletと同じプロジェクトに配置しなければならない。(別プロジェクトに分けて、http://などをつけて実行すると、「java.lang.IllegalArgumentException: url must not contain a 'scheme' part - contains :http」というエラーが出る。)


/**
 * Task Queue worker servlet that offsets a counter by a delta.
 */
public class SimpleCounterWorker extends HttpServlet {
 public void doPost(HttpServletRequest req, HttpServletResponse resp)
  throws IOException {
  Counter.createOrIncrement(
    req.getParameter("name"),
    Integer.parseInt(req.getParameter("delta")));
 }
}



/**
 * Servlet that schedules a counter to be increased.
 */
public class SimpleCounterServlet extends HttpServlet {
 public void doPost(HttpServletRequest req, HttpServletResponse resp)
   throws IOException {

  QueueFactory.getDefaultQueue().add(
    TaskOptions.Builder
     .url("/workers/simplecounter")
     .param("name", "thecounter")
     .param("delta", "1"));

  resp.sendRedirect("/simplecounter.jsp");
 }
}


TaskQueueの制約について

 実装はとても簡単に見えるが、アプリケーション全体をうまく機能させるには、排他制御を考慮しながらMemcacheなどを使ってやりとりしなければならないなど結構大変だったりする。また、キューに追加できるタスクの総数やMemcache自体に格納できるサイズの制限などもあるので注意が必要である。

TaskQueueを使うに当たって関係すると思われる限界値は以下のとおり。

  • キューに追加できるタスクの総数が最大10万件/1日
  • Memcacheに一度に格納できるのは1MB。
  • Memcacheに登録できる総容量は10GB。
  • クライアントがダウンロードできるのは10MB。


 信頼性という意味では、特にMemcacheについては、いろいろ問題があるようである。そもそも、code.google.comに「アプリケーション側ではキャッシュの値が常に利用可能だと仮定すべきではありません。」とあるので、信頼してはいけない。
 したがって、sharding counterのように、同時に複数のTaskが参照更新するものは、消えている場合を想定して、リクエスターにcounterの初期化ロジックを入れることまで考慮しなければならないだろう。しかし、そうすると、同時に初期化するものが複数存在してしまう可能性があり、排他制御が機能しなくなる可能性もある。

<参考記事>
Task QueueはMapReduceの夢を見るか
Memcacheでスピンロックを実装してTask Queue処理結果を集約してみるテスト
Task Queue君とmemcache君、疑って正直すまんかった

TaskQueueを使った処理の流れ

 Reflex iTextを使ったパフォーマンステストでは、TaskQueueを使って以下のような処理を実行させている。

  • クライアントは1タスクあたりの処理ページ数(以下unit)と、総ページ数(以下total)を指定する。
  • dispatcherは、total / unit (余りは繰り上げ) で算出された数だけ、TaskQueueにpdfservice処理を登録する。
  • クライアントにkeyを返却して一旦終了。
  • pdfserviceはPDFを生成し、byte配列をMemcacheに保存する。(非同期)
  • クライアントはkeyをパラメータにして処理を要求する。
  • dispatcherはTaskが完了しているかチェックし、指定されたkeyの処理が全て完了していればMemcacheからPDFを取り出す。PDFマージ処理を行い、クライアントに返却する。



実行結果

今回のアプリにおいては、MemcacheにPUTできる最大のサイズ1Mb以内にするために、1Taskあたり70Pにする必要がある。(つまり、unit=70)また、1回にダウンロードできるサイズは10Mbであり、1PDFにつき700P以内にする必要がある。この処理において起動されるTaskは10本で、全てのタスクが終了するのに1分20秒かかった。

前回のアプリと比較するために、700ページを12リクエスト(合計8400ページ)処理させた場合について計測してみると、unit=70の場合(700/70*12=120タスク)で約15分かかった。

考察

 実行結果をみてもわかるように、前回の結果が約8千ページを4~5分であったのに対し、TaskQueueを使った今回の結果は約15分と、遅くて残念な結果となってしまった。前回のアプリのような大量にリクエストを投げる場合には、リクエスト数に応じてインスタンス数が増えるが、一方のTaskQueueを使った場合は、同じインスタンス内でのみTaskを起動していたのではないかと想像できる。10Taskで1分20秒で、120Taskで15分なので、ほぼリニアに処理時間が増加している計算である。マルチで実行している場合は、120Taskでも1分20秒で完了するはずだ。
 スケールしない(させない?)のは、同一プロジェクト内でしかWorkerをコールできない仕組みと何か関係があるかもしれない。

 しかしTaskQueueを使うといいこともある。大量にリクエストを投げる方法では、8千ページ前後が限界であったが、TaskQueueを使えば、時間はかかるものの、8千ページ以上のPDF生成処理を行うことができる。(上限はMemcacheの2Gbで、MAX約70万ページ)
 レスポンス速度重視であれば、TaskQueueの多用は禁物であるが、非同期処理で、いろいろな用途はあると思われる。 

<関連>
TaskQueueはスケールしない!?2
TaskQueueはスケールしない!?2

<追記>
 ・ 軽いWorkerタスクを別途用意して連続して負荷をかけることでVMは16程度起動することがわかっている。今度はServletContext+UUIDで負荷分散状況を調べてみた
 ・ WdWeaverさんの実験 スケールアウトの真実

0 件のコメント:

 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。