📖

re:Invent 2024: AWSによるAurora DSQLのアーキテクチャ詳解

に公開

はじめに

海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!

📖 AWS re:Invent 2024 - Deep dive into Amazon Aurora DSQL and its architecture (DAT427-NEW)

この動画では、Aurora DSQLのアーキテクチャについて詳しく解説しています。トランザクショナルなSQLデータベースとしてのDSQLの特徴、Journalを用いた永続性の実現方法、Adjudicatorによる分離制御、そしてStorageエンジンによる効率的なデータ検索の仕組みが説明されています。特にマルチリージョン構成では、3つのリージョンを活用して高い可用性と一貫性を実現する手法が紹介されています。また、実装にはRustを採用し、Turmoilなどの決定論的シミュレーションテストやFuzzing、TLA+やPによるFormal methodsを活用して、システムの信頼性を確保している点も詳しく解説されています。
https://www.youtube.com/watch?v=huGmR_mi5dQ
※ 動画から自動生成した記事になります。誤字脱字や誤った内容が記載される可能性がありますので、正確な情報は動画本編をご覧ください。
※ 画像をクリックすると、動画中の該当シーンに遷移します。

re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!

本編

Aurora DSQLの概要と本セッションの目的

Thumbnail 30

予想以上に音が大きいですね。ありがとうございます。皆様、本日はお越しいただき、ありがとうございます。私たちは、Aurora DSQLが市場に出て、プレビューに参加していただける皆様のような方々をお迎えできることを大変嬉しく思っています。私たちが構築したものについて、興味深いと感じた点、GAに向けて追加してほしい機能、そしてAWSのデータポートフォリオ全体を通じて、どのように皆様のデータベースのニーズにより良く応えられるかについて、フィードバックをいただければと思います。

Thumbnail 50

本日のトークでは、DSQLのアーキテクチャについて詳しくご説明します。昨日は、DSQLでアプリケーションを構築する方法についてお話しました。私の同僚のJamesとEricが本日トークを行い、明日はワークショップがありますので、これは私たちの全体的なストーリーの一部となります。ただし、今回はアーキテクチャの詳細についてお話しします。最も重要なのは、このプロダクトを使用するために、今回のトークの内容を知っている必要はないということです。これは皆様の興味や理解のために、そして私が話したいと思ったことと、他にもいくつかの理由でお話しするものですが、これらはすべてDSQLの内部の仕組みです。私たちは最初から、内部の仕組みを気にすることなく、優れた特性を持つアプリケーションを構築できるデータベースを設計しようと努めてきました。

Thumbnail 110

また、始める前に申し上げておきたいのですが、こういった内容について話すことには少し後ろめたさを感じています。これは、AWS全体の優秀な専門家チームの仕事であり、多くの方々から素晴らしいサポートをいただきました。私がこれからお話しする内容は、私たちチーム全員がこのシステムに貢献した成果の一部だとご理解ください。

Aurora DSQLの特徴と設計思想

まず簡単におさらいをさせていただきます。ほとんどの方は、MattのKeynoteでの発表や、昨日の私のトーク、あるいはドキュメントでご覧になったと思いますが、Aurora DSQLとは何かについて簡単に説明します。そして、書き込みと同時実行制御、スケーラブルなデータベース内でのSQL実行の読み取りとスケールアウトについてお話しします。最後に、クロスリージョン、スケーラビリティ、そして分散システムを構築する上で最も重要だと考えている、不必要な調整やコミュニケーションを避けることについてお話しします。

Thumbnail 150

Thumbnail 160

Thumbnail 190

では、おさらいの部分からです。Amazon Aurora DSQLは、トランザクションワークロード向けに最適化されたリレーショナルSQLデータベースです。私たちは、OLTPスタイルのワークロード、つまり、マイクロサービスアーキテクチャ、SOAアーキテクチャ、Webサイト、モバイルアプリなどを支えるようなワークロードに最適化して構築しました。これは分析用に最適化されたデータベースではありません。ダッシュボードや分析、レポート作成のためのデータベースにはなりません。そういった用途には、AWSのポートフォリオや、より広範なデータエコシステム全体に素晴らしい選択肢があります。

Aurora DSQLは、スケールアップとスケールダウンの両方に対応できるように設計されています。AWSでスケーラビリティについて語るとき、ついスケールアップの方に注目しがちです。確かにこれは、1秒間に数百万トランザクションという大規模なアプリケーションを構築するためのものです。しかし同時に、1秒間や1日に数十のリクエストしか処理しないような、スケールダウンが必要なアプリケーションにも対応しています。私たちは、あらゆる規模のアプリケーションに最適なデータベースを作りたいと考えていました。最も重要なのは、DSQLを選んでアプリケーションの構築を始めれば、ビジネスが成長しても、その選択を後悔することはないということです。私のキャリアの中で上司と交わした最も辛い会話の一つは、「データ移行のために6ヶ月間、機能開発を中断しなければならない」というものでした。私たちの目標の一つは、そのような会話を二度と必要としないことです。

Thumbnail 270

これはServerlessです。確かにAWSではこの用語を少し、いや、かなり乱用してきたかもしれません。しかし、ここで意味しているのは、ある意味でデータベースをAWSの原点に立ち返らせるということです。最初の2つのAWSサービスであるS3とSQSを考えてみてください。DynamoDBやLambdaのように、それこそが私たちの言うServerlessです。インフラストラクチャは必要ありません。コンソールやCLIにアクセスし、エンドポイントを作成して、そのエンドポイントに対してSQLを実行するだけです。パッチ適用や、メンテナンスのためのダウンタイムを心配する必要はありません。

Thumbnail 320

Read Replicaの追加など、データベース運用に関わるような運用上の懸念事項を気にする必要もありません。Active-Activeとマルチリージョン機能を提供します。 これには、単一リージョンのデプロイメントにおけるAZ間のActive-Active構成や、複数リージョンにまたがるアプリケーションのための、強い整合性を持つトランザクショナルなマルチリージョンActive-Active構成が含まれます。これは、顧客に優れたレイテンシーを提供したい場合や、コンプライアンスまたはレジリエンスの要件でリージョンをまたぐ必要がある場合に活用できます。

Thumbnail 340

Thumbnail 370

PostgreSQLと互換性があります。これは、PostgreSQLのSQL機能の大部分が現在のDSQLで動作するということを意味します。今後数ヶ月から数年をかけて、この機能範囲を拡大していく予定です。既存のPostgreSQLクライアントで接続でき、PSQLコマンドラインインターフェースも使用できます。そして、ほとんどのPostgreSQLコードは、わずかな修正でDSQLで動作します。 DSQLは、クラウドの構築と運用における私たちの経験に基づいて作られています。このトークを進めていく中で、私たちが下したやや物議を醸すような決定のいくつかについて触れますが、それらの決定はすべて、大規模なアプリケーションの構築と保守における私たちの経験に基づいています。ロッキング、悲観的アプローチなど、AWS運営の約20年間で多くの厳しい教訓を学んできました。

Thumbnail 420

私は今、Aurora Innovation 10周年記念のシャツを着ています。Auroraを10年前にローンチして以来、そのサービスの構築と改善を通じて学んだ多くの教訓をAurora DSQLに組み込んできました。 私たちはトランザクショナルデータベースの概念を再考しようとしています。1960年代や1970年代にさかのぼるMVCCのような考え方について触れますが、同時に、私たちが新しく特別だと考えるアイデアもあります。どのような大規模システムでもそうですが、これは新しいアイデアと古いアイデアの組み合わせなのです。

トランザクション処理とACID特性の実現方法

Thumbnail 450

トランザクショナルデータベースについての会話を始めるなら、まずトランザクションから始めるのが最適でしょう。 最も簡単なトランザクションの例として、2つのInsert文を実行する場合を見てみましょう。ここでは、dogsテーブルに2つの行を挿入しています - SnufflesとSophieをテーブルに追加しているわけです。このトランザクションをどのように実行するかについて話す前に、まずこのトランザクションを実行する際に求められる特性について考えてみましょう。

Thumbnail 480

これを考える際の一つの枠組みとして、古典的なACID 特性があります。これは古い論文での少々まずい言葉遊びなのですが、現在では業界でトランザクショナルデータベースの特性を考える際の主要な考え方となっています。私たちが求めているのは、まず原子性(Atomic)を持つデータベースです。つまり、トランザクションは完全に実行されるか、まったく実行されないかのどちらかでなければなりません。次に、一貫性(Consistent)を持つデータベース、つまり一貫性のあるデータモデルを構築し、それを維持できるデータベースが必要です。また、分離性(Isolated)を持つトランザクション、つまり同時に実行される他のトランザクションの影響を受けないことも重要です。これは60年に渡って続くデータベース研究の非常に深いトピックであり、この講演では分離性に対する私たちの考え方と、なぜそれが正しいと考えているのかについてお話しします。

最後に、おそらく最も重要な特性として、永続性(Durable)を持つデータベースが必要です。つまり、コミットが返されたときには、そのデータが確実に永続的に書き込まれていることが保証されます。顧客の注文やアクセス制御の変更などのデータが消えてしまう心配をする必要がないのです。また、明示的には語られていない特性もあります - セキュリティ、整合性、暗号化など、これらはすべてDSQLに組み込まれています。

Thumbnail 580

Thumbnail 600

では、2つのInsert文を含むこのトランザクションを実行する際に、これらの特性をどのように実現するのかについて見ていきましょう。ここで、昨年のre:Inventを振り返ってみたいと思います。これは2023年の月曜日の夜、メインステージでのPeter DeSantisの発言です。「ログがデータベースである」と。これは私たちが行ってきた多くのアーキテクチャ変更の背後にある重要な洞察となっています。では、これが具体的に何を意味するのか見ていきましょう。データベースに挿入しようとしている2匹の犬がいます。左側がSnuffles で、右側がSophieです。これらをデータベースに書き込もうとしています。そして、「ログがデータベースである」という結論に基づき、このInsertはSnufflesとSophieをログに書き込むことを意味します。ログに書き込まれることで、それらは永続的になり、原子的に処理することができます。つまり、ログへの書き込みだけで、4つの特性のうち2つを実現できることになります。このログサービスを私たちはJournalと呼んでいます。これはAmazonで10年以上かけて構築してきた内部ログサービスで、S3、DynamoDB、Kinesis、Lambdaなどのサービスを支える重要なインフラストラクチャの一部となっています。これは原子的で分散型のスケーラブルなレプリケーションシステムです。このような基本的なコンポーネントを基に構築することの利点の一つは、原子性や永続性といった非常に難しい問題を解決する必要がないことです。これらの特性は、データベースを構築するために使用できるこのツールに既に組み込まれているのです。

Thumbnail 670

SnufflesとSophieをJournalに書き込んでいきましょう。しかし、ここで分離の問題も解決しなければなりません。もし別のトランザクションが同じ行(同じIDを持つ行)や、このテーブルのユニークインデックスを持つ行を挿入しようとした場合はどうでしょうか?競合するこれらのトランザクションが両方ともコミットしないようにするにはどうすればよいのでしょうか?これが3番目のポイント、つまり分離(Isolated)であり、私たちのアーキテクチャではこれを別のサービス、アーキテクチャの別の部分、別の分散レイヤーで実現しています。私たちはSQLに関して、データベースのモノリシックなアーキテクチャを、トランザクション実行の各タスクに対応する、スケーラブルなレイヤーに分解したのです。

Thumbnail 750

このような分離システムを、私たちはAdjudicatorと呼んでいます。Adjudicatorの役割は、このトランザクションと最近コミットされた他のトランザクションとの間の競合を探すことです。最近コミットされたトランザクションの定義については、後ほど説明します。私たちはAdjudicatorに「このトランザクションをコミットしてもいいですか?」と問い合わせます。OKが出たら、Journalに書き込み、そうしてAtomicで、Durableで、Isolatedなトランザクションが完了するのです。そしてもう一つの特性があります - スケーラブルです。任意のサイズと任意のトランザクションレートにスケールアウトできるデータベースが必要な場合、単一のAdjudicatorだけでは不十分です。Adjudicatorもスケールできる必要があります。

私たちは、キースペースを分割してスケーラブルなサービスとしてAdjudicatorを設計しました。ただし、ほとんどのデータベースとは異なり、Adjudicatorでのキースペースの分割方法は、ストレージでの分割方法とは異なります。Adjudicatorでは、トランザクションの競合検出というタスクに最適化され、複数のAdjudicatorが協調して動作する必要性を減らすようなキースペースの分割方法を選択できます。協調が必要な場合は、分散コミットプロトコルを使用して連携します。私たちのアーキテクチャでは、この分散コミットプロトコルは2フェーズコミットの変種で、数十年にわたるデータベース研究から得られた優れたアイデアを取り入れ、並列化とフォールトトレランスを大幅に向上させています。

Thumbnail 830

Thumbnail 850

さて、このトランザクションが完了したので、もう少し難しい例に進みましょう。これはMaxで、Maxの誕生日です。年齢を増やしたいと思います。つまり、「update dogs, set age equals age plus one」というわけです。Snufflesの誕生日も同じですね。そしてMaxは誕生日プレゼントをもらいました。素敵ですね。このトランザクションを実行するには何が必要でしょうか?まず、これがトランザクションであることを宣言して開始します。つまり、これらすべてのことがAtomicかつIsolatedに実行される必要があるということです。次に、MaxとSnufflesの現在の年齢を取得する必要があります。なぜなら、SQLで「age equals age plus one」と指定したように、このUpdateは読み取り-修正-書き込みの操作だからです。1を足すのは簡単です。誰でもできますよね。そして、データベース内の古い値を新しい値で上書きし、データベースにコミットする必要があります。

Thumbnail 880

Peterの話に戻って、少し補足させていただきます。ログがデータベースである、というのは確かですが、ログからのクエリは効率的ではありません。なぜなら、ログには変更の履歴がすべて含まれているからです。ログの最初から時間をさかのぼってクエリを実行することは可能かもしれませんが、それは過去のすべてのトランザクションに対する操作となります。これは非常に遅く、トランザクションストリーム全体の処理時間が長くなってしまうため、望ましくありません。このアプローチではO(n^2)の時間複雑度となり、スケーラブルな処理速度とは言えません。

では、私たちは何をする必要があるのでしょうか?このJournalをインデックス化し、MaxやSnufflesの現在の年齢のような情報を簡単に検索できるようにするデータ構造を、Journalの上部と周辺に構築する必要があります。

Thumbnail 930

ここで、Storageエンジンの出番となります。Storageエンジンは、データベースからデータを効率的に検索し、抽出する方法を提供します。しかし、一般的なデータベースアーキテクチャとは異なり、Storageレイヤーは永続性や同時実行制御の責任を負いません。Journalがすでにそのデータの永続性を保証しているからです。すべてのStorageエンジンを失ったとしても、単一のトランザクションも失われることはありません。また、トランザクション間の競合検出や同時実行制御も担当しません。それはAdjudicatorの仕事だからです。

これにより、より単純な役割に特化することで、より効率的でスケーラブルなStorage systemを設計することができます。ディスクへの同期や電源復旧、その他データベースエンジンが苦手とする多くの作業について心配する必要がありません。データベースエンジンの構築が難しいのは、多くの機能を持ち、それらの組み合わせを正しく実現することが非常に困難だからです。Storageエンジンから永続性と同時実行制御という難しい作業を切り離すことで、システムをシンプルにし、より低コストで効率的なものにすることができました。

Thumbnail 1020

従来のデータベース技術であるキーのパーティショニングを使用して、このシステムをスケーラブルにします。データベース内の行のスペースを取り出し、異なるStorageノード間に分散させます。ここでは、SnufflesとMaxが異なるStorageノードに配置されています。ワークロードが増加すれば、より多くのStorageノードを追加してより多くのデータを処理できます。読み取りワークロードが増加すれば、各Storageノードのコピーを増やすことができます。書き込みの負荷が増加すれば、それらのStorageノードを分割して書き込みの速度に対応できます。また、ここでは複数のJournalが存在することがわかります。Journalのスケーリングについては後ほど説明しますが、このシステムでは必要な数だけJournalを持つことができます。

Thumbnail 1080

Storageエンジンを構築する中で重要な取り組みの1つは、クエリの一部をStorageエンジンにプッシュダウンして効率的に実行する機能を追加したことです。これは分散データシステムのパフォーマンスにとって極めて重要です。なぜなら、大規模コンピュータシステムの歴史全体を通じて最も一貫した傾向の1つがあるからです。その傾向とは、データ量は非常に速いペースで増加し、スループットもかなり速く向上していますが、レイテンシーはほとんど改善されていないということです。実際、エンド・ツー・エンドのレイテンシーが20%改善されるごとに、ストレージは約10倍増加しており、この傾向は数十年にわたって続いています。

Thumbnail 1170

分散データベースにおいて、ストレージレイヤーに処理を押し下げる(Push down)できることは、クエリ処理レイヤーがクエリに応答するためにストレージとやり取りする必要のある往復回数を大幅に削減します。ここでPush downと言う場合、例えばSnufflesの行を取得したり、特定の条件に一致するすべての行を見つけるためにテーブル全体を順次スキャンして「良い犬」をすべて取得したり、条件に一致する行をカウントして「良い犬」の数を数えたり、あるいはSnufflesや「良い犬」たちの年齢とお気に入りのおやつだけを射影して取得したりするような操作を指します。これらの操作をSQLエンジンからストレージエンジンに押し下げることで、SQLエンジンがストレージと通信する頻度が大幅に減少し、パフォーマンスに非常にポジティブな効果をもたらします。

分散システムにおけるスケーラビリティと同時実行制御

Thumbnail 1220

さらに複雑なクエリを見てみましょう。ここでは、特定の状態の犬をすべて選択し、データベースから空腹の犬をすべて選択して、その数をカウントします。そして、アプリケーションコードの中でどの犬にエサを与えるかを決定し、在庫のフードの量を減らし、FidoとMaxの状態を「満腹」に変更します。この場合、彼らにエサを与えることを決定したわけです。

これらのSQL文の間には、アプリケーションコードへの往復が発生します。JavaScriptやJava、あるいはシステムを構築した言語への往復です。これがSQLトランザクションの性質です - このように対話的な処理となります。アプリケーションは「開始」を宣言し、SQLを実行し、ビジネスロジックを実行し、場合によっては人間に戻って確認を取り、また別のSQLを実行し、クライアント側でビジネスロジックを実行する、といった具合です。これがスケーラブルなSQLデータベースの実装を難しくしている要因の1つです。これは比較的単純なSQLの例ですが、SQLではほぼ任意の計算が可能です。実際、SQLは完全なプログラミング言語としての機能を持っています。

Thumbnail 1320

クライアントとやり取りする完全なプログラミング言語を実行するには、完全な計算サービスが必要です。これは、10年前にAWS Lambdaというスーパースケーラブルな完全な計算サービスを発表したことを考えると、理にかなっています。私たちはそのような計算サービスの運用について長年取り組み、多くのことを学びました。そしてその教訓をDSQLの計算レイヤーに組み込みました。フロントエンドには、PostgreSQLプロトコルを受け取り、トランザクションの開始時に各接続を適切な場所にルーティングするTransaction and Session Routerがあります。これは、PG Bouncerのようなものをログに適用したものと考えることができますが、データベースに組み込まれています。

実際の計算レイヤーは、大規模なFirecracker Micro VMのセットです。これは、Lambdaとコンテナサービスのために開発した技術で、re:Invent 2018で発表したオープンソースのマイクロVMハイパーバイザーです。これを使用してPostgreSQLエンジンの周りにセキュアなボックスを配置しました。SQLは任意のプログラミング言語であり、ほぼ無制限の能力を持っているため、このセキュアなボックスが必要でした。そのため、PostgreSQL自体、少なくともSQLの解析、実行、クエリの最適化、クエリの実行方法の決定などの部分を、安全でスケーラブルな方法で実行できるようにする必要がありました。

これらのFirecrackerは、ここでは3つ表示されていますが、1つのデータベースに数千、数万、あるいはそれ以上存在する可能性があり、それらは全て私たちのフリート内の異なるマシンに配置されている可能性があります。これは、データベースに接続する際には完全に見えない形で実現されています。接続を作成してトランザクションを開始すると、スナップショットから新しいFirecrackerを作成して、あなたの作業を処理します。これは、ヘッドノードを実行する1台のマシンというような、一般的なPostgreSQL データベースの考え方とは大きく異なります。PostgreSQLはフォークを行い、複数のプロセスが存在しますが、これらは完全に独立したクエリプロセス、完全に独立したPostgreSQLインスタンスであり、文字通り互いに通信することができません。それらは完全に分離されており、この完全な分離こそが優れたスケーリングの鍵となっています。

Thumbnail 1430

トランザクションの分離性とリードについて説明しましょう。トランザクション内でリードを実行する際に、それらすべてがトランザクションストリームの一貫したポイントからのものであることをどのように保証するのでしょうか。Snufflesの年齢を読み取り、次にMaxの年齢を読み取る際に、システムに適用された別のトランザクションの部分的な効果が見えてしまうことは避けたいのです。このトランザクションの全期間を通じて、リードを実行する際には、ある一連のトランザクションの全てを見て、別の一連のトランザクションは一切見えないようにしたいのです。では、それをどのように実現するのか説明しましょう。

これは、2日前にMatt Garmanがステージで話し、また前回のre:InventでPeter DeSantisが基調講演で説明した、原子時計、衛星時計配信、そしてカスタム時計配信ネットワークを使用して実現しています。私たちはAWS内にカスタム時間配信ネットワークを構築し、これをAWS Time Syncサービスとして公開しています。これはSQL内で重要な役割を果たしていますが、通常のEC2インスタンス上で、お客様独自のデータインフラストラクチャにも利用可能です。クエリプロセッサがトランザクションを開始する際、beginやstart transactionを実行すると、時計の値を読み取ります。この時刻をTと呼びましょう。そのトランザクションがストレージにアクセスするたびに、「ストレージ、時刻Tにおけるこれらの処理を実行してください」と指示します。この時刻がトランザクションの全期間を通じて一定であるため、ストレージの1つのシャードに「時刻5におけるSnufflesについて教えて」と問い合わせ、別のノードに完全に独立して「時刻5におけるMaxについて教えて」と問い合わせることで、ノード間の通信なしにデータベース全体で一貫したスナップショットを取得できます。

Thumbnail 1590

では、この「時刻T」はどのように機能するのでしょうか? これは、マルチバージョン同時実行制御、またはマルチバージョニングと呼ばれる古典的なデータベース技術を使用しています。各ストレージノードは、ジャーナル内のデータだけでなく、そのジャーナルのインデックス、さらにそのインデックスの最近の変更履歴も保存しており、これにより特定の時点でのリードに関する問い合わせに対応できます。特に素晴らしいのは、これによってストレージノード間の調整の必要性が大幅に減ることです。2フェーズコミットのようなプロトコルを実行する必要もなく、この段階でのロックも必要ありません。このクエリプロセッサと他のクエリプロセッサ間、あるいはこのクエリプロセッサが通信しているストレージノード間での調整も必要ありません。Snufflesを含む複数のストレージノードがある場合、それらは互いについて知る必要もなく、通信する必要もありません。そのセットにリーダーやプライマリは存在せず、すべてが対等なピアとなっています。

Thumbnail 1690

「時刻3でこのリードを実行せよ」と指示すると、ストレージノードは時刻3のSnufflesを探します。時刻4のSnufflesが存在しても、それは無視されます。もし自身の時計を確認して「すべてのSnufflesを確認したが、時刻2までのSnufflesしか見ていない、まだ知らない時刻3のものが存在するかもしれない」と判断した場合、そのクエリプロセッサに対して、その特定の時点での最新バージョンを確実に持つために、ジャーナルを時刻3まで適用する間、少し待つように指示します。 これに代わる方法としては、「時刻3でSnufflesを読み取るが、その読み取りが完了するまで新しいSnufflesを受け入れない」というようなロッキング方式があります。ロッキング方式の問題点は、ここでリード処理を行うリーダーが、ライターをブロックし、システム内の他のコミットを遅延させてしまうことです。これはスケーラビリティの観点から好ましくありません。また、このロックを取得する場所を見つける必要があることも意味します。ストレージサーバーの完全なピアセットを持つだけでは不十分で、このパーティションのリーダーを見つけて作成し、そこでロックを行う必要があります。

Thumbnail 1760

マルチバージョニングを採用することで、私たちは完全にその調整を回避し、読み取りパスでのロックを完全に避けることができます。これはPostgreSQLの中にあるマルチバージョニングとは異なる実装です。彼らの実装にはメリットとデメリットがありますが、ここで私たちが大きく変えたかったのは、Vacuumを気にする必要がないということです。私たちが採用したこのデータ構造の実装アプローチは、そういった問題を回避できると考えています。 ここまでで、Transaction and Session Router、SQLを実行するQuery Processor、分離性を担当するAdjudicator、アトミック性と永続性を扱うJournal、そしてこれらのインデックスとPushdown Computeを処理するStorageについて説明してきました。これらの各レイヤーは、特定のワークロードの要求に基づいて、水平方向に独立して動的にスケールします。読み取りが多いワークロードであれば、Storageがより多くのレプリカでスケールアウトするのが見られます。書き込みが多い場合は、より多くの分割が発生し、Storageがより多くの場所、より多くのJournalに分割されます。各読み書きに対して多くのSQLを実行する場合は、より多くのQuery Processorが配置されます。

Strong Snapshot Isolationの採用とその理由

これらの各レイヤーを独立してスケールできることで、それぞれのDSQLクラスターを特定のワークロードに合わせて適切なサイズに調整できます。これは多くの面で、Auroraの物語の続きと言えます。10年前、私たちはAuroraでPostgreSQLのストレージレイヤー、レプリケーションレイヤー、データ整合性レイヤーを分離し、独自のストレージエンジンに組み込みました。今回私たちが行ったのは、その考えをさらに推し進め、データベースのすべての機能を完全に分離したのです。PostgreSQLエンジン自体はQuery Processorレイヤー内で動作しており、他のすべては私たちが構築したカスタム実装となっています。

Thumbnail 1860

では、スケーラビリティとその源泉について説明しましょう。 分散システムにおいて、スケーラビリティの本質はコンポーネント間の調整を避けることにあります。すべてのコンポーネントがリクエストごとに他のすべてのコンポーネントと通信しなければならない場合、そのシステムはスケーラブルではありません。同じ手法が回復性の向上にも役立ちます。調整がなければ、ロックが長時間保持されることはありません。障害が発生したコンポーネントと通信しなければ、それがパフォーマンスに影響を与えることもありません。分散システムにおいて、調整とコミュニケーションが少なければ少ないほど、スケーラブルになるだけでなく、より回復力が高く信頼性の高いシステムになります。

これらのレイヤー間でトランザクションのコミットがどのように行われるか見てみましょう。読み取りと書き込みは、Routingレイヤーを通してQuery Processorに流れ、Query Processorは必要なデータを持つShardの適切なStorageレプリカを選択してそこからデータを取得します。各Shardにリーダーは存在せず、特定のものを選ぶ必要もありません。Storageシャードの任意のレプリカを選択でき、必要なデータを持つ特定のShardとだけ通信すれば良いのです。書き込みはQuery Processor内に留まります。マルチリージョンの話をする際に、なぜこれが重要な特性なのかを説明します。現時点で重要なのは、これらの書き込みがシステムの他の部分と調整を行わないということです。書き込みはStorageに行かないため、分離性の実装がはるかに容易になり、Query Process間での通信も必要ないため、そのレイヤーのスケーラビリティが向上します。

Thumbnail 1990

これらがどのようにしてデータベースに反映されるのか疑問に思われるかもしれません。トランザクショナルなデータベースなので、コミット時に永続化されます。これがトランザクション実行の2番目のステップです。 ここでは、トランザクションのコミットには3つのAdjudicatorのうち2つが必要になることがわかります。データが3つのAdjudicatorシャードのうち2つにまたがっているため、それらのAdjudicatorに対して分散分離チェックプロトコルを実行する必要があります。書き込みは常に1つのJournalに対してアトミックに行われ、決して複数の部分に分割されることはありません。そのJournalから、書き込まれたキーを含むStorageエンジンがそれらをJournalから取得し、自身のストレージに適用します。書き込まれたキーを含むStorageエンジンだけが関与する必要があります。このトランザクションに関与しないキーを含むStorageエンジンは、このトランザクションが発生したことを認識する必要すらありません。彼らの観点からすれば、それは発生しなかったも同然です。これがスケーラビリティの向上に寄与します - トランザクションに関与していなければ、それについて心配したり知る必要もないのです。

Thumbnail 2070

それでは、Isolation層について詳しく見ていきましょう。先ほど、読み取り操作のためにタイムスタンプを選択し、Multiversioningを使用してread レベルのIsolationを実現する方法についてお話ししました。トランザクションの開始時に、Query Processorは、AWS Time Sync Serviceを使用してスタート時間を選択し、このトランザクションの開始時間が確実に含まれる時間枠をローカルクロックに問い合わせます。そして先ほど説明したように、Storageにアクセスする際には、そのトランザクションの開始時間でのすべての読み取りを行い、書き込みやInsert、Updateなど、データベースへの変更はすべてQuery Processor内でスプールされます。

読み取り、変更、書き込みを行うUpdateは、読み取り側では完全にSelectと同じ動作をし、書き込み側では完全にInsertと同じ動作をします。これらはQuery Processor内で行われます。そしてCommit時には、Isolationルールをチェックして、このトランザクションをCommitできるかどうかを確認する必要があります。

Thumbnail 2140

Thumbnail 2150

ここでの大きなスケーラビリティのポイントは、Commit時まで調整が不要だということです。これは、スケーラビリティとレイテンシーの大きな最適化になっています。 そしてCommit時には、Optimistic Concurrency Controlプロトコルの変形版を使用します。このプロトコルは、このトランザクションと同時に発生した可能性のある短い時間枠内のトランザクションを見て、Isolationルールを守りながらこのトランザクションをCommitできるかどうかを判断します。

データベースに詳しい方なら、Isolationルールには多くのバリエーションがあることをご存知でしょう。最も強力なのはSerializableと呼ばれるもので、これは同時実行トランザクションが存在しないかのように扱えるIsolationルールのセットです。一方、スペクトルの反対側には、Read Uncommittedのような非常に弱いIsolationレベルがあります。これは言わばYOLOトランザクションで、本当に必要ないもので、ステートメントごとに処理するだけのものです。DSQLでは、Strong Snapshot Isolationと呼ばれるIsolationレベルを選択しました。

Thumbnail 2210

Strong Snapshot IsolationはPostgreSQLのREPEATABLE READレベルと同等で、私たちの知る限り、PostgreSQLで最も一般的に使用されているレベルです。PostgreSQLがこのSnapshotレベルをREPEATABLE READと呼んでいることは、データベースコミュニティでは少し議論の的になっています。実際にはANSI REPEATABLE READではないのです。これはPostgreSQLが少しごまかしているところですが、ちなみにREPEATABLE READよりも優れています。PostgreSQLの人々は正しいのです(彼らはよく正しいのですが)。私たちのIsolationレベルであるStrong Snapshot IsolationはPostgreSQLのREPEATABLE READと同等です。ここには2つの要素があります。1つは「Strong」という言葉で、私たちにとってこの「Strong」は強い一貫性を意味します。具体的にはLinearizedされており、これについては後で説明します。DSQLでは、分散システムの並び替えやデータが消失するような影響は見られません。

Thumbnail 2280

Snapshot Isolationとは、コミットされていないデータを決して参照できないということを意味します。つまり、システム内の他のトランザクションのデータは、それが最終的にコミットされるにせよロールバックされるにせよ、一切参照できません。これは私たちにとって実装が容易でした。なぜなら、Query Processorは互いに通信することがないため、コミットされていないデータを参照できる経路が存在しないからです。読み取りは繰り返し可能です。つまり、同じ行を複数回読み取った場合、書き込みを行わない限り、毎回同じデータが表示されます。書き込みを行った場合は、その書き込みの結果が反映されます。さらに、すべての読み取りは、行、テーブル、スキーマにまたがる論理的な時間の一点から行われます。そして最後に、競合する書き込みは拒否されます。これは、同じ行に対して同時に書き込みを行う2つのトランザクションがある場合、少なくとも1つはコミット時に拒否されるということです。DSQLを使用する際は、競合するトランザクションによってコミットが失敗する可能性があることを認識し、場合によってはリトライする必要があります。Snapshot Isolationはシリアライザブルではありません。なぜそれでもこれを選択し、シリアライザブルでないにもかかわらず正しい選択だと考えているのか、すぐにご説明します。

マルチリージョン構成とネットワーク分断への対応

Thumbnail 2370

これら2つのトランザクションは両方ともコミットされますが、これはシリアライザブルな分離レベルでは許されません。シリアライザブルな場合、少なくとも1つはコミットできないはずです。これらの2つのトランザクションは、同じ行を読み取り、一方が1行目を書き込み、もう一方が2行目を書き込んでいます。シリアライザブルなデータベースでは、これら2つのトランザクションのうち少なくとも1つは、ロッキングによってSelectで待機するか、オプティミスティックな場合はコミット時にアボートする必要があります。両方をコミットすることはできません。シリアライザビリティに関する重要なポイントは、シリアルなデータベースでは、単なる読み取り操作でもトランザクションがアボートする可能性が高くなるということです。一方、Snapshot Isolatedなデータベースでは、読み取り操作はトランザクションのアボートに全く影響を与えません。

Thumbnail 2470

Thumbnail 2490

DSQLの分離レベルでアプリケーションの設計と最適化を考える際、読み取り操作について過度に慎重になる必要はありません。シリアル分離が全ての面で優れているという主張をする人もいますが、それは間違いです。シリアル分離は、スケーラブルなアプリケーションを構築する際に、特に読み取り操作の慎重な検討を必要とするなど、多くの懸念をアプリケーションプログラマーに押し付けることになります。一般的なOLTPアプリケーションでは、 書き込みよりも読み取りの方が圧倒的に多くなります。なぜなら、ほとんどすべての書き込みは実質的に読み取りを伴うからです。挿入時のユニークキーの確認が必要であり、更新は読み取り-修正-書き込みの操作となり、多くのワークフローはデータベースからデータを取得し、クライアントサイドでロジックを実行して、何を 書き込むかを決定することから始まります。

Thumbnail 2540

これを実装するレシピとしてはどのようになるでしょうか?先ほど説明したように、すべての読み取りをt_startで実行し、データベースに書き込む際のコミット時に、t_commitと呼ばれる2番目のタイムスタンプを選択します。これは物理クロックと分離実装プロトコルに基づいて選択されます。トランザクションがコミットできるのは、t_startとt_commitの間に他のトランザクションが同じキーに書き込みを行っていない場合のみです。そして、その書き込みはt_commitでデータベースに行われなければなりません。他の読み取り側には、その論理的な時間で見えるか、まったく見えないかのどちらかでなければなりません。これが コミット時にAdjudicatorとの間で交わす契約です。Adjudicatorに対して「これらが書き込もうとしているキーです。もし他のトランザクションが私のトランザクションウィンドウ内でこれらの同じキーに書き込んでいなければ、コミット時間を選択してこのトランザクションをそのコミット時間とともにJournalに書き込んでください」と伝えます。このトランザクションをコミットした後は、より小さなt_commitを選択することは決して許されません。このプロパティにより、データベース内の時間が論理的に後戻りすることがなく、読み取りと書き込みのストリームの一貫性が保たれます。

Thumbnail 2580

先ほど説明した通り、私たちはSnapshot Isolationが分散アプリケーションにとって最適なポイントだと考えています。Serializableな分離レベルにはメリットがありますが、同時にコストも伴います。より低い分離レベルにはパフォーマンス面での利点があることはよく知られていますが、私たちのアーキテクチャでは、より低いレベルに下げることのメリットはありません。Read Committedにすれば、Multiversion Concurrency Controlを避けることはできますが、ACIDの仕様を満たすためにはWrite操作をより緊密に調整する必要が出てきます。Read Uncommittedにする意味もありません。なぜなら、Query Processor fleetのスケーリング方法により、並行トランザクションと未コミットのトランザクション間の分離が自然に得られるからです。

Thumbnail 2700

時間の経過とともに、特定のアプリケーションの要件を満たすために、DSQLに他の分離レベルや一貫性レベルを追加していく可能性は十分にあります。しかし、ほとんどの分散アプリケーションにとって、Strong Snapshot Isolationが最適なポイントだと考えています。Strong Consistencyは非常に重要だと考えています。これはAmazonでの長年の運用から得られた教訓の一つです。アプリケーション開発者にとって、Eventual Consistencyを考慮しながら正しいアプリケーションコードやビジネスロジックを書くことは非常に難しいことがわかりました。Write操作を行ったのにその結果が消えてしまったり、他のシステムがWrite操作を完了したと言っているのにその効果が見えなかったりすると、データベースから読み取った情報の信頼性を判断するコードを実装するのは非常に困難です。私たちは比較的低い分離レベルを選択しましたが、正しいアプリケーションを書くためにはStrong Consistencyが不可欠であり、私たちのアーキテクチャではそれほど大きなコストをかけずに実現できると考えています。

Thumbnail 2710

マルチリージョンのパフォーマンスは、一つのことを最適化することに尽きます - それはリージョン間のラウンドトリップです。私たちのサイエンスチームが何度も試みましたが、光速は物理的な制約として存在します。光ファイバー内での光速は、1ミリ秒あたり約123マイル、つまり200キロメートルです。これは非常に速く聞こえますが、大陸を横断したり地球を横断したりする場合には実際にはかなり遅いのです。このような大きな地理的距離の往復にかかるコストは、できるだけ少なくしたいものです。実際、マルチリージョンデータベースにおけるパフォーマンスの全ては、このラウンドトリップの回数を最適化することに関わっています。

Thumbnail 2760

私たちのアーキテクチャでは、Commit時まではトランザクション間の調整は必要ありません。Mattのキーノートでお話ししたように、Begin、Select、Insert、Updateといった操作は、ユーザーのリージョンでローカルに実行されます。Availability Zone内で実行している場合は、そのAvailability Zone内で完結し、Zone間をまたぐ必要すらありません。複数のInsert、Select、Update操作を含む比較的複雑なトランザクションでも、それぞれの操作でクロスリージョンのペナルティを気にする必要はありません。これは、Primary選出を使用する市場の他の製品のように、一方が高速で他方が低速というわけではなく、マルチリージョンSQLの実装の両側で実現されています。

Thumbnail 2870

Commit時には、2つの理由からクロスリージョンの調整が避けられません。第一に、分離ルールを満たすために調整が必要です。第二に、そしてより重要なのは、あるリージョンで壊滅的な障害が発生した場合でもデータが確実に保持されるよう、複数のリージョンにデータをコピーする必要があるということです。これは物理法則の基本です - これ以上の改善はできません。その特性を得るためには、少なくとも1つの他のリージョンにデータを移動させる必要があります。 しかし、読み取り専用トランザクションは全く調整を必要とせず、常にローカルでCommitできます。これは主に、読み取り専用トランザクションのCommitが実質的に何もしない操作だからです。Write対象のキーがないため、AdjudicatorやJournalに問い合わせる必要がありません。単なる読み取りなので耐久性を気にする必要もなく、シングルリージョンモードでもマルチリージョンモードでも即座に応答を返すことができます。

Thumbnail 2900

私たちにとって2番目に重要な設計目標は、高速なフェイルオーバーを実現することでした。つまり、マルチリージョンで障害が発生した場合に、できるだけ早く復旧できるようにすることです。これを実現するために、リーダーシップの変更、つまり特定のキーを担当するAdjudicatorの切り替えをできるだけ高速に行えるようにしました。トランザクションプロトコルの設計において、これらのAdjudicatorが一時的なソフトステートのみを保持するようにしました。データベースのサイズに応じて増加するステートや、ロックステートのような永続的なハードステートは持たせていません。

リージョン障害時の挙動と可用性の確保

Thumbnail 2950

プレビューリリースでは、3つのリージョンにまたがる構成をサポートしています。そのうち2つのリージョンがアクティブで、アプリケーションからアクセスできるエンドポイントを提供します。残りの1つのリージョンはJournalのみを持つWitnessリージョンです。このJournal-only Witnessリージョンにより、2つのリージョン間で分断が発生した場合でも、一方が明確に過半数側にとどまり、そのパーティション側のクライアントに対して可用性と一貫性を維持できます。また、広範な地理的エリアにまたがる分散システムの場合、このWitnessを中間に配置することで、レイテンシーを最適化できるという利点もあります。では、リージョンが分断された場合はどうなるのでしょうか?

Thumbnail 3020

これは、誰かが海底ケーブルのアンカーを引き上げてしまい、リージョンが孤立してしまうような場合に起こり得ます。もちろん、そのようなことは起こって欲しくありませんし、私たちは非常に冗長性の高いマルチリージョンアーキテクチャを持っています。しかし、私たちがここにいるのは、まさにそういったことが実際に起こるからなのです。

そうなると何が起こるのでしょうか?まず、Journalは3番目のリージョンへのレプリケーションを停止します。分離を処理するための短期的なステートであるAdjudicatorのリーダーシップは、この障害リージョンから移動し、残りの正常なリージョンに完全に移行します。障害が発生したリージョンのクライアントはトランザクションを実行できなくなり、そのリージョンでの読み取りも書き込みもできなくなります。データベースはその障害側で利用できなくなります。一方、過半数側では、データベースは可用性、耐久性、強い一貫性を維持し続けます。

物理法則やCAPの定理に反すると主張する人もいるかもしれませんが、それは正しくありません。ネットワーク分断の過半数側を確実に特定できれば、その側で可用性と一貫性を維持し続けることは可能です。3つのリージョンがあり、2つを含む側を過半数側として選択すれば、可用性と一貫性を維持できます。これが私たちの採用している方法です。ここでのコストは、この不運な犬(Sophie)がトランザクションを実行できず、データベースがSophieにとって利用できなくなることです。

アプリケーションでこれを使用する方法としては、可能であればネットワーク分断の正常側で読み取りを行います。これはRoute 53のレイテンシー考慮型ルーティングやヘルスチェックなどを使用して実現できます。ただし、一部のクライアントはこれらの他のリージョンにアクセスできず、その時点でシステムは利用できなくなる可能性があります。これが根本的なトレードオフです。これは物理的な制約なのです - Strong Consistencyを諦めない限り、分断の両側で可用性を確保することはできません。

Thumbnail 3140

それではフェールオーバーのタイミングでは何が起こるのでしょうか?読み取りパスは変更なし、常にローカルを見ます。書き込みパスも変更なし、常にローカルを見ます。Adjudicatorは少量の一時的な状態を正常なリージョンに移動させます。Journalはすでに正常なリージョンで過半数を確保していたため、これまで通りのクォーラムベースのプロトコルを実行し続けます。両側にコピーがあるためデータ損失はなく、分断の正常側での可用性の損失もありません。

Aurora DSQLの品質保証と今後の展望

Thumbnail 3180

新しいデータベースを構築することは困難な作業です。多くの新しいコードと新しい設計上の決定が必要でした。このプロジェクトを構築する中で最も難しかったことの1つは、これが正しく動作することをどのように確認するか、つまりこのシステムが機能することをどのように確認するかを考えることでした。

Thumbnail 3200

新しいコードすべてにRustというプログラミング言語を選択しました。これには複数の理由があります。優れたパフォーマンスを提供してくれます。Rustコードのパフォーマンスには非常に満足しており、また同様のパフォーマンス特性を持つ他の言語では時として避けにくい安定性とセキュリティのバグのクラス全体を回避できるメモリ安全性も提供してくれます。Rustを選択したことには非常に満足しています。現在、AWSではほぼすべてのサービスで広くRustを使用しており、実装の際のデフォルトの選択肢となっています。

Thumbnail 3250

私たちは決定論的シミュレーションテストという手法に深く投資してきました。分散システムで最も難しいことの1つは、ネットワーク障害時、クロックが信頼できない期間、アーキテクチャの特定の部分が信頼できないか不安定な期間における動作をテストすることです。決定論的シミュレーションテストは、これらのケースをビルド時に決定論的に記述されたテストでテストできる手法です。本質的に、これは大規模なマルチリージョン分散データベースシステムのユニットテストです。IDEの中で実行でき、これらの分散プロパティをテストできます。私たちはTokyoのためのオープンソースの決定論的シミュレーションライブラリであるTurmoilを構築しました。GitHubで確認できます。また、正常なケースと障害ケースの両方でコードの動作を深くテストできる、他の非常にクールな決定論的シミュレーションテストインフラストラクチャも構築しました。

私たちは、人間が書いたテストと、システムが書いたテストの両方に投資してきました。システムによるテストは、ビルド時に新しいシナリオを作成し、障害シナリオの状態空間を探索することができます。これは非常に優れた方法であり、大規模な統合テストインフラを構築することなく、分散システムのテストを加速できる方法として私たちが重視しているアプローチです。

Thumbnail 3350

私たちは特にSQLの領域におけるFuzzingに深く投資してきました。 私たちは、何百万、何十億もの独自のSQLトランザクションを生成し、それらをSQLの仕様や、Aurora PostgreSQLやRDS上で動作するPostgreSQLの動作と照らし合わせてチェックできるツールを構築しました。これにより、私たちのSQL実装の正確性とPushdown実装の正確性に大きな自信を持つことができるようになりました。Fuzzingはテストを加速し、人手では生成不可能な規模のテストコーパスを生成する方法です。この1年ほどの間にFuzzingを使って生成・実行したSQLの量を人間が書こうとすれば、少なくとも数千年はかかっていたでしょう。このFuzzingの一部はオープンソースツールをベースにしており、一部は私たち自身が開発したツールをベースにしています。これらのツールの一部は将来的にオープンソースとしてリリースする予定です。

Thumbnail 3420

私たちは、TLA+やPなどのツールを使用したアーキテクチャレベルでのFormal methodsに深く投資してきました。これらのツールにより、設計したプロトコルの正確性について形式的に推論し、正確性において最も重要だと考える動作を確実に実装できます。これらは数学的な精度と具体性を持って記述できる言語であり、プロトコルの実装だけでなく、仕様や必要な動作をその精度と正確さで記述することができます。私たちはAWSで全般的にFormal methodsを使用しており、10年以上にわたってこの分野に大きく投資してきましたが、このプロジェクトでも非常に有用でした。また、特定のコード行や実装の動作を推論するために、コードレベルでもFormal methodsを使用しています。

Thumbnail 3480

そして、実装の正確性と形式的な仕様との間のギャップを埋めるために、Runtime monitoringという技術を使用しています。これについては今後数週間でより詳しくお話しする予定ですが、この技術により、システムのアプリケーションログを取得し、プロトコルを記述する際に使用・開発した形式的な仕様と比較・チェックすることができます。つまり、実際のRustによる実装が、PやTLA+によるプロトコルのコードやモデルと同じ動作をすることを確認できるのです。これは、プロトコルの形式的な実装における仕様と実装のギャップを効果的に埋める、非常に強力な技術です。

皆様、ご清聴ありがとうございました。DSQLで皆様が何を実現されるのか、とても楽しみにしています。皆様からのフィードバックをお待ちしています。現在の機能範囲や、追加してほしい機能、投資してほしい分野、パフォーマンスに関する経験、その他どんなことでもお聞かせください。プレビューを実施する理由は、このようなフィードバックを得て、GAでローンチする製品が皆様にとって、皆様のニーズにとって最も有用なものになるようにするためです。ぜひフィードバックをお寄せください。実際に試してみてください。プレビューは一般に公開されており、今すぐConsoleで確認できます。APIツールをダウンロードして試すこともできます。ぜひ試していただき、ご意見をお聞かせください。ありがとうございました。


※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。

Discussion