金曜日, 10月 30, 2009

【Google App Engine】 Keyとカウンタは別々に考えるといいかも このエントリーを含むはてなブックマーク



この記事書いているあいだにこんなニュースを見つけた。これはすごいや。
100万PV/日のmixiモバイルアプリをGoogle App Engineで実装した@gclue_akira氏に直撃インタビュー

さて、本題

不可能と思い込んでいたことができることもある

 何度かイタい目に遭うことで、またどうせ出来ないだろうと思い込んでしまうことを学習性無力感という。長期間にわたって鎖に繋がれたままの犬は、鎖を外してもしばらくは逃げないそうである。GAEはもともと情報が少ないのと、嵌りどころ満載なこともあって、学習性無力感に陥ることがしばしばある。だから、一人悶々とやるより、Blogに晒したり、GAE Nightなどの集まりに出向くなどして、情報共有に努めるのがよろしいかと思う。そこで重要なのは、ありのままの事実を晒すということ。現時点で正しい答えを識っているものは皆無に等しいのだから、誰かに正しい答えを訊くよりかは、自分で実際にやってみて、その結果だけを信じるというスタンスが重要な気がしている。また、思い込みによる間違った情報でも、公開することがきっかけとなり、いろいろと議論が深まって、正しい結論へと導き出せるかもしれない。というわけで、今度のGAE Nightへは、そういうスタンスで乗り込むつもりなので、そこんとこよろしく。(間違った情報を晒すつもりはないが言い訳はさせてね・・)
 ところで、当Blogで公開したもののうち、私自身が「学習性無力感」によって不可能と思っていたことがいくつかある。例えば、ココで紹介した前方一致検索は、Low Level APIの2つのFilterを使って、次のように書ける。このように、1つの項目に2つのFilterを指定することが可能で、Bigtable的には、これはRange Scanと解釈されるようである。

 String value = "コーヒー";
 Query query = new Query("Product");
 query.addFilter("product_name",FilterOperator.GREATER_THAN_OR_EQUAL,value);
 query.addFilter("product_name",FilterOperator.LESS_THAN,value + "\ufffd"); // "\ufffd"はUNICODEの最大値


 前記事では、GREATER_THAN_OR_EQUALの一つしか使っていなかったが、LESS_THANを同時に指定することで、”コーヒー”で始まっていないものを省くことができる。これまでaddFilterは1つだけが指定できると思い込んでいたのだが、内部の動きを知って、なるほどと納得した次第である。

1000件までしか検索できないことはない件

 ココでも述べているが、以前はたしかに1000件しか検索できなかった。正確にいうと、JDOでは1000件以上検索できたが、Low Level APIだと1000件が限界だった。ところが、SDK1.2.5では、FetchOptionsのlimitの値を増やして実行することで、2000件以上検索できることがわかった。しかし、30秒ルールの壁があるため、実際には2000件程度が表示の限界だろうと思われる。私たちのアプリでは、2468件の時点で30秒以上となりエラーとなった。(まあ、鎖から開放された犬が全速力で逃げようとして、30M付近の透明の壁にぶち当たったといった感じかな)
 また、Keyによる検索や更新については制限があり、Quotas_and_Limitsによれば、Batch Getは最大1000件まで可能で、Batch Put/Deleteでは最大500件まで可能である。また、countEntities()で取得できる件数も最大1000件であるが、datastore statistics APIを使うと1000件以上カウントできるとのこと(MLより)

The query restrictions are an artifact of the way App Engine's datastore is constructed, which makes certain operations (e.g. queries and reads) very fast and scalable but does limit the types of queries you can make, though you can typically get around these restrictions by re-thinking your model a bit.

We are working on adding built-in cursor support for easier paging through entities and have just added a datastore statistics API for, among other things, getting the total entity count, even if it exceeds 1,000. More details here:

http://code.google.com/appengine/docs/java/datastore/stats.html

And we also have a data export utility included with the SDK to make it easier for you to back up or even move off of App Engine should you choose to, and we're continuing to look at ways of making App Engine, particularly the datastore component, easier to use.

http://code.google.com/appengine/docs/python/tools/uploadingdata.html#Downloading_Data_from_App_Engine


使用例


 Query query = new Query("__Stat_Kind__");
 query.addFilter("kind_name", Query.FilterOperator.EQUAL, "ここにkind名を指定");
 Entity stat = datastore.prepare(query).asSingleEntity();
 Long count = (Long)stat.getProperty("count");


 また、以下のようにすれば、既存のAPIでもcountを取得できる。
 これは、Keyだけを対象にした検索であり実際の値までは取りにいかない。また、Keyはメモリー上のSSTableに存在するため非常に高速と思われる。(これはこれで非常におもしろいアイデアである)
 でもGoogleの人によると、「実際に計ったわけではないがStaticsの方が速いだろう」とのこと。その理由は、全件読むのではなくKindごと管理している件数を返すだけだから。MLのひがさんのポストより


Low level API:
 DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
 int count = ds.prepare(new Query("your kind").setKeysOnly()).asList(FetchOptions.Builder.withOffset(0)).size();


パフォーマンス

  • 92300件のデータをカウントするのにかかる時間を計測
    • Datastore Statistics : 0.553秒
    • 自前カウンタ(base)をkeyでget : 0.030秒
    • prepare.asList().size() : 17.338秒
    • prepare.countEntities()は結果が1000件

カウンタの実装とボトルネックについて

 アプリが扱うデータは無限個が前提だが、30秒ルールや件数取得の問題があって、実際に無限個のデータを全部扱うのは難しい。なので、私たちは、各々のレコードにシーケンス番号をつけ、Pagingなどの部分的な処理を基本とすることで、これらの制約を回避するように工夫してきた。
 シーケンス番号は、レコードのプロパティに保持しており、また、レコードを生成する都度、カウンタを管理するEntityの値をインクリメントする仕組みである。こうすることで、Pagingの際に、どこからどこまで処理したかを特定できるし、全体の個数もわかるようになる。ただし、カウンタ更新とレコード挿入は1つのトランザクションとして実行しなければならず、これがとても重いのが難点であった。例えば、トランザクション処理させるためには、カウンタのEntityとデータのEntityをEntityGroupとして括らなければならないが、そうすると、挿入の都度、Kind全体がロックされることになるので、大きなボトルネックとなってしまう。元来、Datastore書込は読込の10倍以上遅い。さらにDatastoreのトランザクション処理も遅い。そのうえ、テーブルロックに近い設計をしてしまうのは、ありえないぐらい遅くなる。とはいえ、トランザクション処理を諦めるわけにはいかないので、書込に関しては遅くなるのはしょうがない。(せいぜい、Memcacheを駆使した遅延書込で改善できたらいいなあぐらいは考えてはいたが・・。まあ、無力感に満ちた犬ですな)
 でも、いろいろ実装してみると、実は、レコードにつけるシーケンス番号はいらないかもしれないということがわかってきた。たしかに、ユニークなKeyは必要だが、Paging処理であれば、keyだけあれば十分。また、前方一致検索の例のように、複数件ヒットした場合でも、Keyと「商品名」を連結した新たな項目を作ることで対応できる。
 問題は全体の件数をどう取得するかであるが、これは先ほどの例のようなQueryか、Statistics APIを使えば可能である。Keyについては、KeyRange allocateIds()の自動採番を利用する方法もある。どうしても連番が欲しい場合には、カウンタEntityを作成しても構わないが、レコードのKeyとしては使わず、EntityGroupも構成しない方がいいだろう。そうすれば、カウンタそのものをスケールさせることもできる。(例えば、カウンタEntityを複数もちランダムに選択したものをインクリメントする。そして、すべての合計値をシーケンス番号とする、といった具合に。そんなサンプルがGoogle I/Oで紹介されていた。)
 ちなみに、トランザクション処理をしないとどうなるかというと、レコードの更新失敗が起きたときに、カウンタEnitityだけがインクリメントされてしまい、いわゆる歯抜けの状態が起きてしまう。(ユニークな番号としては使える)これは実際に起こる可能性があるものと考えた方がよいが、補償トランザクションなどでデクリメントするような処理は絶対にやってはいけない。(その時点でカウンタEnitityが更新されている可能性があるため)

Keyに何を入れるべきか

 当初はシーケンス番号をKeyとしていたが、普通のプロパティに入れてもFetchできるので、これをわざわざKeyにする必要はない。また前述したように、そもそもシーケンス番号でなくてもPagingはできる。Keyにはユニークなものを入れさえすればいいのだが、allocateIds()などで作成した機械だけがわかる値を入れても、何かもったいない気がする。なので、KeyFactory createKey()を使って、アプリのKeyを入れるのがよいのではないかと思う。このように、Keyさえ作成できれば、allocateIds()で取得したときのように、Batch PUTも可能である。
 実際に私たちはこのような方法で、以下のような商品マスタ管理用のKindを作成した。ここでは、Shop_code+Product_code+Revisionといったように、アプリでユニークになるKeyを作成している。Revisionは、レコードが更新されるたびにインクリメントされる番号で、楽観的ロックに使っている。また、更新前のレコードは消さないでそのまま保存している。そうすることで履歴管理にもなる。Datastoreの容量は膨大にあるので、こういった大福帳のような感じの設計もありかなと考えている。
ただし、履歴をレコードとして持つ場合、Datastore statisticsのcountだと履歴分も全て件数に含まれてしまうので注意が必要。また論理削除する場合も同様に削除データが件数に含まれる。履歴を持つ、または論理削除する設計の場合で、カウンタ機能が必要な場合は自前のカウンタが必要な気がしている。



日曜日, 10月 18, 2009

【雑記】 そろそろMVCモデルについて一言いっておくか このエントリーを含むはてなブックマーク


 なーんて、MVCを語れるほどの知識はないのだが、琴線に触れてしまったので、私なりに言いたいことを言うことにする。
 本当は、こんな話より先に、先日参加したGAE Nightの話や、Winnyの金子さんが無罪になった話を書きたいのだけど、ココとか、ココとか、ココとか、ココとか、毎日毎日毎日毎日、MVCを語られると、何かいいたくて、もう我慢できなくなってしまった。(これはエンジニアの性なのか!?)
 中島さんのBlogのなかで最も釣られてしまうキーワードは「えせ」。これを使うということは、自分の考えだけが正しくて、他は間違いであるということを暗にいっているようなもの。多くの人はそれに反応してしまうから、感情論になって、あまりよい結論は見い出せなくなってしまっているんじゃなかろうか。中島さんの言っていることは概ね理解できるし、RESTfulな設計などは私の考えと被る部分もあって、ほぼ同意できるのだが、アーキテクチャーの議論でありがちな不毛な議論に発展しそうになっているところがちょっと悲しいところ。せっかくいいこといっているのにもったいない感じである。
 MVCモデルそのものを含めて、現時点では何が正しいかわからないわけで、むやみに「えせ」という言葉を使うべきではないと私は思うのだが・・・。
 ちなみに、私が考えているReflex設計は、ココココでもいっているように、そもそもオブジェクト設計をしないので、エセMVCモデルの筆頭みたいなものなのであるが、中島さんのいっている「核となるのはビジネスロジックを含んだModelの部分。そこをしっかりと実装し、内部構造を隠す粒度の荒いインターフェイスを定義し、外から何をされてもデータの整合性が壊れない様にすることは何よりも大切」という考えは同じである。
 エセMVCモデルでありながら、考えが同じってどういうこと?と思われると思うので、そのあたりを中心に話をまとめてみたい。
 あらかじめ言っておくと、この手の話は、本当はスルーするのが一番である。

混乱させている曖昧な言葉・・Model

 まず、曖昧な言葉の筆頭は、Modelである。Modelはビジネスロジックを指して使うこともあるが、O/Rマッパで作成したデータを扱うオブジェクトを意味することもある。後者の意味で使う人が多いので、ドメインドリブンな人から「けしからん!」とよく怒られる。
 何事も曖昧なまま強引に話を進めると酷い目にあうのが常なので、まずはModelという言葉の意味の定義が重要だ。私が好きなModelの定義は、先の記事にも書いてあるとおり、「モデリングの成果物」というもの。つまり、オブジェクトを設計していくことをモデリングいい、その成果物であるオブジェクト(=データと振る舞いをもつもの)がModelなのである。モデリングはドメイン、つまり問題領域全般にわたるので、おのずとビジネスロジックのすべてがModelに含まれることになる。なので、本来、Modelには2面性があって、ビジネスロジックを指す意味で使われることもあるし、データという意味で使っても構わないものなのである。

 また、MVCモデルはオブジェクト設計を前提にしたものであるが、多くの人はロバストネス分析をやって分析モデル(BCEモデル)を作ることを省略している。これらは脳内でやっている人がほとんどだろう。BCEモデルは、抽象クラスの「操作」と「属性」により、データはEntity、制御はControlといったように整理できる単純明快なモデルなのだが、これを設計モデルのMVCモデルに落とすときに、データであるEntityと制御であるControlの一部がModelになるため、これがモデラーのセンスを必要とする非常に難しい作業となってしまっている。
 その様子は、以下の図がわかりやすい。(参照:UMLモデリングのノウハウ、最後の秘訣)
 このとき、Modelに記述すべきビジネスロジックが少ないと、「エセMVC」とか、「ドメインモデル貧血症」とかいわれるはめになるわけだ。



 また、BCEモデルにおいては、EntityからEntityを呼ぶことはできないという制約があるが、ModelにはControlの一部が入るため、ModelからModelの呼び出しが可能になる。DDDではそれを前提に設計するため、逆にControllerの責務が曖昧になっている印象を受ける。
 SCAなどのコンポーネントアーキテクチャーではもっと進んでいて、各コンポーネントはreferenceと、serviceという口をもち、それぞれを結合できるようになっている。また、それらを内包した大きなコンポーネントを作ることができる。このアーキテクチャーにはControlerは存在しない。敢えて言うならマッシュアップサービスということになるのだろう。



混乱させている曖昧な言葉・・ビジネスロジック

 次に曖昧だと思う言葉はビジネスロジックである。ビジネスロジック=すべての処理ロジック(あるいはプログラムコード)というふうに理解すると、Controlに書くべきFlowや、Viewに書くべきJavaScriptも含まれることになる。これらは、それぞれのレイヤに存在すべきビジネスロジックである。ちなみに、Modelに書くべきビジネスロジックのことを、私はドメインロジックと呼んでいる。もちろん、ドメイン(問題領域)のオブジェクトに対して、「外から何をされてもデータの整合性が壊れない様にすること」の処理は当然記述することになる。
 FlowやJavaScriptのStep数が大きくなることがあっても、設計上は関係ないはずなのだが、ドメインロジック以外の部分が大きくなることを嫌がる傾向にあるようだ。
 MVCモデルでは、ドメインのすべてをModelに含めることが基本なので、すべてのビジネスロジックまでも、Modelの中に記述しなきゃならないのではないか、という発想をしてしまう。こんな窮屈な発想では、RESTfulで疎結合な設計に合わなくなると思うし、FlowやJavaScriptの利点を軽視してしまうことになって、マッシュアップやHTML5のような「いまどき」のアプリにも合わなくなってしまうと思う。

 前述したDDDやSCAで何がいいたかったかというと、ドメインにビジネスロジックを集約させることにこだわるのであれば、DDDやSCAぐらいの思い切った発想で設計すべきであろう。これに躊躇してしまうということは、後述のビジネスロジックが本当は各レイヤで分断されていて、それを良しとしているからではないか。ビジネスロジックをModelに集約すべきといったって、実際に存在する各レイヤのロジックを無視するわけにはいかず、結局は中途半端になってしまうのが現状。このあたりがModel集約化の話に私が素直に同意できない一番の理由である。

 ということで考えたのが、ココにも書いているような、各レイヤにビジネスロジックをもたせるという設計手法。ただし、ビジネスロジックは、揮発性※であるということが条件となる。また、インターフェース設計は重要で、基本的にCRUDの4つだけを定義して、パラメータはentityそのものとするのがミソ。(※ 揮発性とは永続化しないこと)

entity = blogic(entity);




先日作った請求書アプリを例に具体的に説明しよう。まず、各項目と金額はドメインであり、Entityを検索すれば正しく得ることができる。では、金額とともに表示する「¥」や「,」はどのレイヤのロジックで編集すべきだろうか?また、消費税額計算や合計はどうすればいいだろうか。私のおすすめは、View、もしくは、Controllerのレイヤで記述すること。これらは、上記のblogic関数を使って表現できるもので、わざわざModelでやらなくても各レイヤでやればよいと思う。それは、データの整合性はModelが保証していて、揮発性の上記のblogic関数を使う限りにおいては、整合性を壊すことは決してないからだ。
 
 実際にいくつかのプロジェクトでこれを実践してみたのだが、パフォーマンスや生産性の面で非常に有効という結果を得ることが出来た。パフォーマンスは、高速化プロジェクト その2 で述べているとおりである。リモートにあるModelではなく、ブラウザというローカル環境で実行する部分が増えるわけだから当然といえば当然である。生産性の面では、以下のように、それぞれのレイヤで分業できるという利点がある。このおかげで非常に短納期に開発できた。



ドメインサービスとしてのReflex BDB

 Reflexの新3層アーキテクチャーにおけるドメインは、EntityのCRUD操作以外の何ものでもないのだが、登録変更削除においては、「外から何をされてもデータの整合性が壊れない様にすること」がもちろん保証されなければならないし、検索においては、単なるPKによる検索だけではない点は補足すべきだろう。例えば、PK以外のKeyによる検索、大小比較、全文検索、Pagingといった検索などについては、要求仕様に応じて、それなりにドメインロジックを実装しなければならない。
 まだ公表する段階ではないのだが、Private CloudのソリューションであるReflex BDBの次のバージョンにおいて、単なるKey/Valueの検索ではなく、ドメインサービスとして機能するようなものを作っている。内部的にはBDBなのでKey/Valueではあるが、GAEのProperty Indexのような仕組みを追加することで、汎用的なKeyによる検索などができるようにするつもり。ちなみに、これはConsistent Hashと伝染プロトコルを使ったスケールアウトアーキテクチャーとなっており、Dynamoのようにそれぞれのノードを柔軟に追加削除できる。(予定)
 また、各ノードのアプリはOSGiで管理されており、ドメインに応じたユーザアプリの開発と配布が容易にできるようになっている。(かもしれない)
 ここで強調したいのは、ドメインサービスとすることで、RDBやKey/Valueといった下位のデータ層を隠蔽しているという点。重要なのは結果だけとなるので、O/Rマッピングとか諸々のめんどくさい話は全部ドメインサービスのなかに閉じ込めることができる。
 Reflex BDBは、単なるKey/Value Storageからドメインサービスへの進化をめざしていく。



Key/ValueStorageではなく、ドメインサービスを提供する
・ 単なるPKによるGET/PUTだけでなく、様々なKeyによるRESTfulなCRUD APIを提供
・ <>比較、Range、前方一致検索、全文検索、Pagingなど




(※ 2010/12現在、Reflex BDBは、Reflex Tagging ServiceのPrivate版として開発中。BDBではなくCasandraかHBASEになる予定。GAE版はこちら
2011/3、Reflex BDBは、Oracle Berkeley Edition版としてリリース予定。Tagging Serviceはデータ操作(Resource Operator)としてのI/Fが役割となる。)
2012/5 reflexworks で詳細を発表

やっぱりMVCモデルは進化する

 冒頭、この手の話は、本当はスルーするのが一番であるといったが、最後にそれについて補足する。
 私自身、長年にわたってオブジェクト設計やMVCなどを議論してきて、誰もが納得する解を見出すために、何百人月以上浪費してきた。しかし、コレ!というものは結局見つかっていない。(多くの支持を得ているMVCモデルでさえこの有様である)
 私以外にも、モデリングにコダワリをもつオブジェクト厨も多いと思う。誰もが納得する汎用的な完璧な解を見い出すために現在もなおモデリングを永遠と続けている輩もいる。しかし、完璧な正解はないと思うし、ある程度妥協して完成を見い出ださないと、永遠に時間ばかりが過ぎていく。人生なんてすぐに終わってしまうだろう。特に実際のプロジェクトでは納期が重要。設計に関しては、必要に応じて最適なアーキテクチャーを選択しながら柔軟に考えていけばよいと思う。Bugのない品質のよいものを作り上げるのが一番大事で、次に生産性とか、保守運用性とかの話がくるべき。
 Reflex設計が、そもそもオブジェクト設計をしないというのは、モデリングにかかる膨大な工数を削ることが第一の目的である。Entityだけに着目することで、モデラーの感性の入り込む余地を少なくすると同時に、誰でもサクサク設計できるようにしたい。そのための現実的な設計手法なのである。別に「エセMVC」とか、「貧血症」などと呼んでいただいて結構。でもこのおかげで、モデリングにコダワリをもつ輩も「穀潰し」じゃなくなるわけだから、利用する価値があるかもしれないだろう?

 ということで、MVCモデルは進化する、ってことでいいじゃん、と思う次第である。

<関連>
 MVCモデルは進化する
 Reflex Tagging Serviceについて話します

火曜日, 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さんの実験 スケールアウトの真実
 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。