このWebサービスは、sinatra on GAE/JRuby という構成で作ってあるのだけど、実は4、5日程度でひととおりの機能が動作するくらいになっていた。
別にGAEバンザイと言いたい訳ではなくて、本題はここから。
GAEって制限が多くあるので、これを回避するのが結構大変。さらに「マケプレ・フラグ」は価格情報を得るためにAmazonのProduct Advertising APIを使っていて、実はこちらにも制限がある。
GAEは30秒以内にレスポンスを返さなくてはいけない
利用者が商品検索して30秒も待ってくれる訳はないので、それは問題にならない(というより30秒も待たせるならGAEに関係なく設計を見直すでしょ)。
問題は、cronで実行するようなバッチ処理も同様の制限があるということ。
「マケプレ・フラグ」では、登録されている全ての商品の価格を定期的をチェックする必要がある。当然30秒で全てチェックできない。
また、希望の割引率を上回ればメールを利用者に送る。1通だけなら問題ないけど、複数の利用者に送ることになるので、これも30秒で終わらない。
対処法
処理を分割してTaskQueueを利用する。
「マケプレ・フラグ」では、大量に価格チェックする処理があるので、商品毎に分割してTaskQueueを使ってみた。
GAE/JRubyでTaskQueueを使うには、cronと同様にURLを叩くだけ。
require 'appengine-apis/labs/taskqueue'
queue = AppEngine::Labs::TaskQueue::Queue.new
task = AppEngine::Labs::TaskQueue::Task.new(:url => '/taskqueue_sample')
queue.add task
これだけで使える。
GAEは負荷がかかっているときは10秒以内に処理しなければいけない
今回Webサービスを作るまで、30秒制限は有名なので知っていたけど、こっちは知らなかった。
"Request was aborted after waiting too long・・・"というエラーが出ている場合は、この制限に引っかかっている。
価格をチェックするためにTaskQueueを使ってAPI呼び出しを大量にしていたら、このエラーが発生していた。TaskQueueは失敗すると、勝手にリトライしてくれるので、動作自体はしているけど、リソースがもったいない。
対処法
10秒以内に処理が終わるように分割するか、負荷をかけないか、の2択を考えて、後者で対応した。
価格チェックはバッチ処理なので利用者のレスポンスには影響ないので、TaskQueueを間隔を空けて実行することにした。
TaskQueueはcountdownという引数で、その秒数分待ってから実行するように指定できる。
queue = AppEngine::Labs::TaskQueue::Queue.new
products = Product.all()
cnt = 0
for product in products
task = AppEngine::Labs::TaskQueue::Task.new(:url => '/taskqueue_sample', :countdown => cnt)
queue.add task
cnt += 3
end
今回は、このように、3秒おきに実行するようにしたら、エラーが出なくなった。
GAEのスピンアップに時間がかかる
もしかすると、GAEをpythonで利用していたり、フレームワークを挟まず利用していれば大丈夫なのかもしれない。
GAEは利用者からのアクセスがないとすぐスピンダウンする。スピンダウン後の初回アクセスはスピンアップの処理に時間がかかる。
今回、sinatra on GAE/JRubyという、JavaVM、Ruby、sinatraという3段構成で作ってしまったため、スピンアップの時間がすごいことになっている。20秒を余裕で超える。
条件が悪いと、スピンアップするだけで、先の30秒制限に引っかかるんじゃないかと思うときもあった。(sinatraでこんな状態なのにRailsで実運用できるのだろうか。)
バッチ処理のときにスピンアップするならともかく、利用者がアクセスしたタイミングでスピンアップされると、ページを表示するまで、20から25秒近く待たせる訳で、自分が利用者ならその時点で帰る。
対処法
重量級のフレームワークを使わないというのも対処法だけど今回はすでに時遅し。
「マケプレ・フラグ」では、仕方無く1、2分おきにcronで何もしないダミーのURLにアクセスすることで、スピンダウンしないようにしている。ただ、この方法はできればやらないで欲しいということらしい。
でも、他に回避策がないので、今のところはこのままで。
GAEの無料リソースだとメールを1分に8通までの送信にしないといけない
GAEを無料リソース内で利用する場合、メール送信は1分に8通までという制限がある。AmazonのAPIは1秒に1回以上のアクセスを多少しても大丈夫だったりするけど、GAEは速攻で例外になるので注意。
対処法
Memcacheに最後に送信した時刻を保持。メールを送信するときに、その時刻を確認して、8秒程度の間隔より短かい場合に待つようにする。
TaskQueueは並列処理なのでMemcacheを使うときに排他制御しておかないといけない。
Memcacheでスピンロックを実装してTask Queue処理結果を集約してみるテスト
排他制御は上記サイトのようにmutexを実装したacquireLockと、releaseLockを利用。
acquireLock('mail_lock')
last_send = memcache.get('last_sendmail')
if not last_send.nil? then
sleep(8 - (Time.now - last_send))
end
memcache.set('last_sendmail', Time.now, 8)
releaseLock('mail_lock')
こんな感じでMemcacheの有効期間を8秒でセットして、Memcacheに時刻が保持されていた場合に、その時刻から8秒過ぎるまで待つようにした。
Product Advertising APIは、1秒に1回以上リクエストしてはいけない
APIを呼びだしたら、1秒待つように作ればいいという訳じゃない。Webサービス全体で1秒に1回以上リクエストしてはいけないというところがポイント。
「マケプレ・フラグ」では、利用者が商品検索する、商品ページを表示するときやcronで定期的に価格をチェックするときにAPIを呼び出す。
例えば違うユーザが同時に商品を検索すれば、1秒に1回の制限に引っかかる可能性がある。cronで商品の価格をチェックしている最中にユーザがページにアクセスすれば、かなりの確率で引っかかるだろう。
対処法
Memcacheに最後にAPIでアクセスした時刻を保持。1秒以内の連続呼び出しの場合に待つようにする。
これは上記のメール送信と同様の仕組みなので以下省略。
まとめ
このように、結局、回避する設計を考えたり、変更するのに、さらに4日程度かかった。
GAEは簡単にサービスを作れるのだけど、こういう制約を回避しつつ、ユーザが不便にならないように設計するのは、実は大変という、そういう話。
これらの制限の内、解消される予定のものがあるので、期待している。
Google App Engineのロードマップ。半年以内に30秒制限もスピンアップ待ちも撤廃?