2011-09-24

Google App Engine の backends インスタンスをデータベース的に使おうとしてみた

ウィルスとかフィッシングとかにやられる奴ってなんなの? ひっかかる努力をしないと、ひっかからないだろあんなもん、と思っていた時期が私にもありました。まんまとフィッシングに引っかかった情弱です。

現実を見たくなかったので、Google App Engine の backends インスタンスを、DB サーバ的に使えないかなぁと、試してみました。単純なケースでは、パフォーマンス的にも、コスト的にもあまりメリットがないです。

ソースコードは https://github.com/torufurukawa/backendtest に置いてあります。

コード

backends.yaml
backends:
- name: memdb
  start: memdb.py
「memdb」という名前で識別するインスタンスで、起動時には memdb.py を見るように設定します。

memdb.py
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
DATA = {}
class InitializeHandler(webapp.RequestHandler):
    def get(self):
        global DATA
        DATA['foo'] = 'foo'
application = webapp.WSGIApplication([('/_ah/start', InitializeHandler)], debug=True)
util.run_wsgi_app(application)

memdb インスタンスが起動するときに呼ばれる /_ah/start に対してハンドラを設定します。ここでは動作確認用に、DATA 辞書をちょっといじっています。

memdb モジュールの DATA 辞書を、 Key-Value ストアとして使うことにします。

まずデータを書き込むとき用のハンドラ群。

main.py (抜粋)

MEMDB_BACKEND_ID='memdb'
MEMDB_HOSTNAME=backends.get_hostname(MEMDB_BACKEND_ID)
DATA='spam'*10 
def stop_watch(op_name):
    """ロギングと共通レスポンス用のデコレータ。
    関数を実行して、実行時間をログとレスポンスに書き出す。
    """
    def outer(func):
        def wrapper(self):
            start_at=time.time()
            func(self)
            end_at=time.time()
            log='[%s] %s'%(op_name,end_at-start_at)
            logging.info(log)
            self.response.out.write(log)
        return wrapper
    return outer

class BackendWriteHandler(webapp.RequestHandler):
    @stop_watch('backend:write')
    def get(self):
        hostname=MEMDB_HOSTNAME
        response=fetch('http://%s/memdb/set/%s/%s'%(hostname,get_key_name(),DATA))
class MemdbSetHandler(webapp.RequestHandler):
    """/memdb/set/(.+)?/(.+) で呼ばれるハンドラ"""
    d ef get(self,key,value):
        importmemdb
        memdb.DATA[key]=value
        self.response.out.write(value)
def get_key_name():
    return''.join([random.choice('abcdefghijklmnopqrstuvwzyz')for i inrange(10)])


外部から、通常のインスタンス上 の BackendWriteHandler の get が呼ばれます。ランダムに作ったキーと定数値を指定して、memdb インスタンスの /memdb/set/<key>/<value> を GET します。

memdb インスタンスのハンドラが、MemdbSethandler です。モジュールグローバルの DATA 辞書を、key と value で更新します。これで memdb 上の値の更新ができます。

続いて、呼び出しです。main.py から抜粋。

class BackendReadHandler(webapp.RequestHandler):
    @stop_watch('backend:read')
    def get(self):
        hostname = MEMDB_HOSTNAME
        response = fetch('http://%s/memdb/get/%s' % (hostname, get_key_name()))
        data = response.content
class MemdbGetHandler(webapp.RequestHandler):
    """/memdb/set/(.+)?/(.+) で呼ばれるハンドラ"""
    def get(self, key):
        import memdb
        value = memdb.DATA.get(key)
        self.response.out.write(value)
外部からアプリの通常のインスタンスへは、BackendReadHandler が呼ばれます。memdb インスタンスに対して /memdb/get/<key> を呼び出します。

memdb のハンドラは MemdbGetHandler で、DATA 辞書から値を取り出して返す、というものです。

実行してみた

Datastore、Memcache、backends で読み書きをしてみました。きちんとやってなくてさーせん。なんどかブラウザからちょこちょこ URL にアクセスして、安定していたあたりの10件の平均時間 [ミリ秒] と標準偏差です。

Storage     Write     Read
Datastore     14±1     4±0
Mecache     2±0     2±0
Backend     49±83     87±88

かなり遅いのと、所要時間がえらく不確定です。アクセスの感覚もゆっくりとったので、Datastore よりも早いんじゃないのかくらいの期待をしていたのですが、ぜんぜんです。

コストのほうを計算してみました。

100ミリ秒(0.1秒)ごとに読み書きのいずれかが発生するとしましょう。楽観的に考えて、スパイクはなし、と。
そうすると、1ヶ月での読み書き回数は、
0.1 [秒] x 3600 [秒/時間] x 24 [時間/日] x 30 [日/月] = 25,920,000 [回/月]

デフォルトの B2 クラスのインスタンスの1ヶ月の使用料は、
0.16 [ドル/時間] x 24 [時間/日] x 30 [日/月] = 115 [ドル/月]
です。

同じ回数のアクセスを、Datastore に対して行うと、
書き込みのみ費用 = 0.01 [ドル/10k回] x 25,920,000 [回/月] = 26 [ドル/月]
読み込みのみ費用 = 0.07 [ドル/10k回] x 25,920,000 [回/月] = 181 [ドル/月]

読み込みだけ発生すれば、backends のほうが安いです。が、実際にはそんなわけないですしねぇ。うーむ。

とは言え…

Datastore から複数のエンティティを取得すると、その分読み込み回数のカウントが増えます。アプリによっては、それを backendsではうまくハンドルできるかも知れません。memcache のデータは揮発する可能性がありますが、backends はメモリに気をつけていれば datastore ほどではないにしてもなんちゃって永続化できます(そのあたりの監視や処理にCPU時間が必要でしょうけど)。

なので、この使い方がすなわちダメではないんでしょうが、個人的にもっとあからさまな速度差や、コスト差が出るのかなぁと妄想していたので、少しざんねんです。今日は残念な日なのでしょう。