misc.tech.notes

主に技術的な雑記的な

DynamoDBをキャッシュストアっぽく使うPyPIライブラリを作りました

仕事は一応納まったけど、リモートワークなので忘年会とかは特に無くて、奥さんも体調が良くないので年末年始に備えて寝てしまったので、一人寂しくブログ書いてます(;´Д`)

何はともあれソースなど

github.com

pypi.python.org

背景や動機

Lambdaでアプリケーションを作っていると、外部のSaaSAPIを叩いたりする時にrate limitやレイテンシが気になったり、DynamoDBに重めのクエリを投げると性能だったりキャパシティが気になって結果をキャッシュしておきたかったり、キャッシュストアが欲しくなることがままあります。

なんですが、ElastiCacheはVPC内で使うとENI生成時の遅延が気になるし、せっかく楽がしたくてServerlessなのにマネージドとはいえインスタンスを気にしないといけないのも微妙なところです。となると、Lambdaから手頃に使えるデータストアは結局DynamoDBしかないわけです。(AppEngineならMemcacheがあるから、もうAppEngineで良いんじゃないか?とか思ってしまうw)

じゃあ、もうDynamoDBをキャッシュストアの代わりに使うしかないよねってことで、それっぽいライブラリを探したんですが、見つからなかったので作りました。

インストール

Pythonおよびpipのインストールは省略すると、PyPIなので、これだけです。

pip install ddbc

余談ですが、APIが変わったらしく、PyPIのパッケージのアップロード方法がいつの間にか変わってたんですね。前からあるやつは手元のvirtualenv環境が古いままだからか気が付かなった。。。

準備

IAM権限

以下のようなPolicyを持つIAM Role/Userを使用します。

<cache-table> の所は実際に使用するキャッシュ用のDynamoDBテーブル名が入ります。
<region>:<account-id> の所はお使いの環境に合わせてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "dynamodb:CreateTable",
              "dynamodb:DeleteItem",
              "dynamodb:GetItem",
              "dynamodb:PutItem",
              "dynamodb:DescribeTable"
            ],
            "Resource": "arn:aws:dynamodb:<region>:<account-id>:table/<cache-table>"
        }
    ]
}

キャッシュ用のテーブルを作成

上記の権限を割り当てた環境で以下のようなコードを実行します。既に存在する場合は何もしないので、2回以上実行してしまっても無問題です。

#!/usr/bin/env python

import ddbc.utils

ddbc.utils.create_table(
    table_name='cache_table',
    region='us-east-1', # optional
    read_units=10,      # default: 5
    write_units=10      # default: 5
)

使用方法

キャッシュクライアント生成

import ddbc.cache
import time

cache = ddbc.cache.Client(
    table_name='cache_table',
    region='us-east-1', # optional
    default_ttl=100,    # default: -1 (Infinity)
    report_error=True   # default: False
)

default_ttl がキャッシュデータをセットする際にデフォルトで適用するデータのTTLです。未指定時は無限になります。
report_error はDynamoDB APIのコール時に発生した例外を報告(再スロー)するかどうかです。キャッシュストアなので、一時的なエラーやスロットリングなどの想定されるエラーは無視しても良い場面が多いので、未指定時は例外は握りつぶし、読み込み時は失敗時に None (変更可能)、書き込み系の操作なら結果を True (成功) / False (失敗) で返します。

キャッシュデータ操作

ざっとこんな感じです。pylibmc をなんとなく意識した感じで dict っぽく使えます。

cache['foo'] = 'bar'
print(cache['foo']) # => 'bar'

time.sleep(100)
print(cache['foo']) # => None

cache.set('foo', 'bar', 1000)
time.sleep(100)
print(cache['foo']) # => 'bar'

del cache['foo']
print(cache.get('foo')) # => None
print(cache.get('foo', 'buz')) # => 'buz'

補足

ちなみに、DynamoDBへのリクエストも減らすためにメモリ上にもキャッシュを持ちます。書き込みは毎回リクエストが発生しますが、読み込みはメモリ上のキャッシュが有効な間はそこからデータを取得します。なので、Lambdaもコンテナの再利用がされている場合はメモリにキャッシュ済みの有効期限内のデータを使うケースではDynamoDBへのリクエストは飛びません。

また、キー範囲が偏ると性能が出ないDynamoDBに配慮して一応キーはハッシュ化して分散するようにしています。

現状、有効期限が切れたデータは削除する際のスループット消費の方が気になるので、消されずにそのまま放置されます。

あと、1つのキーに対するデータは pickleシリアライズした文字列長でDynamoDBの制限である400KBまでしか入りません。

docs.aws.amazon.com

最後に

Lambda上でのオンライン処理で使えるデータストアが実質DynamoDBくらいしかない現状では、それなりに便利かと思うので良かったら使ってみてください。

AWSさんには、Serverlessなキャッシュストア的な新サービスと、SimpleDBの再興を切に願うばかりです。来年は是非おなしゃす。

それでは、良いお年を。