Edited at

AWS Lambdaによるサーバーレス構成でのCacheとCashを考える

More than 1 year has passed since last update.


はじめに

Serverless Advent Calendar 2017の12日目の投稿です。

ここでは、AWSベースのサーバーレスアーキテクチャで、主にFaaS(Function-as-a-Service)の利用を前提としますが、データキャッシュ(Cache)が必要な場合にどのように実現するのか、その技術解とお金(Cash)の関係について整理してみました。


サーバーレスアーキテクチャにしたは良いけど

サーバーレスアーキテクチャをベースにすることで、処理数に応じてリソースの最適化を図れるので、スモールスタートしやすく、またマイクロサービス化しやすいので、積極的に検討するようにしています。

ただ、よく自社で課題になるのが、データキャッシュに関する処理。

例えば、何かしらのIDを持つイベントを受け取り、それに対して名称を付与するようなケース。AWS Lambdaでデータを処理して、DynamoDBにデータを保存するような場合に、どこで名称のデータを保持するかが問題になります。

lambda-cache.png

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で処理する内容であり、呼び出し回数が少ない状況では都合が良さそうです。

おおよそのコードの内容ですが、以下のようにして、定期的にキャッシュが更新されるようにしています。


pytyon

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回実行/秒以下の頻度の呼び出しであれば、メリットがありそうです。

キャッシュの更新や呼出をどう行うかによって、どの方法が良いかは変わりますが、処理の特性とコストを踏まえて、キャッシュの方式を選択できると良いと考えています。





  1. Writeスルー:キャッシュに書き込んだデータが、整合性を保って読み取り可能になる。