DynamoDBをキャッシュストアっぽく使うPyPIライブラリを作りました
仕事は一応納まったけど、リモートワークなので忘年会とかは特に無くて、奥さんも体調が良くないので年末年始に備えて寝てしまったので、一人寂しくブログ書いてます(;´Д`)
何はともあれソースなど
背景や動機
安西先生・・・Lambdaから気軽に使えるキャッシュストアが・・・ほしいです・・・
— Masashi Terui (@marcy_terui) December 27, 2016
Lambdaでアプリケーションを作っていると、外部のSaaSのAPIを叩いたりする時に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までしか入りません。
最後に
Lambda上でのオンライン処理で使えるデータストアが実質DynamoDBくらいしかない現状では、それなりに便利かと思うので良かったら使ってみてください。
AWSさんには、Serverlessなキャッシュストア的な新サービスと、SimpleDBの再興を切に願うばかりです。来年は是非おなしゃす。
それでは、良いお年を。