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:
では memcached.py のコードリーディング。 (説明のため、一部コードの割愛、記述位置を変更しています。)
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 の基本的な使い方は、
- cache のインデックスの有無を確認する。
- インデックスが見つかったら、 cache データの取り出しを試みる。
- 見つからなかった場合は、データを生成して cache に保存する。
の繰り返しです。 実際には、ロードバランスやデータの共有を考慮しなければいけません。 memcached.py ではこれらをラップしてくれます。
まず、 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 を返します。
次に、 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 の 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'
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
