Memcache API

http://code.google.com/appengine/docs/memcache/

はじめに

Memcacheサービスは君のアプリケーションに複数のアプリケーションからアクセス可能でハイパフォーマンスなオンメモリkey-valueキャッシュを提供してくれる。Memcacheは一時データのようにデータストアのような永続性やトランザクションが不要なデータや、高速アクセスのためにデータストアからコピーしておくようなデータに向いている。Memcache APIはDanga Interactiveのmemcachedと同様の機能を備え、互換性も保っている。

Memcache APIを利用すると次の理由からアプリケーションのパフォーマンスがあがり、データストアの負荷が軽減される:

  • データストアへのクエリが劇的に減少する
  • とても人気のあるページでもデータストア割り当ての使用量を削減する
  • 高負荷なクエリや操作の結果をキャッシュする
  • 一時的なカウンタの使用を可能にする

Memcache APIを使うとアプリケーションで一貫性のあるキャッシュを作成できる。キャッシュはアプリケーションのすべてのインスタンスから利用でき、メモリの圧力によって(つまり、キャッシュにたくさんのデータが溜まりすぎたとき)、もしくは開発者によって設定されたポリシーがあればそれに従ってそのデータは破棄される。キャッシュのポリシーはキャッシュ内に保持されるそれぞれのkey-valueペアに対して設定できる。また、キャッシュをすべて破棄したり、データの一部に有効期限を設定することも可能だ。

from google.appengine.api import memcache

# キャッシュに存在しなければ値を追加。1時間でキャッシュは破棄される
memcache.add(key="weather_USA_98105", value="raining", time=3600)

# いくつかの値を設定。これらのキーに対応する値がすでに存在するときは上書きされる
memcache.set_multi({ "USA_98105": "raining",
                     "USA_94105": "foggy",
                     "USA_94043": "sunny" },
                     key_prefix="weather_", time=3600)

# 整数値をアトミックにインクリメントする
memcache.set(key="counter", 0)
memcache.incr("counter")
memcache.incr("counter")
memcache.incr("counter")

Memcacheを使う

Memcacheはハイパフォーマンスな分散メモリオブジェクトキャッシュシステムで、データストアの負荷を軽減して動的なウェブアプリケーションを高速化することを目的としている。データストアへのクエリの結果や、ウェブサイトの一部でレンダリングされたHTMLをキャッシュするのがMemcacheの典型的な使用例になる。

Memcacheパターン

Memcacheのパターンはきわめてシンプルだ:

  • ユーザまたはアプリケーションからクエリを抜き出す
  • クエリを満たすのに必要なデータがmemcache内にあるかどうか確認する
    • データがmemcacheにあれば、それを返す
    • データがmemcacheになければ、クエリをデータストアに投げてその結果をキャッシュに設定する

以下の擬似コードは典型的なmemcacheリクエストを理解してもらうためのものだ:

def get_data():
  data = memcache.get("key")
  if data is not None:
    return data
  else:
    data = self.query_for_data()
    memcache.add("key", data, 60)
    return data
guestbook.pyを改良してMemcacheを使うようにする

Getting Started Guideにあるゲストブックアプリケーションは毎リクエストデータストアにクエリを投げている。このゲストブックアプリケーションを改良して、データストアのクエリに頼る前にmemcacheを使うようにしてみよう。

まずはじめに、memcacheモジュールをインポートして、クエリ実行前にmemcacheを確認するメソッドを作成する。

from google.appengine.api import memcache

def get_greetings(self):
  """get_greetings()
  
  greetingsがキャッシュされているかどうか確認する。
  もしなければrender_greetingsを呼んで、キャッシュを設定する。

  戻り値:
    greetingsを含むHTML文字列
  """
  greetings = memcache.get("greetings")
  if greetings is not None:
    return greetings
  else:
    greetings = self.render_greetings()
    if not memcache.add("greetings", greetings, 10):
      logging.error("Memcache set failed.")
    return greetings

次にページのためのクエリの発行とHTMLの生成を分離する。キャッシュがヒットしないときは、このメソッドを呼んでデータストアにクエリを発行し、HTML文字列を作成した後、それをmemcacheに保存する。

def render_greetings(self):
  """render_greetings()
  
  データストアにgreetingsを問い合わせ、結果をイテレートして
  HTMLを作成する

  戻り値:
    greetingsを含むHTML文字列
  """
  results = db.GqlQuery("SELECT * "
                        "FROM Greeting "
                        "ORDER BY date DESC").fetch(10)
  output = StringIO.StringIO()
  for result in results:
    if result.author:
      output.write("<b>%s</b> wrote:" % result.author.nickname())
    else:
      output.write("An anonymous person wrote:")
    output.write("<blockquote>%s</blockquote>" %
                  cgi.escape(result.content))
  return output.getvalue()

最後にMainPageハンドラを変更してget_greetings()メソッドを呼ぶようにし、キャッシュが何度ヒットもしくはミスしたかという状況も表示する。

import cgi
import datetime
import wsgiref.handlers
import logging
import StringIO

from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.api import memcache

logging.getLogger().setLevel(logging.DEBUG)


class Greeting(db.Model):
  author = db.UserProperty()
  content = db.StringProperty(multiline=True)
  date = db.DateTimeProperty(auto_now_add=True)


class MainPage(webapp.RequestHandler):
  def get(self):
    self.response.out.write("<html><body>")
    greetings = self.get_greetings() 
    stats = memcache.get_stats()
    
    self.response.out.write("<b>Cache Hits:%s</b><br>" % stats['hits'])
    self.response.out.write("<b>Cache Misses:%s</b><br><br>" %
                            stats['misses'])
    self.response.out.write(greetings)
    self.response.out.write("""
          <form action="/sign" method="post">
            <div><textarea name="content" rows="3" cols="60"></textarea></div>
            <div><input type="submit" value="Sign Guestbook"></div>
          </form>
        </body>
      </html>""")

  def get_greetings(self):
    """
        get_greetings()
        greetingsがキャッシュされているかどうか確認する。
        もしなければrender_greetingsを呼んで、キャッシュを設定する。

        戻り値:
           greetingsを含むHTML文字列
    """
    greetings = memcache.get("greetings")
    if greetings is not None:
      return greetings
    else:
      greetings = self.render_greetings()
      if not memcache.add("greetings", greetings, 10):
        logging.error("Memcache set failed.")
      return greetings

  def render_greetings(self):
    """
        render_greetings()
        データストアにgreetingsを問い合わせ、結果をイテレートして
        HTMLを作成する

        戻り値:
           greetingsを含むHTML文字列
    """
    results = db.GqlQuery("SELECT * "
                          "FROM Greeting "
                          "ORDER BY date DESC").fetch(10)
    output = StringIO.StringIO()
    for result in results:
      if result.author:
        output.write("<b>%s</b> wrote:" % result.author.nickname())
      else:
        output.write("An anonymous person wrote:")
      output.write("<blockquote>%s</blockquote>" %
                   cgi.escape(result.content))
    return output.getvalue()  
     
class Guestbook(webapp.RequestHandler):
  def post(self):
    greeting = Greeting()

    if users.get_current_user():
      greeting.author = users.get_current_user()

    greeting.content = self.request.get('content')
    greeting.put()
    self.redirect('/')


application = webapp.WSGIApplication([
  ('/', MainPage),
  ('/sign', Guestbook)
], debug=True)


def main():
  wsgiref.handlers.CGIHandler().run(application)


if __name__ == '__main__':
  main()