🐥

(翻訳記事)サーバーレスの課題: キャッシュアサイド パターンとして Momento を使用してスケーリングは実現可能ですか?

2023/12/16に公開

この記事はAWS Community Builder に掲載された記事の翻訳です。原文はこちらをご参照ください。

https://dev.to/aws-builders/serverless-challenge-is-scaling-achievable-using-momento-as-a-cache-aside-pattern-48cg

執筆された方はこの方です。
https://dev.to/ymwjbxxq

re:Invent2023 のため、この投稿は遅れました。いくつかの発表が私のアーキテクチャ上の決定に影響を与える可能性がありました。

Redis 用の Amazon ElastiCache Serverless について話します。AWS はそのサービスで私たちに力を与えてくれます。この Redis サーバーレス バージョンはその証拠です。
これは部分的なサーバーレス ソリューションかもしれませんが、より効率的でシームレスなエクスペリエンスへの一歩となります。

発表内容を一目見て、次のことに気づきました。
・基本料金 90 ドル/月/GB + 0.34 セント/GB 転送データ
・10 分ごとに自動スケーリングする機能だけでは、トラフィックの急増には不十分
・VPC内部で動作する

情報を確認した結果、Momento がより良い選択であることがわかりました。 Momento は 2022 年 11 月に導入され、私はそれを注意深くフォローしていました。私のアプリケーションでは、DynamoDB や S3 などのダウンストリーム ソースを保護するために、ほぼすべてのものをキャッシュする必要があります。

私は Momentoに連絡を取りキャッシュの必要性を説明し、特に 500KB を超えて 2MB までのアイテム サイズをキャッシュする必要がある場合の具体的なケースを検討しました。
S3Redis を使用した前回のテスト中に、できる限り最小限に抑えたいと考えていたスケーラビリティの問題に遭遇しました。

私の負荷テストは常に同じです:
・アイテムは CDN に 3 分間キャッシュされる
・試験時間 2時間
・総リクエスト数 ~200M
・RPS > 10K 最大 30K
各リクエストは最大 20 または 30 までの N キーをロードでき、それらの参照は DynamoDB に保存されます。 APIGW で 10,000 RPS に達すると、200,000 または 300,000 RPS でキャッシュにヒットし、30,000 RPS のトラフィックではほぼ 100 万 RPS に達する可能性があります。

ここでは、最大 1MB のサイズの生オブジェクトが最近急増しています (圧縮すると約 100KB になります)。


私は不具合なく驚くべき高みを達成し、エラー数をゼロにまで減らし、Momento を常に信頼できる優れたパートナーに選定しました。

テスト結果は非常に明らかで、次の 2 つの領域で大幅な改善が見られました:
・スピード
・コスト

スピード
最初は予期せぬ速度の問題が発生し、キャッシュ サイズとクロスリージョン アクセスのせいで結果は予想よりも遅くなりました。このアクセスにより各リクエストに 20 ミリ秒が追加されましたが、同じリージョン内にある場合は削除できました。

ペイロード サイズは大きな課題であり、圧縮が解決策であることはわかっていました。最初の Rust 実装は失敗しましたが、Momento チームが専門知識を活かして SDK にデータ圧縮を実装し、ソリューションをさらに効果的にしました。
https://youtu.be/Bxbeb478bhI

Momento が Zstandard アルゴリズムを専門的に使用した結果、速度を犠牲にすることなくデータが 90% 圧縮されました。レイテンシは VPC 内に Redis を配置する場合と同等であり、データ処理がより高速かつ効率的になります。

圧縮を使用してキーのバッチを並行して SET/GET する Rust のコードは次のようになります。

use crate::dtos::page_item_response::CacheItem;
use async_trait::async_trait;
use momento::response::Get;
use std::sync::Arc;
use tracing::{error, warn};
use typed_builder::TypedBuilder;

#[cfg(test)]
use mockall::{automock, predicate::*};

#[cfg_attr(test, automock)]
#[async_trait]
pub trait CacheService {
    async fn get_by_keys(&self, keys:&[String]) -> anyhow::Result<Vec<CacheItem>>;
    async fn set_by_keys(&self, items: &[CacheItem]) -> anyhow::Result<()>;
}

#[derive(TypedBuilder)]
pub struct Momento {
    cache_name: Arc<String>,
    client: Arc<momento::SimpleCacheClient>,
}

#[async_trait]
impl CacheService for Momento {
    async fn get_by_keys(&self, keys: &[String]) -> anyhow::Result<Vec<CacheItem>> {
        let tasks = keys.iter().map(|key| {
            let key: Arc<String> = Arc::from(key.clone());
            let mut shared_client = (*self.client).clone();
            let cache_name = self.cache_name.clone();

            tokio::spawn(async move {
                let response = shared_client
                    .get_with_decompression(&cache_name, key.as_bytes())
                    .await
                    .ok();

                match response {
                    Some(Get::Hit { value }) => {
                        let value: String = value.try_into().ok()?;
                        serde_json::from_str::<CacheItem>(&value).ok()
                    }
                    Some(Get::Miss) => {
                        warn!("MISSING CACHE KEY: {}", key);
                        None
                    }
                    None => {
                        error!("MOMENTO get is null {}", key);
                        None
                    }
                }
            })
        });

        Ok(futures::future::join_all(tasks)
            .await
            .into_iter()
            .filter_map(|result| result.ok().flatten())
            .collect())
    }

    async fn set_by_keys(&self, items: &[CacheItem]) -> anyhow::Result<()> {
        let tasks = items.iter().map(|item| {
            let item_cloned = item.clone();
            let mut shared_client = (*self.client).clone();
            let cache_name = self.cache_name.clone();

            tokio::spawn(async move {
                let cache_item = CacheItem::builder()
                    .value(item_cloned.value)
                    .id(item_cloned.id)
                    .cache_key(item_cloned.cache_key)
                    .build();
                let json = serde_json::to_string(&cache_item).unwrap();
                shared_client
                    .set_with_compression(&cache_name, cache_item.cache_key.clone(), json, None)
                    .await
                    .map_err(|e| error!("ERROR - Momento - set_object {:?}", e))
                    .ok();
            })
        });

        futures::future::join_all(tasks).await;
        Ok(())
    }
}

コスト
Momento の価格設定はシンプルです。
転送料金は 1 GB あたり 0.50 ドル。
1 秒あたり最大 100 万回以上の操作を許可 (プロファイルに基づく)。

Redis クラスターの NetworkBytesOut メトリクスを月単位で集計すると、おおよそのコストがわかります。

XTB: $0.50/GB * X,000GB = $xxx

アクティブ圧縮オプションを考慮すると、価格は約 90% 削減できます。

Redis を使用して同様の負荷のコストを計算するのは簡単ではありません。 VPC 内で操作する場合は、NAT ゲートウェイのデータ処理や、インターネットへのデータ転送 (存在する場合) など、さらに考慮すべき事項があります。

コストの比較は、同一のものを比較するほど単純ではないことに注意することが重要です。結論を出す前に、Momento に連絡することをお勧めします。これらはワークフローの大幅な割引を提供し、コストの競争力を高めることができます。

VPC なしで Lambda で使用できるサーバーレス キャッシュに加えて、Momento Cache システムは、VPC 内で使用できる次のようなさまざまな接続オプションを提供します。

・VPC Peering
・Private Link
・NAT Gateway
・Optional Zone Aware Endpoints

テナンシーの観点から見ると、共有テナンシーと、さまざまな分離レベルの専用テナンシーの両方を提供できます。
https://youtu.be/96BRO89bd9Y

Momento は単なるキャッシュではありません。 Redis がサポートしていないトピックのサポートを提供します。 Redis で pub-sub を使用するには、API ゲートウェイや Web サーバーなどの WebSocket を構成し、認証用のレイヤーを追加する必要があります。ただし、Momento では、これらの機能はすべて単一のプラットフォーム内に構築されています。 Momento Web SDK を使用して、ブラウザでトピックを公開およびサブスクライブできます。データがトピックにパブリッシュされると、すべてのサブスクライバーがデータを受信します。これは、サーバー側のコードを構築せずにブラウザーを接続できることを意味します。

Momento は、強力なサービス、製品化までの極めて長い時間、および信頼性を提供するため、サーバーレス SaaS サービスの真の例です。パワーと市場投入までの時間に関して、Lambda のようなサービスと比較します。

まとめ

圧縮を使用すると、コストを最大 90% 大幅に削減できることがわかりました。 Redis 上で Momento を使用した場合も同様の p99 レイテンシーが発生しましたが、テスト中に達成されたスケーラビリティは印象的でした。さらに、キャッシュ システムによってエラーやダウンストリーム サービスのオーバーフローが発生することはなかったので、自分の選択に自信が持てました。 Momento は、豊富な無料枠、環境間やトラフィックのピーク間の無駄を削減するオンデマンドの価格モデル、インフラストラクチャのセットアップやメンテナンスを必要とせずにすぐに立ち上げて実行できる機能を備えているため、私のワークロードにとって魅力的なソリューションとなっています。

Discussion