View All Posts. MiCHiLU.com powered by Django ;-)

[Django][Python]: Django で memcached を使う

memcached は "super-lightning-fast interface!!" (超稲妻迅い)と評判のインタフェースを提供する、メモリベースのシンプルなキャッシュフレームワークです。 今回はこの memcached の Python バインディングである memcached.py [1] (version 1.34) と Django での設定例をほんの少し。

[1]http://danga.com/memcached/apis.bml - Python API

memcached は非常にシンプルで、 ちょうど Python の Dictionary のようなデータ構造に、生存期間を足したようなものです。 ロードバランスやフェールオーバー、cacheの共有といった、複数の cache service を連携して動作させるためのコントロールは、 memcached そのものではなくそれより上のレイヤーで実装されるようです。 Python では memcached.py がその役割を担っています。

INDEX:

  1. memcached.Client.__init__()
  2. memcached.Client.set()
  3. memcached.Client.get()
  4. django.core.cache.backends.memcached

では memcached.py のコードリーディング。 (説明のため、一部コードの割愛、記述位置を変更しています。)

memcached.Client.__init__()

memcached.py

class Client:
    ...

    def __init__(self, servers, debug=0):
        self.set_servers(servers)
        self.debug = debug
        self.stats = {}

    def set_servers(self, servers):
        self.servers = [_Host(s, self.debuglog) for s in servers]
        self._init_buckets()

    def _init_buckets(self):
        self.buckets = []
        for server in self.servers:
            for i in range(server.weight):
                self.buckets.append(server)
>>> import memcache
>>> mc = memcache.Client(['127.0.0.1:11211'])

というように、 (cache_server_ip:port, weight) のリストを引数に取ってインスタンス化します。 port=11211, weight=1 がデフォルト値です。 インスタンス化の際に class _Host によって cache service を提供しているサーバリストが作成されます。 サーバスペックに応じて weight を設定することが可能ですが、 weight の数だけサーバリストに追加し、余分な分だけ選択される確率が高くなる、という方法を取っています。

cache service に対応したインスタンスを作成したので、これに対していろいろと操作をしていきます。 memcached の基本的な使い方は、

  1. cache のインデックスの有無を確認する。
  2. インデックスが見つかったら、 cache データの取り出しを試みる。
  3. 見つからなかった場合は、データを生成して cache に保存する。

の繰り返しです。 実際には、ロードバランスやデータの共有を考慮しなければいけません。 memcached.py ではこれらをラップしてくれます。

memcached.Client.set()

まず、 cache データの格納です。 Client.set() メソッドがデータを格納しますが、単に Client._set() が呼ばれています。

memcached.py

class Client:
    ...

    def _set(self, cmd, key, val, time):
        check_key(key)
        server, key = self._get_server(key)
        if not server:
            return 0

        self._statlog(cmd)

        flags = 0
        if isinstance(val, types.StringTypes):
            pass
        elif isinstance(val, int):
            flags |= Client._FLAG_INTEGER
            val = "%d" % val
        elif isinstance(val, long):
            flags |= Client._FLAG_LONG
            val = "%d" % val
        else:
            flags |= Client._FLAG_PICKLE
            val = pickle.dumps(val, 2)

        fullcmd = "%s %s %d %d %d\r\n%s" % (cmd, key, flags, time, len(val), val)
        try:
            server.send_cmd(fullcmd)
            server.expect("STORED")
        except socket.error, msg:
            server.mark_dead(msg[1])
            return 0
        return 1

引数 time (cache_timeout) のデフォルト値は "0" です。 引数 key は check_key(key) で文字列の長さがチェックされます。 デフォルトでは SERVER_MAX_KEY_LENGTH=250 です。 超える場合は、例外 Client.MemcachedKeyLengthError が送出されます。 char code もチェックされます。 ord(char) < 33 となる文字が含まれる場合は、例外 Client.MemcachedKeyCharacterError が送出されます。 格納データはその型をチェックし、型に応じたフラグが立てられて文字列化されます。 dict などのオブジェクトは pickle (可能ならば cPickle ) で dump されます。

_get_server(key) では、 key の値から 対応するデータが格納する(格納されている)サーバを算出します。

memcached.py

class Client:
    ...

    def _get_server(self, key):
        if type(key) == types.TupleType:
            serverhash, key = key
        else:
            serverhash = hash(key)

        for i in range(Client._SERVER_RETRIES):
            server = self.buckets[serverhash % len(self.buckets)]
            if server.connect():
                return server, key
            serverhash = hash(str(serverhash) + str(i))
        return None, None

同時に server.connect() で cache サーバに対する soket を確立します。 既に soket が確立されている場合は再利用されます。 接続要求が失敗した場合はハッシュが計算し直され、新たに選択されたサーバへ要求を試みます。 soket の確立に失敗したサーバにはフラグが設定され、検出されてから _Host._DEAD_RETRY (30秒) の間は soket 接続要求の対象にはなりません。 選択可能なサーバが無くなってしまった場合には None を返します。

memcached.Client.get()

次に、 cache データの取り出しです。

memcached.py

class Client:
    ...

    def get(self, key):

        check_key(key)
        server, key = self._get_server(key)
        if not server:
            return None

        self._statlog('get')

        try:
            server.send_cmd("get %s" % key)
            rkey, flags, rlen, = self._expectvalue(server)
            if not rkey:
                return None
            value = self._recv_value(server, flags, rlen)
            server.expect("END")
        except (_Error, socket.error), msg:
            if type(msg) is types.TupleType:
                msg = msg[1]
            server.mark_dead(msg)
            return None
        return value

get() メソッドは単一の key (cache インデックス) に対応する value (cache データ)を返します。 key リストの引数に対して value リストを返す get_multi() メソッドもあります。 server.send_cmd("get %s" % key) でデータ取得のリクエストが行われた後、 _expectvalue(server) で cache header を取得し、 インデックスの有無の確認とデータ型のフラグを取り出します。 _recv_value(server, flags, rlen) で格納前のデータを復元します。

memcached は cache_timeout=0 とすると cache を expire しないので、使い方によっては Client.flush_all(), Client.delete() といったメソッドを使用します。 また、 値のオートインクリメント/デクリメントを行う Client.incr(), Client.decr() があります。

ここまでで memcached.py は終わりです。

django.core.cache.backends.memcached

Django の django.core.cache.backends.memcached.CacheClass をみてみます。

django.core.cache.backends.memcached

from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError

try:
    import memcache
except ImportError:
    raise InvalidCacheBackendError, "Memcached cache backend requires the 'memcache' library"

class CacheClass(BaseCache):
    def __init__(self, server, params):
        BaseCache.__init__(self, params)
        self._cache = memcache.Client(server.split(';'))

    def get(self, key, default=None):
        val = self._cache.get(key)
        if val is None:
            return default
        else:
            return val

    def set(self, key, value, timeout=0):
        self._cache.set(key, value, timeout or self.default_timeout)

    def delete(self, key):
        self._cache.delete(key)

    def get_many(self, keys):
        return self._cache.get_multi(keys)

CacheClass 自体は、ライブラリ memcached.py のおかげで非常にシンプルです。 django.core.cache による cache_timeout のデフォルト値は300秒です。

最後に Django での settings.CACHE_BACKEND の設定例です。 Django では ライブラリ memcached.py の weight をサポートしていないので weight の分だけ繰り返し記述します。

myproject/settings.py

CACHE_BACKEND = 'memcached://127.0.0.1;127.0.0.1:11212;127.0.0.1:11213;127.0.0.1:11213'
see:

Django サーバと memcached サーバを N対N で共有する場合は、 Django サーバ間で settings.CACHE_BACKEND が完全に一致していないと効率的に cache を共有できません。 また、 memcached サーバの host 数が変動するタイミングでは、 cache ヒット率が大幅に下がる可能性があります。

おまけ。 memcached は "-vv" オプションで client からの commands と reponses を出力します。 Django からアクセスした際のサンプルです。

$ memcached -vv
...

<3 server listening
<6 new client connection
<6 get views.decorators.cache.cache_header../
>6 END
<6 set views.decorators.cache.cache_header../ 1 900 6
>6 STORED
<7 get views.decorators.cache.cache_header../
>7 sending key views.decorators.cache.cache_header../
>7 END
...

<3 server listening
<6 new client connection
<6 set views.decorators.cache.cache_page../.d41d8cd98f00b204e9800998ecf8427e 1 900 23182
>6 STORED
<6 set views.decorators.cache.cache_page../.d41d8cd98f00b204e9800998ecf8427e 1 10 23212
>6 STORED
<6 get views.decorators.cache.cache_page../.d41d8cd98f00b204e9800998ecf8427e
>6 sending key views.decorators.cache.cache_page../.d41d8cd98f00b204e9800998ecf8427e
>6 END
Tue, 27 Feb 2007 15:29:22 +0900 source edit
Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 2.1 Japan License.
View All Posts. MiCHiLU.com powered by Django ;-)