Help us understand the problem. What is going on with this article?

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

More than 3 years have 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スルー:キャッシュに書き込んだデータが、整合性を保って読み取り可能になる。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away