Memcache の上限を超えるサイズのデータをキャッシュさせたい

2018年5月3日 20:15

Memcache は GAE(Google App Engine)にて標準で利用できる分散型のインメモリ・データ・キャッシュのこと。ちなみに Memcache 自体は GAE に特化したものではないが、ここでは GAE の Memcache サービス について説明する。

GAE の Memcache は非常に便利で強力だが、キャッシュできるデータ値の最大サイズが 1MB という制限がある。が、バッチ処理で呼び出す場合は合計サイズ 32MB まで大丈夫なので、今回は「データを小分けにしてキャッシュに保存し、読み出し時に元に戻す」というアプローチを考えたわけだ。とはいえ、オブジェクトをそのまま分割するわけにはいかないので、一旦シリアライズしてから……えっと、シリアライズというのは……このへんはちょっと説明が難しいので、pickle モジュールのドキュメント から引用する。

"Pickle 化 (Pickling)" は Python のオブジェクト階層をバイトストリームに変換する過程を指します。"非 Pickle 化 (unpickling)" はその逆の操作で、バイトストリームをオブジェクト階層に戻すように変換します。Pickle 化 (及び非 Pickle 化) は、別名 "直列化 (serialization)" や "整列化 (marshalling)"、 "平坦化 (flattening)" として知られていますが、ここでは混乱を避けるため、用語として "Pickle 化" および "非Pickle 化" を使います。

さて、ツイート中でも「結論から先に言」っているが、ここでも結論から先に言うと、以下のようなコードで実現した。あ、言語は Python 2.7 です。

import pickle
from google.appengine.api import memcache
from app.models import Contact

CHUNK_SIZE = 500000

def fetch_contacts():
    contacts = Contact.query().fetch()
    serialized = pickle.dumps(contacts, 2)
    values = {}
    for i in xrange(0, len(serialized), CHUNK_SIZE):
        values['Contact_{0}'.format(i // CHUNK_SIZE)] = serialized[i : i + CHUNK_SIZE]
    memcache.set_multi(values)
    return contacts

cached_list = memcache.get_multi(['Contact_{0}'.format(i) for i in xrange(20)])
if cached_list:
    try:
        contacts = pickle.loads(''.join([value for key, value in sorted(cached_list.items()) if value is not None]))
    except Exception as e:
        contacts = fetch_contacts()
else:
    contacts = fetch_contacts()

# 以降、contacts の値をよしなに扱う

今回使っているのは pickle 化された文字列を直接扱う pickle.dumps()pickle.loads() の2つ。前者 pickle.dumps() の第2引数の 2 がちょっと分かりにくいかもしれないが、これはプロトコルと呼ばれるもので、数値(バージョン)が大きくなるほどより効率的な pickle 化が行なえる反面、下のプロトコルとの互換性がなくなる。そしてバージョン 3 は Python 3 で導入されたため、今回の Python 2.7 環境ではエラーとなってしまう。よって、2 を指定しているわけだ。