Blog

#AWSSummit [セッションレポート] : アーキテクチャ道場!(AWS-51)

2022/05/27

セッション概要

優れたアーキテクチャを設計するためのプラクティスを AWS のソリューションアーキテクトと一緒に学びましょう。 このセッションでは、 お題として提示されたシステム要件や技術的な課題に対して AWS のソリューションアーキテクトが実際に設計したアーキテクチャ例を題材に、 アーキテクチャを設計する際の着眼点、良いアーキテクチャが備えている性質、モデリングの手法やデザインパターンなどを議論・解説します。

スピーカ

  • AWS 技術統括本部 チーフテクノロジスト

内海 英⼀郎 さん

  • AWS 技術統括本部 インターネットメディアソリューション本部 ソリューションアーキテクト

奥野 友哉 さん

  • AWS 技術統括本部 インターネットメディアソリューション本部 部⻑ シニアソリューションアーキテクト

⼭﨑 翔太 さん

セッション内容

  • セッション概要にも記載しましたが、流れとしてはお題が与えられ、SAのお二人がそのお題に対するアーキテクチャとなぜそのアーキテクチャにしたのかを理論的に分かりやすく説明するという形になります。

お題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に対応すべきイベント履歴が残る

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-ddb.html

お題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で対応しダッシュボード向けに公開する。

感想

  • なぜ、そのサービスを選んだのか、なぜこのアーキテクチャにしたのかを分かりやすく理論的に説明していただいたことで、終始なるほどなと大変勉強になるセッションでした。