re:Invent 2024: AWSによるSaaSストレージ戦略 - マルチテナントデータの最適化
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2024 - SaaS storage strategies: Scaling, securing & tuning multi-tenant data (SAS306)
この動画では、AWSでSaaSアプリケーションを構築する際のマルチテナントデータとストレージに関するベストプラクティスを解説しています。Amazon Aurora、Amazon S3、Amazon DynamoDBなどを活用したデータレイヤーの設計において、セキュリティ、スケーラビリティ、効率性の3つの重要な要素に焦点を当てています。特に、SiloモデルとPoolモデルの違い、Amazon RDS ProxyやAmazon Aurora Limitless Databaseを活用したスケーリング手法、テナントごとのバックアップ戦略など、実践的な知見が豊富に共有されています。また、テナントデータの分離、公平性の確保、効率的なデータモデリングなど、SaaSアーキテクチャ特有の考慮点についても詳しく説明されています。
※ 画像をクリックすると、動画中の該当シーンに遷移します。
re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!
本編
SaaSアーキテクチャの課題と本セッションの概要
それでは始めましょう。私はAWSのPrincipal Solutions ArchitectのRanjith Ramanです。一緒にいるのは、Senior Solutions ArchitectのDave Robertsです。私たちは過去数年間、多くのお客様とAWS上でのSaaS構築に携わってきました。お客様との対話では、スタック全体にわたるマルチテナンシーの実装方法、顧客やテナントのオンボーディングプロセスの最適化、そしてマルチテナント環境におけるビリング、メータリング指標、テナントごとのコスト、コスト配分などについて議論してきました。
最近よく取り上げられるトピックの1つが、マルチテナントデータとストレージに関するベストプラクティス、パターン、ガイダンスについてです。これが今回のセッションの動機となり、マルチテナントデータのスケーリング、セキュリティ確保、チューニングに焦点を当てたベストプラクティスとパターンについて掘り下げていきたいと思います。これは300レベルのセッションですので、アーキテクチャ図やAWSサービスについて説明しますが、コードやIDEの使用には踏み込みません。
SaaSアプリケーションの基本構造と主要な考慮事項
SaaSアプリケーションを構築する際、高レベルで見るとこのような構成になります。エントリーポイントとしてのWebレイヤーがあり、Amazon CloudFrontがAmazon S3バケットから静的コンテンツを配信するようなサービスが配置されます。その後にAPIマネジメントレイヤーがあります。リクエストはAmazon Cognitoで認証を行い、認可を経て、最終的にアプリケーションレイヤーとサービスレイヤーに到達します。そこではコンピュートやデータベース、ストレージのコンポーネントが配置されています。
各レイヤーにはそれぞれの観点、要件、考慮事項があると言えます。例えば、何百何千もの着信メッセージを管理し、適切なテナントとユーザーからのリクエストを識別し、認証を行い、SaaSソリューション内の異なるプランやティアに対してスロットリングを実装する必要があります。コンピュートレイヤーではスケーリング、パーティショニング、ノイジーネイバーの状況に関する考慮事項がありますが、今回は主にAmazon Auroraとリレーショナルデータベースに焦点を当てたデータレイヤーに注目します。ただし、必要に応じてAmazon S3やAmazon DynamoDBのパターンについても触れていきます。
優れたSaaSストレージアーキテクチャを考える上で、セキュリティはほぼ常に最優先される要素です。AWS環境で適用すべき基本的なセキュリティ原則がある一方で、SaaSソリューションでは、そのシステムで実行されているテナントとそのワークロードという観点からセキュリティを見る必要があります。スケーラビリティも重要な要素です。SaaSプロバイダーとして、新規テナントやトラフィックスパイクにどう対応するかを考える必要があります。さらに重要なのは、チームの手動介入なしでシステムがスケールできることを確保することです。
最後の要因は効率性です。SaaSプロバイダーとして、データベースやストレージエンジンを最大限に活用し、運用の手間を最小限に抑えることを考える必要があります。効率性とは、単にパフォーマンスや運用面だけでなく、テナントごとのコスト効率も含まれます。リソースの無駄を防ぎ、システムがアイドル状態にならないようにし、テナントレベルでリソースの利用を最適化する方法を検討する必要があります。
マルチテナントデータベースの基本概念とデプロイメントモデル
これで主要な要因の説明が終わりましたので、基本的な概念とデプロイメントモデルについて見ていきましょう。AWS SaaSチームが長年にわたって提供してきたコンテンツをご覧になった方は、すでにお馴染みかもしれません。 まず、Siloデプロイメントモデルから見ていきましょう。これは、システム内の各テナントに専用のデータベースやインフラストラクチャを割り当てる方式です。 次にPoolモデルがあり、これは複数のテナントが共通のリソースセットを共有する方式です。Bridgeモデルはその中間的な存在で、 SiloとPoolのアプローチを組み合わせたものです。
Siloモデルを見てみると、各テナントに専用のインスタンスとリソースが割り当てられるため、アーキテクチャはシンプルで、それらのインスタンスの周りに自然なセキュリティ境界が作られます。しかし、効率性の観点からは理想的とは言えません。なぜなら、多くのシステムが点在することになるからです。数百、数千のテナントに拡張する場合、同じ数のシステムが必要となり、Siloモデルの管理と保守が難しくなります。Poolモデルに移行すると、共通のリソースセットを共有するため、効率性の面では改善されます。ただし、アーキテクチャ自体は複雑になり、複数のテナントが同じリソースを共有する場合、分離とセキュリティの観点からより課題が生じる可能性があります。
これらのデプロイメントモデルを理解することは重要です。なぜなら、SaaS環境においてテナントのオンボーディングは重要な機能だからです。SaaS管理者がリクエストを行うと、SaaS Control Planeと呼ばれるところに送られます。このControl Planeレイヤーは、SaaSアプリケーション内でオンボーディングを処理し、テナントとそのユーザーのアイデンティティを設定し、そのワークフローのある時点でテナントプロビジョニングサービスを呼び出します。このサービスは、Enterpriseカスタマーに対しては、専用のデータベースや専用のコンピューティングインフラを起動すべきだと判断します。 フリーティアのテナントに対しては、リソースを共有する方が合理的かもしれません。非課金ユーザーに専用のデータベースやストレージを提供するのは現実的ではないかもしれないからです。このように、デプロイメントモデルがテナントオンボーディングプロセスでの意思決定に影響を与えることがわかります。
テナントごとのデータベース、データベースインスタンス、クラスターごと、あるいは共有スキーマなど、異なるデプロイメントモデルがある場合、アプリケーションがこれらの異なるモデル間でリクエストをどのようにルーティングするかを考慮することが重要です。通常、リソースマップまたはマッピングサービスを使用し、そこにテナントコンテキストを渡します。テナントコンテキストとは、リクエストに含まれる情報のことです - すべてのリクエストには、テナントIDのような何らかのテナントコンテキストが含まれているはずです。そのテナントIDを使用して、このマッピングサービスにアクセスし、適切な宛先やデータベースへのルーティングに必要な接続詳細を取得します。
データベースを特定の方法でデプロイしたり、パーティション化したりすれば自動的に分離が実現できると誤解されがちですが、必ずしもそうとは限りません。この例を見てみましょう。各テナントが独自のデータベースインスタンスまたはクラスターを持っていますが、アプリケーション層のマイクロサービスは複数のテナント間で共有されています。もしサービス層でバグが発生し、デバッグ中に誰かがテナントIDをハードコードしてしまった場合、システム内の他のすべてのテナントがそのテナントのデータを見ることができてしまいます。理想的には、AWS Identity and Access Management(IAM)や他のポリシーエンジンを使用してポリシーを適用する、Isolation Contextと呼ばれる追加の保護層が必要です。たとえ誰かがテナントIDをハードコードしたとしても、そのリクエストはデータベースに到達せず、代わりにエラーを返すべきです。これは、何かを特定の方法でデプロイまたはパーティション化しても、求めている分離が自動的に保証されるわけではないことを示しています。
SaaSストレージアーキテクチャにおけるセキュリティとアクセスパターン
さて、コンテキストと基本的な部分について説明したので、先ほど紹介した3つの異なる領域やトピックについて探っていきましょう。これから具体的な考慮事項とトピックを見ていきますが、これが本セッションの残りの時間で扱う内容となります。
まずは、セキュリティとアクセスパターン、そしてアクセスパターンが分離をどのように実現するかについて説明したいと思います。データベース側で分離やパーティショニングを実現するためにできることはいくつかあります。アクセスパターンが重要なのは、特定のユーザーが特定のデータベースにアクセスできるかどうか、適切なポリシーが設定されているか、適切な権限が設定されているかを判断するためです。データベース側でできることは多くありますが、データベースへのアクセス方法についても十分に把握しておく必要があります。
最初のモデルを見てみましょう。これはSilo StorageとSilo Computeを組み合わせたモデルです。このモデルでは、各テナントに専用のAmazon Auroraインスタンスまたはクラスターを割り当てています。コンピュート層では、AWS Lambda関数を使用しています。テナントからリクエストが来ると、Lambda関数は実行ロールとポリシーを持ちます。このポリシーは特定のテナント用に設定されており、関数がテナントのデータベースにアクセスするための一時的なセキュリティ認証情報を返します。テナント2の場合も同様です。独自のLambda実行ロールとポリシーを持ち、それを使用して認証情報を取得してデータベースにアクセスします。
このモデルでは、自然なセキュリティ境界があるため、確実に優れた分離が実現できます。AWS IAM認証を使用していますが、これは非常にうまく機能します。必要な一時的な認証情報を提供し、ネットワークレベルでもうまく動作します。ネットワークセグメンテーションの観点からは、1つのAuroraインスタンスを1つのネットワークセグメントに、もう1つを別のネットワークセグメントに配置することができます。ただし、このモデルの課題は、制限に達してしまうことです。数百、数千のテナントがある場合、これらすべての異なるシステムを管理しなければならず、それが課題となる可能性があります。
では、単一のデータベースインスタンスに切り替えた場合はどうなるでしょうか?分離の特性は前のモデルと非常に似ていますが、ネットワークの分離機能は失われます。前のモデルでは、データベースインスタンスを独自のネットワークセグメントに配置できましたが、このモデルではそれができません。コンピューティングの観点からは、AWS IAM認証を使用して、適切なデータベースにアクセスするための一時的なセキュリティ認証情報を受け取ったり取得したりすることは引き続き可能です。
AWS IAM認証は、同時ユーザー数が少ないサイロ環境では非常にうまく機能すると述べました。しかし、プール環境に移行すると、1つのデータベースに対して1秒あたりに実行できる認証リクエスト数など、AWS IAM認証には明確な上限が設定されています。そのため、このモデルでは、IAM認証の代わりにAWS Secrets Managerを使用します。非常によく似たパターンですが、IAM認証の代わりに、テナントごとにシークレットを持つSecrets Managerを使用します。ポリシーを見ると、特定のテナントに対してSecretsManager.getSecretsValueというアクションをリクエストしています。Secrets Managerはパスワード認証を使用するため、長期の認証情報を返し、Lambda関数はそれを使用して適切なデータベースにアクセスします。テナント2に対しても、非常によく似たアプローチが適用されます。重要なポイントは、サイロアプローチからプールアプローチに移行する場合、認証情報管理にはSecrets Managerの使用を検討すべきということです。
ここまでは、すべてのテナントに対して専用のコンピューティングと専用のAWS Lambda関数を持っていました。
しかし、単一のLambda関数の場合、実行時にどのテナントが呼び出しを行っているかを判断する必要があります。ここで、Token Vending Machine(TVM)と呼ばれるものを使用できます。これは自分で構築する必要のあるモジュールで、実装方法の例を見つけるには「TVM AWS」で検索してみてください。
Token Vending Machineの考え方は、単一のロールを持ち、そのロールにテナントIDのプレースホルダーを含むポリシーを設定するというものです。一番下を見ると、実行時に置き換えられるテナントIDのプレースホルダーがあります。Lambda関数が受け取るのは、リクエストを行っている特定のテナントに対してスコープされた認証情報です。Lambda関数はスコープされた認証情報を使用して、AWS Secrets Managerにアクセスし、適切なシークレットを取得して、データベースにアクセスできます。
このモデルで異なる点は、システム内の各テナントに対して個別のデータベースを持つことです。 Token Vending Machineのプロセスを通じて、各テナントに対してスコープされた認証情報を取得し、Lambda関数がそれを使用してデータベースにアクセスすることは従来通り可能です。ただし、システム内に多数のテナントが存在する場合や、データベース側でユーザー数に制限がある場合は、Lambda関数がSecrets Managerにアクセスし、 単一のシークレットまたはデータベース認証情報を取得して、それを使用してデータベースにアクセスするという解決策を実装できます。このモデルでは、コードレビュープロセスでより厳密な管理を行い、アプリケーションコードやストアドプロシージャを使用している場合はデータベース側で何らかのフィルタリングを確実に実装する必要があります。
SaaSストレージアーキテクチャのスケーリング戦略
これらの基盤を検討してストレージアーキテクチャを作成することは、あくまでも始まりに過ぎません。ストレージアーキテクチャを構築しただけでは、作業は完了したとは言えないのです。SaaSアプリケーションの成長に対応できる必要があります。より多くのテナントが参加するにつれて、ストレージアーキテクチャも成長し、そのニーズに応えられるようにスケールする必要があります。先ほどの例のアプリケーションに戻ると、 アプリケーションを作成し、本番環境に移行して、最初の顧客の受け入れを開始しました。より多くのテナントがシステムに参加するにつれて、アプリケーションのスケーリングを開始する必要があります。幸いなことに、スケーリングを処理してくれるマネージドコンピューティングサービスを使用しています。
より多くのテナントが参加するにつれて、コンピューティングリソースは継続的に成長し、それに伴いストレージへの要求も増加します。テナントが増えるということは、より多くのトランザクションが発生することを意味し、 最終的にはストレージがボトルネックとなります。ここで、ストレージのスケーリングについて考える必要が出てきます。ストレージのスケーリングについて話す際には、3つの考慮事項があります。1つ目は垂直スケーリング - ストレージインスタンスにリソースを追加することです。2つ目はコネクション管理です。 ストレージインスタンスが処理できるコネクション数には限りがあるため、より多くのコネクションを処理できるようにスケールする必要があります。そして3つ目は水平スケーリング - 論理的なデータセットに新しいストレージインスタンスを追加することです。 これらのスケーリングメカニズムはそれぞれストレージアーキテクチャに複雑さをもたらすため、新しいメカニズムを実装する前に、既存のアーキテクチャをできる限り活用することが望ましいです。
スケーリングを開始する際、まずは垂直スケーリングから始めます。 これは単純にリソースを追加するだけなので、わかりやすいアプローチです。ボトルネックに近づくたびに、インスタンスサイズを大きくしてリソースを追加し、 テナントベースを拡大し続けることができます。これは非常にうまく機能し、より多くのテナントにサービスを提供し続けることができます。 ただし、このアプローチの欠点は、単一インスタンスでスケールできる限界が存在することです。
垂直スケーリングと同時に、効率性についても考える必要があります。効率性が高まれば、同じストレージリソースでより多くのトランザクションを処理できるからです。 これはSaaSにおいて特に重要です。なぜなら、SaaSはスケールするものであり、効率性の向上は保有するテナント数全体に波及するからです。1つのテナントに対する改善は、すべてのテナントにとっての改善となります。そのため、効率性向上にイノベーションポイントを投資することで、スケーリング要件を相殺し、より多くのテナントにサービスを提供できるようストレージアーキテクチャを拡張できます。
さらに重要なのは、SaaSによってムダも同時にスケールしてしまうということです。 SaaSアプリケーションを最初にデプロイする際は、とにかく製品を市場に出してお客様の手元に届けることだけを考えています。最も効率的なストレージアーキテクチャのことまでは考えておらず、非効率なアクセスパターンが存在するかもしれません。成長が始まると、これらのアクセスパターンによってムダも同時にスケールしていき、スケーラビリティやコストに影響を与えることになります。これは収益性を左右する可能性もあります。そのため、SaaSの成長フェーズが進むにつれて、効率性への注力が非常に重要になってきます。
効率性について話す際、私たちは2つの領域に焦点を当てています。 1つ目はクエリの効率性を最適化することです。例えば、頻繁な小規模な書き込みリクエストがある場合、それらをマイクロバッチ処理でまとめて効率化するといった方法です。2つ目は、それらのクエリを処理する物理ディスクの効率性を改善することです。 これには、複数のディスクにまたがってクエリを実行しなくて済むように、テナントのデータを同じディスクセットに配置することや、集計クエリが必要な場合はデータを事前に集計しておくことなどが含まれます。物理的な改善を実装する1つの方法として、テーブルパーティショニングがあります。
この単純なクエリを見てみましょう - 特定のテナントに属するすべてのアイテムを取得したいだけです。ここでの課題は、複数のテナントで共有されているテーブルがあることです。そのテナントに一致するかどうかを確認するために、テーブル内のすべてのアイテムをフルスキャンする必要があります。これは非常に非効率的です。特にテナントが多い場合はなおさらです。大規模なプールデータベースがある場合、毎回フルスキャンを行うことになります。他のテナントも同時に同じことを行っていると考えると、それは大きなムダとなります。
幸いなことに、ほとんどのストレージエンジンは何らかの形でテーブルパーティショニングをサポートしています。つまり、このテーブルを小さな物理パーティションに分割でき、それぞれが独自のディスクセットを持つことができます。パーティショニングを行う際は、パーティションキーを使用します。 マルチテナンシーを実装している場合、テナントIDは優れたパーティションキーとなります。特定のテナントを独自のパーティションに配置できるためです。テナントごとに専用のディスクセットを持つことで、パフォーマンスの分離も実現でき、かつデータベース内のルートテーブルも維持することができます。このルートテーブルは、アプリケーションからの要求を適切なパーティションにルーティングする役割を担います。
これは素晴らしい方法です。なぜなら、アプリケーションコードを変更することなく、テナントごとのクエリパフォーマンスを改善できるからです。これはプールモデルの寿命を延ばすのに非常に効果的です。多数のテナントがいる場合、この方法を使用してノイジーネイバー問題を軽減できます。ただし、これにはコストがかかります。運用上のオーバーヘッドが発生します。管理が必要なパーティションが増えるため、そのメリットが運用上のオーバーヘッドに見合うかどうかを検討する必要があります。
Amazon Aurora Limitless Databaseによる水平スケーリングの自動化
私たちが垂直方向にスケールを拡大し、より多くのテナントを受け入れていく中で、クエリの効率化を進めています。より多くのテナントを受け入れていくと、次のボトルネックに直面することになります。それは接続管理です。というのも、アプリケーションがストレージと通信する必要がある度に、そのストレージへの物理的な接続を確立しなければならないからです。マルチテナントアプリケーションを運用しているため、これはテナントごとに発生します。そのため、より多くのテナントからより多くのユーザーが参加してくると、ストレージが処理できる有限の接続数を消費していきます。最終的には、接続プールを使い果たし、これ以上の接続を受け付けられなくなる時点に達します。これは望ましくない状況です - もはやクエリを処理できなくなり、これらの接続を管理するメカニズムを実装する必要が出てきます。
この問題に対処する一つの方法は、アプリケーションに接続管理機能を組み込むことです。これも一つの選択肢ですが、そうすると製品の改善ではなく、接続を処理するための機能開発にイノベーションポイントを費やすことになってしまいます。そこで、マネージドサービスを検討したいところですが、ここでAmazon RDS Proxyが役立ちます。RDS Proxyはストレージとアプリケーションの間に位置します。アプリケーションはRDS Proxyに接続し、RDS Proxyがストレージへの接続を確立しますが、これらの接続を複数のセッションで再利用します。
これは素晴らしい解決策です。RDS Proxyを通じてより多くの接続を処理できるようになり、RDS Proxyのインスタンスを水平方向に増やしてスケールアップし、より多くのテナントを受け入れ、ストレージアーキテクチャをさらに拡張できるからです。しかし、RDS Proxyとセッション変数には問題があり、このアーキテクチャでセッション変数を使用したいユースケースがあります。この単純なWHERE句を見てください。これは基本的なテナント分離で、そのテナントに属するアイテムを返しますが、これをアプリケーションコードの中で行っています。アプリケーションコードは誤りやすく、誰かが変更すると破綻する可能性があります。より安全にするため、ストレージエンジン内でこれを実現したいところです。
このアプローチの一つが、PostgreSQLのRow Level Securityを使用することです。Row Level Securityでは、各アイテムを特定のテナントIDで特定のテナントに割り当てます。これを実装する一つの方法は、テナントごとにユーザーを作成し、そのテナント固有のユーザー認証情報を使用してデータベースにアクセスすることです。PostgreSQLは、そのテナントのアイテムへのアクセスのみに制限することができます。ただし、PostgreSQL内で作成できるユーザー数には限りがあります。もう一つの選択肢は、そのテナントIDを渡し、セッション変数で現在のテナントを設定することです。ストアドプロシージャでも同様のアプローチが可能です。ここでは、そのWHERE句をストアドプロシージャの中に配置しました。このストアドプロシージャを実行する際、テナントIDをセッション変数として渡します。
これらは、テナント分離をより堅牢にする優れたアプローチです。しかし、セッション変数とRDS Proxyの問題点は何でしょうか?RDS Proxyがセッション変数を持つ接続を作成すると、これを固有の接続として認識し、他のセッションでその接続を再利用できなくなります。これはセッション接続のピン留めと呼ばれます。これでは以前と同じ状況に戻ってしまい、接続プールを使い果たすことになります。つまり、RDS Proxyは導入したものの、あまり恩恵を受けられていないということです。この問題に対処するための別の選択肢を考える必要があります。
ここで役立つのが Amazon RDS Data API です。Data API は Amazon Aurora の機能で、Aurora クラスターに HTTP API インターフェースを提供するために有効化できます。Amazon DynamoDB と同じように SDK 統合で作業することができます。SQL クエリを実行し、セッション変数を設定できます。HTTP リクエストを行う際、セッション変数を設定する時にはまだコネクションのピン留めが発生しますが、Data API がそれらのコネクションを管理してくれます。危険な状況に陥りそうになると、コネクションを閉じて再利用することができます。これは素晴らしい仕組みで、より多くのテナントを扱い、セッション変数を同時に使用できるようにコネクションをスケールする手段を得られます。
SaaS アーキテクチャを構築するには、今は本当に良い時期です。安全でスケーラブルなアーキテクチャを構築するためのツールが豊富にあり、さらに進化し続けています。しかし最終的には、単一のストレージインスタンスがサイズの限界に達するという問題に直面します。そこで別の対策が必要になり、一つの選択肢はデータの一部を切り出して専用のストレージに移行することです。もう一つの選択肢は、ストレージインスタンスを追加することです。ただしこれにはアーキテクチャの複雑さが増すという課題があります。テナントのルーティングが必要になり、アプリケーションは適切なストレージにテナントのリクエストを振り分ける必要があります。また、管理すべきインスタンスが増えることで運用の負担も大きくなります。
水平方向のスケーリングを考える際、最初のステップとして Read Replica でのスケールアップから始めることができます。Read Replica は単にデータのコピーで、アプリケーションはこの読み取り専用ノードに読み取りリクエストを送ることができます。これらの読み取りリクエストは書き込みインスタンスに向かわないため、計算負荷が軽減されます。より多くのテナントに対してさらにスケールすることができ、読み取りの多いワークロードであれば、このアプローチは上手く機能します。Amazon Aurora を使用している場合は、最大15個の Read Replica を持つことができ、その一部を Serverless にすることも可能です。これにより、負荷に応じてゼロまでスケールダウンし、より効率的に対応できます。
このアプローチで私たちは長い道のりを進むことができます。中央のインスタンスをスケールアップし、クエリを効率化し、コネクション管理の問題を解決し、Read Replica でスケールアウトしてきました。これは多くの作業と複雑さを追加することを意味します。例えば Amazon DynamoDB と比較すると、DynamoDB はスケーリング、コネクション管理、テーブルのパーティショニングを自動的に処理してくれます。私たちは効率的なクエリを作成するだけでよいのです。しかし、さらにその先に進む必要がある場合はどうすればよいのでしょうか?
では、書き込みトランザクションの処理について説明しましょう。ここで Sharding が登場します。Sharding は異なるシャードにデータを分割する方法です。この場合、異なるシャードは Amazon Aurora クラスターによって表現されます。Sharding を実装する方法は、シャード化したいテーブル内のカラムを特定することです。例えば、テナントテーブルの場合、TenantId がシャードキーとなります。なぜなら、この場合 TenantId が最大のカーディナリティを提供するカラムだからです。
TenantIdのようなShard Keyを取得し、それをハッシュ関数に通すと、ハッシュのマッピングが得られます。これらのハッシュを特定のパーティションIDにマッピングし、そのパーティションは異なるShardに配置されます。ShardはAmazon Auraクラスターとなります。 これは、パーティショニングを実行し、パーティションが異なるShardに分散された状態を表現したものです。
このようにShardとパーティションが用意できましたが、ここで基本的な課題に立ち返る必要があります。異なるパーティションやデプロイメントモデルがある場合、リクエストを適切な場所にルーティングするのはアプリケーションの役割となります。テナントからリクエストがアプリケーションに届くと、アプリケーションはテナントコンテキストを使用します。 受信したリクエストに含まれるTenant IDを使用してパーティションIDを取得し、そのパーティションIDを使ってShard IDを取得します。 そして、適切なShardにリクエストを送信する必要があります。
ご覧の通り、これは非常に複雑な課題となります。プロセスが複雑化し、複数の課題が発生します。まず、クエリの課題として、アプリケーションは特定のデータがどこにあるかを把握し、そこにリクエストを送信する必要があります。複数のShardにアクセスする必要がある場合、アプリケーションはすべての結果を集約し、クライアントに返す前に結合する必要があります。これは本来、リレーショナルデータベースに任せたい作業です。一貫性の観点からも、手動でShardingされたデータベースには一貫性の保証やACIDトランザクションがありません。さらに、運用保守の観点からも、アップグレード、バックアップ、デバッグ、最適化の適用といったタスクはすべて非常に困難になります。これらの課題を解決するために、
Amazon Aurora Limitless Databaseが登場しました。これはPostgreSQLデータベースエンジン向けの専用ソリューションです。異なるShardを管理したり作成したりする必要はありません。 単一のエンドポイントまたはインターフェースにアクセスするだけで、Limitless Database for PostgreSQLが水平スケーリングを自動的に処理します。これは、1秒あたり数百万のトランザクションを処理し、ペタバイト規模のデータを保存できる管理サーバーであり、運用の観点からもトランザクションの一貫性を確保できるシームレスなソリューションです。重要なポイントは、Shardingの複雑さを自分で管理する必要がなく、Limitless Databaseがそれを代行してくれることです。
Aurora Limitless Databaseの実装例と主要概念
Aurora Limitless Databaseの主要な概念には、RouterとShardの集合体であるShard Groupという概念があります。Routerは、マルチテナント環境における異なるテナントやそのユーザーなど、様々なクライアントからの受信リクエストを受け取ったり、インターセプトしたりするAuroraのノードまたはインスタンスです。RouterにはすべてのデータアクセスShardのメタデータが含まれているため、リクエストをどこにルーティングすべきかを把握できます。Shardは、データまたはデータの一部が格納される場所です。
例を見てみましょう。これはeコマースのストアフロントSaaSアプリケーションを簡略化して表したものです。Tenantの概念があり、それからOrderテーブル、そして市や州、国の税金情報を保存する参照テーブルのような Tax Details テーブルがあります。TenantテーブルとOrderテーブルには、Tenant IDが含まれています。手動で実装したShardingソリューションと、Limitless Databaseによって管理されるShardingソリューションには、共通点が1つあります。それはShard Keyという概念です。Limitless Databaseでも、特定のカラムをShard Keyとして指定する必要があり、このShard Keyを使ってLimitless Databaseがパーティショニングを行います。
この例をさらに詳しく見ていきましょう。Orderテーブルでは、データを異なるShardに分散させたいと考えます。Shard Keyを作成する際、つまりTenant IDをShard Keyとして指定すると、Limitless Databaseは3つの異なるShardにデータやデータのスライスを配置します。同様に、Tenant IDを持つTenantテーブルにも同じことが適用されます。これを私たちはCollocated Tableと呼んでいます。2つのSharded Tableが同じShardを共有する形です。この仕組みの素晴らしい点は、同じShard Key値を持つすべてのデータが同じShardに送られることです。これはマルチテナント環境で特に有用で、求めていたデータの分離と隔離が実現できます。また、パフォーマンスの観点からも、特定のTenantに関連するデータが同じShard上にあるため、優れた効果を発揮します。
Tax Rateテーブルについては、Limitless DatabaseではこれをReference Tableと呼んでいます。これは参照テーブルなので、Limitless Databaseはパフォーマンス向上のために、Reference Tableの完全なコピーを全てのShardに作成します。実装の際には、セッション変数でCreate Table ModeをShardedに設定し、この場合Tenant IDとなるShard Keyとなるカラムを指定します。Tenantテーブルを作成する際にはShard Keyを指定し、CollocateするOrderテーブルについては、同様の設定を行いますが、Tenantテーブルとcollocateすることを指定します。これにより、両方のテーブルのデータが同じShard内に配置されます。
最後に、参照テーブルまたはReference TableとなるTax Rateテーブルについては、Create Table Mode ReferenceというSession変数を設定し、Tax Rateテーブルを作成します。もしPostgreSQLの環境でShardingを実装しようとしているなら、手動でShardingを実装する手間を避けて、ぜひLimitless Databaseの利用を検討してください。
Amazon Aurora Limitless DatabaseやData APIなど、これらの機能の多くはここ1年から1年半の間に登場したばかりです。これらは、スケーラブルなマルチテナントストレージアーキテクチャを構築する上で、本当に有用なツールとなっています。
SaaSにおけるバックアップ戦略と課題
アーキテクチャを設計する際には、バックアップについても考慮する必要があります。バックアップは、どのようなストレージアーキテクチャにおいても標準的な要件であり、幸いなことに、これは既に解決済みの課題です。ほとんどのストレージエンジンには、効率的なバックアップとリストアを実行できるネイティブツールが用意されています。ただし、SaaS環境における課題は、これらのツールがディスクレイヤーでバックアップを実行することです。これは効率的で、通常はパフォーマンスへの影響もありませんが、BridgeモデルやPoolモデルのような共有デプロイメントモデルでは、すべてのテナントを同時にバックアップすることになります。つまり、そのバックアップをリストアする際も、すべてのテナントを同時にリストアすることになるのです。
単一のテナントをリストアしたい場合、共有バックアップからそのテナントのデータを分離するメカニズムを導入する必要があります。課題は、ネイティブツールがテナントの分割方法と同じように機能しない可能性があることです。Bridgeモデルでスキーマ単位またはデータベース単位でテナントを分けている場合は、PG dumpのようなツールで対応できるかもしれません。しかし、共有テーブルの場合、単純にテナントIDに基づいてデータを抽出することはできません。独自のソリューションを作成する必要があるかもしれませんが、これによってバックアップとリストアのプロセスが複雑になります。
この複雑さを導入する際、バックアップ時に導入するか、リストア時に導入するかの選択があります。一般的に、リストア時に導入することを選択する理由は単純です:バックアッププロセスをシンプルに保ち、ネイティブバックアップツールを使用し、AWS Backupのようなサービスを使用してスケールで管理できるからです。これによりバックアッププロセスが簡素化され、複雑さはリストアプロセスにのみ導入されます。通常、リストアよりもバックアップの方が頻繁に行われるため、このアプローチは理にかなっています。
Siloモデルでは変更は必要ありません - そのままツールを使用してバックアップとリストアを行えます。しかし、テナントごとのスキーマやデータベースを持つBridgeモデルでは、バックアップを取る際、すべてのテナントが同時にバックアップされます。そのバックアップからリストアする際は、一時的なデータベースや一時的なストレージエンジンにリストアします。これはかなり大きな一時データベースになる可能性があり、維持コストがかかる可能性があります。
これはデータベース単位またはスキーマ単位のテナントモデルなので、効率を上げるためにネイティブツールを使用し、同じツールを使用してライブデータセットにインポートし直すことができます。しかし、Poolモデルではより複雑になります。なぜなら、リストアを行う際、実質的にすべてのテーブルをスキャンして、そのテナントに属するものを特定する必要があるからです。大規模なPoolデータベースでは、このプロセスに非常に時間がかかることがあります - スキャンに数日かかるケースも見たことがあります。
データを抽出した後は、それをライブデータセットにインポートする必要がありますが、これにはパフォーマンスへの影響が考えられます。ストレージエンジンの書き込み容量を消費し、インポート前に既存のデータを削除する必要があるかもしれません。他のテナントへの影響も考慮する必要があり、リストアプロセスの制御が必要になるかもしれません。これらの妥協点はありますが、既存のバックアッププロセスを継続して使用でき、リストア時の複雑さだけに対処すればよいため、ほとんどの顧客にとって好ましいアプローチとなっています。
バックアップ時に分離を行いたい理由もいくつかあります。Siloモデルでは何も変わらず、効率性を犠牲にして単純さを維持します。しかしBridgeモデルでは、以前と同じ分離メカニズムを使用しますが、バックアップフェーズで実行します。ここでの課題は、以前はネイティブツールを使用して物理層で処理を行っていたため、パフォーマンスへの影響がなかったことです。DynamoDBや
Amazon Auroraのようなサービスを見ると、本番データベースへのパフォーマンス影響はありません。しかし今度はストレージエンジンからの読み取りを消費する必要があります。これはパフォーマンスに影響を与えますが、テナントごとのバックアップが得られ、これは素晴らしいことです。なぜなら、リストアを実行する際に、リストアデータベースは必要なサイズに最適化されており、リストアしようとしているデータのサイズだけになるからです。そしてもちろん、それをライブデータセットに戻してインポートすることができます。
バックアップ時に分離を行う主な利点は、すべてのテナントが独自のバックアップを取得できることです。これは、コンプライアンスや規制に対応する必要がある場合や、忘れられる権利に関する法律に対応する必要がある場合に非常に有用です。なぜなら、テナントデータを1つの論理単位として運用的に扱うことができるからです。例えば、オフボーディングについて考えてみましょう。テナントがプラットフォームを離れる場合、バックアップを含むすべてのデータを削除したいと思うでしょう。コンプライアンスのために7年間の保持ポリシーがあり、テナントが離れた場合に共有バックアップがあるとしたら、本当にそれらのバックアップすべてを確認して共有バックアップから削除しますか?おそらくそうはしないでしょう。結局、そのテナントはもう料金を支払っていないのに、そのデータの保存料金を支払い続けることになります。
単一テナントのデータを削除する機能は非常に有用ですが、バックアップとリストアの分離にはそれぞれメリットとデメリットがあり、どちらを選択するかは、ビジネス要件に大きく依存します。そのため、決定を下す前にこれらを検討する必要があります。また、公平性についても考える必要があります。公平性とは、支払いを行う顧客として、支払った分のサービスを受けられるということです。無料利用枠の場合はベストエフォートかもしれませんが、実際にお金を支払っている場合は、支払った分のパフォーマンスが期待できます。
SaaSデータモデル設計と効率性の重要性
それはもっともですが、一方で、SaaSプロバイダーとして、私のストレージインフラを保護できることも同様に重要です。単一のテナントが他のテナントに悪影響を及ぼすことは避けたいものです - いわゆる「騒がしい隣人」は望ましくありません。この公平性を実現するメカニズムについて考える必要がありますが、これはかなり複雑なトピックとなり得ます。公平性を確保する方法は数多くありますが、今日はその多くには踏み込みません。しかし、良い出発点となるのは、アプリケーションの上流でのレート制限です。アプリケーションに入ってくるリクエスト数に制限を設けることができるからです。
その結果、ストレージに入ってくるリクエスト数も制限されます。これを実現する簡単な方法の一つは、フロントエンドにAmazon API Gatewayを配置し、使用量プランを設定することです。1秒あたりの総リクエスト数や同時リクエスト数を設定すれば、ストレージアーキテクチャをより効率的に活用できる素晴らしいレート制限の仕組みが得られます。公平性におけるもう一つのメカニズムは、マイグレーション - つまり、テナントをあるストレージインスタンスから別のインスタンスに移動することです。これは、そのテナントが騒がしい隣人だったり、VIPカスタマーになったりした場合に行うかもしれません。彼らが大きく成長して専用のデータベースが必要になったり、基本プランからプレミアムプランにアップグレードして専用のストレージを得られるようになったりした場合です。
皆さんはホテルに戻って次のストレージアーキテクチャの設計を始めたいと思っているでしょうが、実装を始める前に、アーキテクチャのデータモデルを作成することを考えてください。これはSaaS特有のものではありません - データモデル設計については広く文書化されています。Amazon DynamoDBのページを見てみてください。そこには素晴らしいガイダンスがあります。データモデル内に存在するエンティティ、それらの関係性、アクセスパターンをマッピングすることを考えてください。優れたデータモデルを作成すれば、パフォーマンスが高く効率的で、将来的に新しいアクセスパターンが発生した際にも拡張可能なものになります。
データモデルを設計する際に、SaaSの観点から考慮すべきことがいくつかあります。まず第一に、テナントに属するアイテムを保存するということです。各アイテムにTenantIdを関連付ける必要があります。なぜなら、テナント分離やバックアップなどの機能のために、誰がそのアイテムを所有しているかを知る方法が必要だからです。
また、バックアップやマイグレーションも考慮する必要があります。データはテナントによって所有される形で、これらのメカニズムが利用できるようにする必要があります。SaaSは効率性をスケールさせるものなので、このデータモデルを作成する際には効率性を重視することが重要です。テナントデータを同じストレージインスタンスや同じディスクセット上に配置するなどの工夫は、より効率的な運用につながります。
ストレージとボトルネックについて話すとき、一般的にボトルネックとなるのはコンピュートの方です。ディスク容量が足りないからストレージインスタンスをスケールできないという話はあまり聞きません。通常はコンピュートが問題で、ストレージスペースはコンピュートと比べると安価です。そこで、一般的なアクセスパターンを考慮し、セカンダリインデックスやビューなどでデータを複製することで、それらのアクセスパターンをより効率的に処理できるようになります。これによってコンピュートの負荷を軽減し、クエリをより効率的にすることができます。ストレージアーキテクチャをさらに拡張でき、テナントごとのクエリパフォーマンスも向上するため、二重の利点があります。
データモデルを検討する際、アプリケーションでアップストリームの制限をどのように設定するかを考えることも重要です。 これは厳密にはデータモデル設計の一部ではありませんが、サービスチームと話すと常に聞かれるメッセージです。顧客と話す際は、アップストリームの制限をどのように設定するかを考えるよう伝えてください。制限を設定していないためにストレージがダウンしてしまうと、手遅れになり、顧客に影響が出てしまいます。
今日は多くのトピックを駆け足で見てきましたが、まだまだたくさんのコンテンツがあります。SaaS Factoryチームやその他の社内チームがコンテンツを作成しており、AWS samplesのGitHubにはData for SaaS Patternsリポジトリがあります。より詳しく知りたい方は、そちらに例やベストプラクティスがありますので、ぜひご覧ください。Data for SaaSのYouTubeビデオシリーズも作成しており、そちらのリンクからこれらのコンセプトをさらに深く探ることができます。さらに良いことに、GitHubリポジトリなので、イシューを作成して見たい内容を教えていただけます。私たちはみなさんのためにコンテンツを作成していますので、ぜひフィードバックをいただき、見たいパターンや課題について教えてください。
セッションの終わりに近づいてきました。重要なポイントを振り返ると、SaaSはスケールするということを覚えておいてください。 無駄も効率も同様にスケールし、その効率性が利益を生むか損失を出すかの分かれ目になる可能性があります。そのため、特に成長フェーズにおいては、ストレージアーキテクチャを考える際に効率性を最優先事項として考える必要があります。スケーリングの3本柱を覚えておいてください。 Vertical Scaling、コネクション管理、Horizontal Scalingです。次のスケーリングアプローチを実装する前に、これらをできる限り活用してください。シャーディングのような手法は 一方通行の決定になる可能性があるためです。シャーディングを実装すると、アーキテクチャに大きな変更が加わり、元に戻すのは非常に困難です。
できる限りストレージを活用してください。 テナントデータをTenant IDと一緒に配置し、そのアイテムが確実にそのテナントに所属するようにしてください。Tenant IDに関連付けられたデータへのアクセス権限は制限されるべきです。運用上の理由からも、データを一緒に配置してください。バックアップでも扱えるよう、テナントデータを論理的な単位として扱うことで、作業を簡単にすることができます。公平性を考慮し、アップストリームでレート制限を設定し、 移行プロセスを実装してください。これらは、ストレージアーキテクチャを構築してスケールさせていく際に必要になる要素です。
月曜日のこの時間帯にここにいらっしゃるということは、きっとお疲れのことと思いますが、それだけSaaSが大好きなのでしょうね。今週のre:Inventでは、たくさんのSaaSセッションをご用意しています。テナントの分離について課題をお持ちの方は、明日のSAS312のChalk Talkにぜひお越しください。そこでいくつかのパターンについて見ていきます。また、たくさんのワークショップも開催されていますので、実装について学び、実際にコードを書いて手を動かしてみたい方は、サポーターの方々とぜひお話ください。日々多くのお客様とSaaSに取り組んでいる方々がいらっしゃいますので、皆様の課題についてご相談ください。素晴らしい機会です。また、今回のre:Inventで初めてBuilder Sessionも開催されますので、ぜひ参加して体験してみてください。
アンケートについてですが、これは非常に重要です。私たちの対応が良かったかどうかを知るためだけでなく、より重要なのは、皆様が今後どのようなコンテンツを見たいと思っているのかについてフィードバックをいただける機会だということです。来年はどのようなコンテンツを制作してほしいですか?来年のre:Inventでは何を見たいですか?このような機会を使って、ぜひフィードバックをお寄せください。皆様にはどのような課題があり、私たちはどのようにしてSaaSのさらなるスケーリングをお手伝いできるでしょうか?本日のセッションが、皆様にとって良い学びの機会となっていれば幸いです。Ranjithとともにこのようなお時間を共有させていただき、ありがとうございました。残りのre:Inventもお楽しみください。ありがとうございました。
※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。
Discussion