概要
- 2022年5月25日~26日にAWS Summit Online 2022が開催されました。
- こちらのカンファレンス、現在も登録していただくと各セッションオンデマンド配信をしているので、ぜひ気になれば登録しちゃいましょう!
- そんなAWS Summit Online 2022のセッションの一つ、「アーキテクチャ道場!(AWS-51)」のセッションレポート及び感想を今回はご紹介していきたいと思います。
セッション概要
- 今回記事にしていく**アーキテクチャ道場!(AWS-51)**のセッションでは下記のような内容を扱ったものとなっております。
優れたアーキテクチャを設計するためのプラクティスを AWS のソリューションアーキテクトと一緒に学びましょう。 このセッションでは、 お題として提示されたシステム要件や技術的な課題に対して AWS のソリューションアーキテクトが実際に設計したアーキテクチャ例を題材に、 アーキテクチャを設計する際の着眼点、良いアーキテクチャが備えている性質、モデリングの手法やデザインパターンなどを議論・解説します。
※ セッション概要より引用
スピーカ
-
スピーカは、下記の3名の方がされております。
- AWS 技術統括本部 チーフテクノロジスト 内海 英⼀郎 さん
- AWS 技術統括本部 インターネットメディアソリューション本部 ソリューションアーキテクト 奥野 友哉 さん
- AWS 技術統括本部 インターネットメディアソリューション本部 部⻑ シニアソリューションアーキテクト ⼭﨑 翔太 さん
セッション内容
- 流れとしてはお題が与えられ、SAのお二人がそのお題に対するアーキテクチャとなぜそのアーキテクチャにしたのかを理論的に分かりやすく説明するという形になります。
お題1
- 下記がお題1の内容です。
お題1
オンラインセール
アパレル EC サイトを運営する X 社のオンラインセールは⼈気ブランドのスニーカーが数量限定で出品されることで有名です。EC サイトへのアクセスは⽬⽟商品の販売開始と同時にスパイクすることが分かっており、在庫管理サービスのスケーラビリティが課題になっています。
そこで X 社は新たに⽬⽟商品専⽤の在庫管理サービスを構築して⼤量のアクセスを捌こうと考えました。あなたはこのサービスをどのように設計しますか︖
- 上記のお題に対して、奥野さんが考えたアーキテクチャの解説へと移ります。
- また、上記だと設計の範囲や方針が定まりずらいということで、ある程度前提となるアーキテクチャがシェアされました。
前提となる設定
- ECサイトはマイクロサービスアーキテクチャで設計されています。
- クライアントからマイクロサービスをコーディネートするためのBFF(backends for Frontends)レイヤーに対しリクエストが送られます。
- BFFの裏には、通常商品⽤在庫管理サービス、⽬⽟商品⽤在庫管理サービス、**既存のカートサービス(カートへの商品の追加/カート内の商品の確認機能を提供)**が存在する
- 今回の設計のポイントは、商品購入リクエストが送られた時のトランザクション処理が対象となります。
処理の流れ
①: クライアントから、商品購入リクエストがBFFに送られる
②: BFFは目玉商品用の在庫管理サービスを呼び、在庫の有無を確認
③: 在庫がある場合は、再度、目玉商品用の在庫管理サービスを呼び在庫の引当を行う
④: 在庫が確保できた場合、カートに追加する
今回のトランザクションは、商品がカートに入ったら完了とします。
NG
- 販売のやり方を変えて抽選にする
- 確保した在庫数を超えて購入させる
- 責任範囲としては、このサービス群の⽬⽟商品⽤在庫管理サービスを考えるというお題
解説
- はじめの設計としては、下記の構成を紹介されていました。
NLB + ECS Fargate(コンテナ上にNginx) + NLB + ECS Fargate(コンテナ上に在庫マネージャアプリ) + DynamoDB(在庫数を管理)
-
前段のNginxではBFFのスロットリングや、必要であればBFFの認証機能用として利用します。
-
DynamoDBのテーブルには、商品ごとにストックがあります。
-
在庫確認処理 : BFFより別のサービスより取得したProduct Idをもとにこのサービスで作成したAPIを使用して、Getのリクエストを投げます。
DynamoDBでは、Product IdをPartition keyにしたレコードを保持っています。
Getのリクエストの処理としては、Product IdをもとにDynamoDBにGetItemをして、在庫の数字があれば在庫がありを返します。 -
在庫引当処理 : BFFより商品のIDと希望している購入数を作成したAPIのPOSTでリクエストします。
-
在庫を減らすときの処理 : UpdateExpressionやConditionExpressionを使用します。在庫を指定の個数減らすが、在庫が指定の個数を下回る場合はエラーを返却します。
-
カートへの商品追加処理: 在庫が確保できたら商品追加のリクエストを送ります。
問題点
問題点1
- 特定の Item に Read/Update が集中するため、Partition ごとに存在します。
- なので、3,000 RCUおよび 1,000 WCUの上限に当たる可能性があります。
=> 書き込み/読み込みをどのようにスケールさせるかを検討する必要がある。
問題点を考慮した設計
- いやぁ色々考えられていてすごいです。。
NLB + ECS Fargate(コンテナ上にNginx) + NLB + ECS Fargate(コンテナ上に在庫マネージャアプリ)
- ここまでは、最初の設計と同じだが、DynamoDBへの書き込み箇所に工夫点があります。
- 在庫を管理するDynamoDBの後続処理として、DynamoDB Streamを用意しLambdaへ流します。
- Lambdaは分割したDynamoDBのテーブルに対して、UpdateをするのだがDaxに対しデータを流し、在庫マネージャからの読み取り時は、Daxから読み取り流れとしています。
問題点への解決策
問題点1
- 書き込みのスケール: シャーディングさせる
=> ただ、シャーディングさせると読み込み時Scanを使ってしまうとコスト効率が悪くなります。なので、テーブル分割させる(在庫がある場合にそのPartition Idのlistを保持するテーブル)ことでScanを回避する作りにしたようです。
- 普通ここくらいまで考慮していれば、よく考えられているなと思うのですが、さすがAWSのSA。ここからさらに分割したテーブルへの読み込みへの集中が発生することを問題視しているようです。
=> じゃあどうするのかということで、 DynamoDB Accelerator (DAX)を使っているようです。
使い方としては、分割したテーブルのデータを複製させスケールさせる。なので、最大10個のリードレプリカが作成できるDAXのクラスタを使う。
新たな問題発生(問題点2): 在庫が0になった時にどのようにして分割したテーブルに反映していくか
問題点2
- 在庫管理マネージャのアプリが在庫を確保したときに、DynamoDB Streamを使用して、在庫状態テーブルをupdateします。
- なので、DnyamoDBからStreamでLambdaに流し、在庫状態の Update に失敗した場合でも、リトライが実⾏でき、それでもダメな場合はDLQに対応すべきイベントを残せます。
- Lambdaの障害時でも DynamoDB Streamsに対応すべきイベント履歴が残ります。
お題2
- 下記がお題2の内容です。
SaaS メータリング経費精算SaaSプロダクトのローンチに向けて準備を進めているY社は課⾦システムの開発に着⼿することにしました。
テナント別に集計した使⽤量や課⾦額はテナント向けダッシュボードに表⽰する予定です。
テナント管理者がリアルタイムに利⽤状況を確認できるよう、テナント向けダッシュボードにはできるだけ最新のデータを
反映しなければなりません。
あなたはこのシステムをどのように設計しますか︖
- 上記のお題に対して、⼭﨑さんが考えたアーキテクチャの解説へと移ります。
前提となる設定
-
SaaSのアプリがあり、そこから今回検討する課金システムに使用量データが送られてきます。
-
課金システムは、テナント別の使⽤量・課⾦額、APIをダッシュボードに公開します。
-
SaaSアプリは機能ごとの異なる料金プランを持っています。(機能Aと機能Bがある)
- 機能A: トランザクションの実⾏数に応じて費⽤が発⽣する料⾦プラン
- 機能B: 利⽤開始から停⽌までの時間に応じて費⽤が発⽣する料⾦プラン
-
課金額の計算は、機能を利用した時点での単価で計算するのではない。ダッシュボードに表示する時点の最新単価を用いります。
解説
- まずすごいなと思ったのが、SaaSアプリと今回設計する課金システムの責任範囲の定義をされていたところでした。(なるほどなと勉強になりました。) => 設計ポリシーを定める。
- 次に、シンプルな設計を考えていました。
- トランザクション課金の機能では、トランザクションごとに同期処理で使用量のデータを課金システム側へ送ります。
- 使用時間で課金する機能では、機能を使い始めたタイミングと終了したタイミングで同期処理でそのイベントを課金システムに送ります。
- 課金システム側では、1つのDBにぶっ込んで、ダッシュボードからのリクエストはそのDBのデータを集計し使用します。
問題点
- 上記のシンプルな設計での課題としては以下のものがあります。
課題①
- トランザクション数の増加に伴い、DBへの書き込み/読み込み負荷が増加してボトルネックになったり、時間課⾦機能の使⽤時間を計算するためにクエリが複雑化することです。
問題点①への解決策
- トランザクション課金の機能では、1分毎に事前集計することによってDBへの書き込み負荷を減らします。
- 使用時間で課金する機能でも、開始時間と終了時間のイベントを送信するのではなく、こちらも1分毎に使用時間を同期処理で報告することによってDBへの読み取り負荷を軽減できます。
課題②
- SaaS アプリケーションや課⾦システムの障害時にメータリングができない/トランザクションイベントが遅延なく到着するとは限りません。
問題点②への解決策
- トランザクション課金については、データの送信の仕方を非同期にする。使用量での課金についても、1分毎にデータ送信を非同期で行うようにします。
- トランザクション課金の1分毎の事前集計では、イベント時間ではなく事前集計するサーバ側の処理時間を利⽤することで遅れてきたイベントについても処理します。
課題③
- 二重計上の問題です。
- トランザクション課金のデータが重複して送信されると⼆重計上されてしまいます。
- 課金システムの内部でもデータベースへの登録で再試⾏が発⽣すると⼆重計上されてしまいます。
問題点③への解決策
- トランザクション課金の処理では、トランザクションIDによる重複排除する機能を加えます。
- データベースのスキーマ設計で既存の使用量データに対して新しく入ってきた使用量を加算するのではなく、集計単位で丸めたタイムスタンプ毎にレコードを作成します。(1分毎)
問題点を考慮した設計
-
こちらも色々考えられていて、かなりやばいです。
-
トランザクション課金機能側: Kinesis Data Streamにトランザクション課金のイベントを流し、lambdaが処理を行い、データストアであるDynamoDBに対し、トランザクション課金のデータを入れていくのだが、ここでDynamoDBテーブルのパーティションキーをトランザクションIDとし、条件付き書き込みにより重複を排除します。
-
トランザクション課金の事前集計には、Kinesis Data Streams + Kinesis Data Analyticsを組み合わせることで実現します。
-
使用時間課金の機能側: こちらもSaasアプリ側に、エージェントを配置し、Kinesis Data Streamsでデータを送ります。後続は、Lambda + RDS Proxy + Auroraを使用します。
-
トランザクション課金と使用量の集計データについては、最終的にデータベースに書き込みを行うのですが、書き込み処理でボトルネックにならないように、前段にRDS Proxyを置きコネクション過多に対応します。
-
最終的な、課⾦額の計算とAPIの公開については、Amazon ECS + ALBで対応しダッシュボード向けに公開します。
感想
- なぜ、そのサービスを選んだのか、なぜこのアーキテクチャにしたのかを分かりやすく理論的に説明していただいたことで、終始なるほどなと大変勉強になるセッションでした。