はじめに
Serverless Advent Calendar 2017の12日目の投稿です。
ここでは、AWSベースのサーバーレスアーキテクチャで、主にFaaS(Function-as-a-Service)の利用を前提としますが、データキャッシュ(Cache)が必要な場合にどのように実現するのか、その技術解とお金(Cash)の関係について整理してみました。
サーバーレスアーキテクチャにしたは良いけど
サーバーレスアーキテクチャをベースにすることで、処理数に応じてリソースの最適化を図れるので、スモールスタートしやすく、またマイクロサービス化しやすいので、積極的に検討するようにしています。
ただ、よく自社で課題になるのが、データキャッシュに関する処理。
例えば、何かしらのIDを持つイベントを受け取り、それに対して名称を付与するようなケース。AWS Lambdaでデータを処理して、DynamoDBにデータを保存するような場合に、どこで名称のデータを保持するかが問題になります。
DynamoDBは、スループット課金のため、あまり高頻度なRead/Writeをするには向いていません。もちろん、それだけの投資が行えるサービスであれば良いのですが、ID->名称の変換だけで、それだけのコストが発生するとなると、なかなか費用帯効果が得にくいケースが多いのではないでしょうか?
しかも、DynamoDBのRead処理は、レイテンシがそこまで高速ではなく(といっても数十~数百msですが)、システムのパフォーマンスに制約が発生するので、あまりこのような用途には向かないでしょう。
ということで、普通はインメモリでのキャッシュを利用することに至りますよね。
キャッシュパターン
ここでは、以下を前提に考えます。
- 料金は、東京リージョンでのオンデマンドとして計算する。
- データ転送量は除く。
- 1ドル=115円で計算する。
ElastiCache(Redis)
AWSにおけるキャッシュサービスといえば、まずはElastiCacheですよね。ElastiCacheのエンジンは、MemcachedとRedisがサポートされていますが、私はRedis派です。キャッシュとして保持できるデータもHash/List/Setなどの色々な型をサポートしており、対応できる幅が広く、レプリケーションにも対応しているためです。
ElastiCacheを利用する場合は、以下のような要件の場合に向きます。
- 高頻度なWrite/高頻度なRead
- Writeスルー1
ただ、コストはそれなりに高いです。
インスタンスタイプ | スペック | 単位時間コスト | 1ヶ月のコスト |
---|---|---|---|
cache.t2.medium | 2vCPU、3.22GiB | $0.104/h | $76.13(約8,800円) |
cache.m4.xlarge | 4vCPU、14.28GiB | $0.452/h | $330.87(約38,100円) |
さらに注意が必要なのは、上記は 1ノード である、ということです。ElastiCache(Redis)は、Multi-AZでの冗長化構成に対応していますが、追加のノードはレプリカノードとなるので、利用する場合は単純に2倍のコストがかかります。加えて、非同期レプリケーションとなるので、データのロストを避けるために同一AZにもレプリカノードを配置するとなると3~4倍のコストがかかることになります。
DynamoDB Accelerator(DAX)
いつもElastiCacheを使うかどうかで悩むことが多かったのですが、2017年の4月にDAXが発表され、新たな選択肢ができました。
DAXを利用することで、DynamoDBにデータを保持しつつも、数ミリ秒~マイクロ秒での低レイテンシでの処理が実現可能になります。
DAXを利用する場合は、以下のような要件の場合に向きます。
- 高頻度なRead
- Writeスルー1
大事なポイントは、DAXのタイトルにあります。
Amazon DynamoDB Accelerator(DAX) – Read heavyなワークロード向けインメモリ型キャッシュクラスタ
そう、Read heavy であることです。DAXでは、DynamoDBに書き込んだデータを読み込む際にキャッシュされるので、2回目以降のアクセスなどが高速化されます。書き込み自体が高速化されるわけではないので、頻繁にキャッシュを更新するような場合には、書き込みスループットの設定値を上げる必要が出てきてしまいます。
DAXを利用する場合のコストですが、ElastiCache(Redis)よりは1GiBあたりのコストは低くなります。
インスタンスタイプ | スペック | 単位時間コスト | 1ヶ月のコスト |
---|---|---|---|
dax.r3.large | 2vCPU、15.25GiB | $0.322/h | $235.71(約27,100円) |
DAXでも、可用性向上のためには、3ノード以上でのクラスタ構成での運用が推奨されています。
ただ、DAXの場合、データ自体はDynamoDBに保持されているので、DAXノードがダウンしても、データ自体はロストせずに済みます。
また、LambdaはPythonで実装派の自分としては残念なところなのですが、DAXはまだPythonには対応していません。そのため、DAXを利用する場合はJavaかNodeのSDKを利用する必要があります。
Lambda In-Memory
もうひとつは、適用条件は限られますが、Lambdaのインメモリを利用するパターンです。これは、Lambdaのインスタンスは再利用されるため、それを活用する方法です。
以下のような方法で実現しています。
- キャッシュ対象のデータをLambda関数のグローバル変数に保持する。
- インスタンスが新たに立ち上がった場合など、キャッシュがなければ、(DynamoDBやS3などから)データを取得する。
- 一定時間を過ぎている場合、データを再取得する。
Lambdaのインスタンスの制御は、AWS任せなので、自分でコントロールはできません。
そのため、ゆるい一貫性(ある程度の時間を要して、書き込みしたデータがキャッシュに反映される)で問題が無い場合での利用に限定されますが、Lambdaの処理だけでシンプルに実現でき、コストも抑えられるので、条件が合えば強力な選択肢です。実際のプロジェクトで急遽DynamoDBへのアクセス負荷を減らすのに必要になった際に、役に立ちました。
Lambda In-Memory を利用する場合は、以下のような要件の場合に向きます。
- 高頻度なRead
- 呼び出し回数が少ない
- ゆるい一貫性
- キャッシュデータが大きくない
2017/11/30にLambdaの最大メモリ量が、3,008MB(=2.93GiB)に拡張されました(それ以前は、1,536MB)。そのため、2GiB程度までキャッシュが可能そうです(キャッシュを目的にして増えたわけではないでしょうが)。
キャッシュだけのコストを出すのは難しいですが、1回あたりのLambda処理が0.1秒として、1秒あたり10回の呼び出しを行う場合は、以下の程度のコストがかかります。
- 合計コンピューティング(秒/日)= 10回/秒 x 0.1秒 x 86,400秒/日 = 86,400 秒/日
- 合計コンピューティング(GB-秒/日)= 86,400 秒/日 × 1GB = 86,400 GB-秒/日
- 1ヶ月のコンピューティング料金 = 86,400GB-秒/日 x 30.5日 × $0.00001667/秒 = $43.93(5,100円)
もちろん、呼び出し回数が多ければその分コストは大きくなりますが、元々Lambdaで処理する内容であり、呼び出し回数が少ない状況では都合が良さそうです。
おおよそのコードの内容ですが、以下のようにして、定期的にキャッシュが更新されるようにしています。
import datetime
import json
_beacon_dict = {}
_updatetime = 0
def lambda_handler(event, context):
update_beacon_dict()
req_body = json.loads(event['body'])
add_attributes(req_body)
# データの保存
・・・
return {}
def add_attributes(beacon_data):
'''
キャッシュデータを利用して、属性を付与します。
'''
global _beacon_dict
beacon_id = beacon_data['beaconId']
if beacon_id in _beacon_dict:
# キャッシュからデータを取得してパラメータを追加
beacon_data['name'] = _beacon_dict[beacon_id]
def update_beacon_dict():
'''
有効期限をチェックし、キャッシュデータの更新を行います。
'''
global _beacon_dict
global _updatetime
diff_time = 0
if len(_beacon_dict) != 0:
now = datetime.datetime.now()
epoch_now = datetime.datetime_to_epoch(now)
epoch_updatetime = datetime.datetime_to_epoch(_updatetime)
diff_time = epoch_now - epoch_updatetime
if len(_beacon_dict) == 0 or diff_time >= 5*60*1000:
# キャッシュデータの更新
・・・
_beacon_dict = new_dict
_updatetime = datetime.datetime.now()
まとめ
キャッシュのパターンを整理すると、以下のようになります。
※費用は、あくまで目安です。インスタンスタイプやクラスタ構成などの条件で変わります。
方法 | 適用が向くケース | 制限 | 1GiBキャッシュあたりのコスト |
---|---|---|---|
ElastiCache(Redis) | ・高頻度なWrite ・高頻度なRead Writeスルー |
・可用性を上げるなら3ノード以上必要 | 1ノードの場合: 2,600~2,800円 3ノードクラスタの場合: 7,800~8,400円 |
DynamoDB Accelerator(DAX) | ・高頻度なRead ・Writeスルー |
・可用性を上げるなら3ノード以上必要 | 1ノードの場合: 1,800円 3ノードクラスタの場合: 5,400円 |
Lambda | ・高頻度なRead ・呼び出し回数が少ない ・ゆるい一貫性 |
・キャッシュデータ2GiB以下(目安) | 10回実行/秒 100ミリ秒/回 の場合: 5,100円 |
1GiBキャッシュあたりのコストで見ると、Lambdaでもそれなりの単価になりますね。上記はキャッシュする/しないに関わらず、Lambdaの実行にかかるコストのため単純な比較はできないですが、10回実行/秒以下の頻度の呼び出しであれば、メリットがありそうです。
キャッシュの更新や呼出をどう行うかによって、どの方法が良いかは変わりますが、処理の特性とコストを踏まえて、キャッシュの方式を選択できると良いと考えています。