re:Invent 2024: AWSがDynamoDBの内部構造と新機能を解説
はじめに
海外の様々な講演を日本語記事に書き起こすことで、隠れた良質な情報をもっと身近なものに。そんなコンセプトで進める本企画で今回取り上げるプレゼンテーションはこちら!
📖 AWS re:Invent 2024 - Dive deep into Amazon DynamoDB (DAT406)
この動画では、Amazon DynamoDBチームのSenior Principal EngineerであるAmrith Kumarが、DynamoDBの内部アーキテクチャと最新機能について解説しています。特に、毎秒数億リクエストを数十マイクロ秒で処理するためのメタデータストアMemDS、トラフィックスパイクに備えるための新機能Warm Throughput、そしてリージョン間での強力な整合性を実現するSynchronous Global Tablesの仕組みについて詳しく説明しています。DynamoDBが大規模システムでも予測可能な低レイテンシーを実現できる理由や、パーティションの分割方法、リージョン間でのデータレプリケーションの仕組みなど、システムの内部構造に関する貴重な知見が共有されています。
※ 画像をクリックすると、動画中の該当シーンに遷移します。
re:Invent 2024関連の書き起こし記事については、こちらのSpreadsheet に情報をまとめています。合わせてご確認ください!
本編
DynamoDBの大規模システム構築:概要と目的
それでは始めましょう。大規模なシステムを構築することは難しく、お客様は正確で予測可能なパフォーマンスを求めています。Amazon DynamoDBで構築されているアプリケーションの中には、本当に印象的な規模のものがあります。 皆様が作成されたアプリケーションは非常に複雑です。DynamoDBはツールですが、DynamoDBがどのように構築されているかを理解していれば、アプリケーションの構築がより簡単になります。また、私たちが途中で学んだことをお伝えできれば、さらに役立つでしょう。この講演の目的は、そうした学びを共有することです。
私はAmrith Kumarで、DynamoDBチームのSenior Principal Engineerです。AWSに入社してから約4年半、ずっとDynamoDBチームで働いています。今日は、大規模なDynamoDBの構築方法についてお話しし、私たちが学んだことの一部を共有したいと思います。これらは皆様がアプリケーションを構築する際に役立つはずですし、私たちが経験した失敗を繰り返さずに済むでしょう。エンジニアとして、チームが構築したものを誇りに思っていますので、それらについてもご紹介したいと思います。
このプレゼンテーションの最初のパートでは、DynamoDBのアーキテクチャの概要を説明し、その後、3つの具体的なトピックに深く踏み込みます:パーティションの検索に使用する内部データストアのMemDS、2週間前にリリースしたWarm Throughという新機能、そしてSynchronous Global TablesまたはStrongly Consistent Global Tablesについてです。上部の黄色で示された機能に加えて、DynamoDBが選ばれる理由は、どんな規模でも予測可能なレスポンスタイムを持つアプリケーションを構築できるからです。これはお客様が求めているものです。私たちはまさにそのようなデータベースを構築しています。このトーク全体を通して、この3つのポイントを繰り返し強調します:あらゆる規模での予測可能な低レイテンシーです。これこそが私たちの本質です。
DynamoDBのアーキテクチャと基本概念
DynamoDBをしばらく使用していると、DynamoDBのすべてのデータがTableまたはIndexに保存されていることがわかります。 このトーク全体を通して、Tableについて話す際は、Indexにも同様に当てはまります - 内部的には、IndexもTableと同じ方法で保存しています。Tableを作成する際、テーブル名と、そのテーブル上でデータをどのように分散させたいかを指定していただきます。私たちは水平方向にスケールするので、その分散方法が重要です。例えば、ユーザーのログイン、名前、その他の情報があるとします。従来のリレーショナルデータベースの意味でログインをPrimary Keyとすることにしました - つまり、一意で非NULLです。そこでデータベースの分散方法を指定します。
私は35年間データベースに携わってきました。主にデータベースの構築に関わり、短期間ですが使用する側の経験もあります。私の指は、AWSのコマンドラインよりもSQLの方が遥かに打ちやすいのです。このチームで働き始めた時、MySQLのCLIのようなものがないことは私のスタイルを制限しました。そこで自分用にDynamoDB Shellを作りました。これはSQLっぽいインターフェースを提供するもので、オープンソースで公開されており、ダウンロードして使用できます。Homebrewでも入手可能です。SQLのように見えるものが表示されますが、DynamoDBがSQLデータベースになるわけではありません。これは単に、テーブルを作成したい、Primary Keyをログインにしたいということを示すのが簡単だからです。
左側にログインでソートされたテーブルがあり、ハッシュを計算します。これは実際のAWSのハッシュでもDynamoDBのハッシュでもなく、単なる一般的なMD5です。データをハッシュでソートし、その後いくつかのチャンクに分割します。ここでは4つのパーティションとしましたので、データはこの4つのパーティション分のデータとなります。
これらのパーティションはStorage Nodeに保存されます。Storage Nodeとは、 DynamoDBサービスがプロビジョニングするホストで、そこにデータを配置します。データを1コピーだけ保存するのでは不十分です。なぜなら、私たちは継続的にホストにソフトウェアをデプロイしており、また私たちが運用している規模では、ホストは常に障害が発生するからです。データを1コピーだけ持つのは決して良いアイデアではないので、現在は3コピーを保持しています(ただし、この数は将来変更される可能性があります)。そして、 この3コピーを3つの異なるAvailability Zone(AZ)に保存します。AZは障害分離ゾーンであり、AZ同士は運命を共にすることはありません。アプリケーションを構築する際に高可用性を求めるなら、これは皆さんにとっても活用すべき良いコンセプトです - アプリケーションを複数のAZに分散させるのです。
データが3つのAZにあると説明しましたが、 レプリケーションは私たちが管理します。皆さんがこれらを行う必要はありません。テーブルを作成し、Primary Keyを指定すれば、残りは私たちが処理します。このようにデータをすべてレプリケートすることを決めた上で、さらにもう一つのことを行います。各パーティションの レプリカの1つがリーダーとして定義されます。リーダーとは、書き込みと強整合性のある読み取りを処理するレプリカのことです。リーダーは常に最新の情報を持っています。書き込みを行う場合、リーダーが書き込みを処理し、他のAZの1つが書き込みを確認した時点で書き込みが完了したと認識されます。3つ目のAZは数ミリ秒かかる場合があります。
さて、 パーティションのサイズは約10ギガバイトに保つようにしています。このサイズを選んだ理由は、テーブルに対して多くの操作を行うため、それらを並列で実行したいからです。DynamoDBはどのようなスケールでも予測可能な低レイテンシーを提供することが特徴です。1テラバイトのテーブルや200テラバイトのテーブルがあった場合、それをすべて1つのパーティションに入れてしまうと、操作にかかる時間が変動してしまいます。私たちは予測可能な時間を望んでいます。10ギガバイトは適度なチャンクサイズで、パーティションやレプリカに障害が発生した場合でも、10ギガバイト分のデータのリカバリーで済みます。
テーブルにデータをロードしていき、パーティションの1つが10ギガバイトのサイズに近づくと、パーティションを分割します - これもすべて自動的に行われます。 パーティションは実質的にキーハッシュの連続した範囲です。このパーティションを分割したい場合、中間のどこかに線を引いて2つのパーティションを作成します。つまり、P0、P1、P2、P3があった場合、新たにP4とP5が作成され、P3は消滅します。 DynamoDBはServerlessで、つまりサーバーのプロビジョニングは皆さんが行う必要はなく、私たちが代わりに行います。各リージョンには数万台のサーバーがDynamoDBのために用意されており、ここに示しているのは1つのテーブルの1つのパーティションの3つのレプリカで、それぞれが異なるAZに配置されています。
右隣と左隣の人を見てください。その人たちのことを知っていますか?おそらく知らないでしょう。なぜなら、このインフラストラクチャーはDynamoDBを使用する全ユーザーで共有されているからです。あなたとその隣の人がDynamoDBを使用しているなら、どこかのホスト上であなたたちのパーティションとレプリカを共有している可能性が高いのです。これは共有インフラストラクチャーなので、特定のテナントがホストを独占することは望ましくありません。そのため、パーティションごとに制限を設けています:1秒あたり3000回の読み取りまたは1000回の書き込みを超えることはできません。これらの制限は変更される可能性があります。パーティションが10ギガバイトを超えると分割されます。トラフィックが継続的に4000を超える場合も分割される可能性があります - サイズによる分割、スループットによる分割、これらはすべて自動的に行われます。
Request Routerとストレージノードの連携
これがストレージ側で私たちが運用しているインフラストラクチャーです。予測可能な低レイテンシーをどんなスケールでも実現したいため、アプリケーションが直接これらのストレージノードに接続することはできません。実際の仕組みとしては、アプリケーションは通常SDKを通じて私たちと通信します。低レベルAPIを使用している方もいますが、ここではSDKの使用について説明します - 操作している地域のDynamoDBのリージョンエンドポイントを解決します。
そうすると複数のロードバランサーの1つに接続されます。各リージョンには数万のLoad Balancerがあります。また、それらのLoad Balancerの背後にはRequest Routerのフリートがあります。アプリケーションから接続すると、SSLの終端は、どこかのRequest Router上で行われます。
DynamoDBにリクエストを送信すると、最初に認証を行い、署名が正しいことを確認し、実行しようとしている操作が許可されていることを確認します。テーブルやインデックスへの読み取りや書き込みのアクセス権限があるかどうかを確認します。今年初めに、Resource-based Policiesをリリースしました。以前からDynamoDBを使用している方々は、Identity-based Policiesのみをサポートしていたことをご存じでしょう。Resource-based Policiesは、リクエストのたびに検証されます。
100万を超えるお客様がいます。1時間に10億を超えるリクエストを送信するお客様もいて、そのようなお客様の何人かがこの会場にいらっしゃいます。各リクエストは認証と認可が必要です。その後、テーブルに設定した制限内での操作であることを確認する必要があります。Provisioned Modeのテーブルの場合、テーブルに過剰な負荷がかかっていないか、超過したくないアカウントレベルの制限があるかどうかをチェックします。これらすべてが完了したら、データの保存場所を特定し、リクエストをStorage Nodeに転送します。
まず、アプリケーションの処理の流れをご説明します。リクエストは Load Balancer に到達し、Request Router を経由して、実際にデータが保存されている Storage Node まで届きます。DynamoDB では、すべてのアイテムが暗号化されています。お客様が独自の暗号化キーを提供される場合はそれを使用し、そうでない場合は当社のキーを使用します。DynamoDB に保存されているデータは、すべて例外なく暗号化されています。リクエストが Storage Node に到達すると、他のユーザーと共有しているノードの負荷を適切に管理しながらレスポンスを返します。この処理を私たちは1秒間に何億回も実行しています。
私たちが気づいたことの1つは、アプリケーションは大規模になると同じパターンの処理を繰り返すということです。例えば、ショッピングカートのアプリケーションでは、カートへの商品の追加と削除を繰り返し、チェックアウト時も同様の処理を行います。写真のアップロードアプリケーションでも、同じ処理の繰り返しです。私たちはこういった処理パターンに最適化することで、予測可能な性能を実現しています。先ほど説明したように、リクエストは Load Balancer を経由して Request Router に到達します。この図で矢印を赤と緑で色分けしているのには理由があります。
リクエストを送信すると、 どの Load Balancer やRequest Router にも転送できます。なぜなら、すべての Request Router はステートレスだからです。しかし、Request Router は特定の Storage Node にリクエストを送信する必要があります。なぜなら、そこに実際のデータが存在するからです。Request Router は1秒間に何億回も、認証、認可、制限値の確認、リソースポリシーの遵守を確認し、データの場所を特定する必要があります。 このような仕組みについて、これまでドキュメントであまり詳しく説明してきませんでした。そこで、本プレゼンテーションの前半では、この点について説明していきます。
ここまでの内容を簡単に振り返ってみましょう。私たちの目標は、どんな規模でも予測可能な低レイテンシーを提供することです。これを実現するために、水平パーティショニングを採用しています。共有インフラストラクチャのレート制限や、テーブルに設定されたProvisioned モードの制限、On-demand テーブルの制限を確実に遵守する必要があります。そして常に、セキュリティ、耐久性、可用性は譲れない要件として確保します。データのセキュリティを確保し、書き込み時には2つの Storage Node に確実に保存してから完了を通知し、データが常に利用可能な状態を維持します。私は DynamoDB チームに4年以上在籍していますが、DynamoDB は12年以上の歴史があります。この期間中、計画的なダウンタイムの総数はゼロです。
私たちの意図は、データベースを常時100%稼働させることです。なぜなら、それがお客様のアプリケーションに求められる要件だからです。つまり、セキュアで、耐久性があり、常に利用可能で、どんな規模でも予測可能なレイテンシーを提供すること - それが私たちの目指すところです。
MemDS:高速なパーティションメタデータ検索システム
それでは視点を変えて、あなたがRequest Writerだとしましょう。リクエストを受け取って、それをStorage Nodeに送信する必要があります。具体的な例を見てみましょう。ここではGetItemやPutItemがあり、Partition KeyはJorge_souzaまたはRichard_roeとなっています。先ほど説明したように、Partitionはキーハッシュの連続した範囲です。そこで最初に何をするかというと、キーハッシュを計算します。Jorge_souzaのハッシュ値は何でしょうか?それはこの長いD99で始まる文字列です。そして、このLoginテーブルでは、分割後にP0、1、2、4、5が存在することがわかっています。
このプレゼンテーションを何度か予行演習してみて、この説明全体が数秒かかることがわかりました。しかし、私たちは一桁ミリ秒という予測可能なレスポンスを提供する必要があります。毎秒数億回のルックアップを処理しなければなりません。それも数十マイクロ秒で実行する必要があります。しかも、確実に。なぜなら、数十マイクロ秒でこれができなければ、一桁ミリ秒でデータを提供することは不可能だからです。そこで、このトークの最初のパートでは、まさにこれを実現するために構築したシステムであるMemDSについてお話しします。
現在処理しているトラフィックの規模感をお伝えしますと:200テラバイト以上の数百のテーブルがあり、100万人の顧客がいて、その中には毎秒50万リクエストを生成する顧客もいます。各リージョンには数万のStorage Nodeと数万のRequest Routerがあります。そして、これらのテーブルは継続的に作成、削除、分割されており、後ほど移動についても説明します。このデータがどこにあるのかを追跡し、数十マイクロ秒で回答を提供するにはどうすればよいのでしょうか?
明らかに、何らかのメタデータストアが必要です。左側にすべてのRequest Router、右側にすべてのStorage Nodeがあります。Storage Nodeには小さなディスクがあり、そこに実際のデータが保存されています。そして、独自のデータベースに保存されているPartitionメタデータがあります。このPartitionメタデータデータベースにはディスクがあり、永続性があり、テーブルの作成と削除はControl Planeを通じて行われます。これで機能するでしょうか?問題が見えますか?ここでは毎秒数億のリクエストを処理していることを忘れないでください。リクエストが来て、認証され、承認されます。Jorge_souzaはどこにありますか?ここで読み取ってみましょう。毎秒1億リクエストを受け取るとすると、このデータベースは毎秒1億リクエストを処理することになります。これはどんなデータベースになると思いますか?DynamoDB自体よりも大きくなってしまいます。
これは少し問題です。明らかにこれは機能しません。しかも、テーブルは継続的に作成され、削除され、Partitionは分割されています。これらはテーブルを更新しようとします。テーブルを更新しようとすると何が起こるでしょうか?何らかのロックを取得する必要があり、ここでロックを取得しようとすると、Requesterがロックアウトされてしまいます。そのため、予測不可能なレイテンシーが発生します。明らかにこれは機能しません。そこで賢いエンジニアたちはこれを見て、簡単だと言います。その前にキャッシュを置けばいい。いいでしょう。これはすべてのエンジニアが最初に言うことです。これで機能するでしょうか?機能すると思う人はいませんね。頭を振っている人が見えます。これも機能しません。いくつかの理由があります。リクエストが来て、Request Routerに到達し、キャッシュにない場合。ここに行くことになります。そのため、これが処理を行う必要がありますが、Storage Nodeのロックなど、同じ問題が発生します。ただし、キャッシュに入れば、それは素晴らしいことです。
接続のエンドポイントがRequest Routerに向いているような構成があるとします。同じRequest Routerに対して繰り返しリクエストを送信し、jorgeや他のユーザーについてloginsテーブルを参照し続けることになります。キャッシュには全てのPartitionの場所に関する情報が保持されています。ここで、エンジニアの一人がこのRequest Routerにソフトウェアをデプロイしようとすると、約20秒間ダウンしてしまい、キャッシュが消失してしまいます。ソフトウェアのデプロイの度に、情報を再度読み込む必要が生じ、予測不可能なレイテンシーが発生してしまいます。さらに、AZ全体が停止したりネットワークから孤立したりすると、全てのトラフィックを別の場所にフェイルオーバーする必要が生じ、また別の問題を引き起こすことになります。このシステム全体が不安定な状態にあるため、このアプローチは採用できませんでした。私たちはこれをホワイトボードに描いて検討しましたが、採用を見送ることにしました。
そこで、私たちは次のような設計を考え出しました。ここでは、Storage Nodeにディスクがあります。注目すべきは、真ん中にMemDSと呼ばれるディスクを持たない、完全にインメモリのデータ構造があることです。MemDSはそれ自体が並列データベースです。数百のノードがPartitionのメタデータを持っており、それらは数マイクロ秒以内の誤差で、ほぼ同一の状態を保っています。前のスライドを振り返ると、Storage NodeがPartitionの分割や移動を行うたびにPartitionのメタデータを更新する話をしました。私たちはそのような複雑な処理を避けたかったので、Partition Publisherという新しいコンポーネントを追加しました。ここで質問させていただきたいのですが:この設計は機能すると思いますか?「機能しない」とは言わないでください。なぜなら、これが現在実際に稼働しているシステムだからです。もし機能しないと思われる方がいらっしゃいましたら、後ほどお話ししましょう。
アーキテクチャは以下のようになっています:MemDSはストレージを持たず、全てメモリ上にあり、数百のノードが同期に近い状態でデータを保持しています。Partition Publisherの役割は、各テーブルについて、Storage Nodeが持つPartitionとそのキー範囲を継続的にポーリングすることです。 新しいテーブルの作成プロセスを見てみましょう。誰かがControl PlaneでCreateTable()を呼び出すと、数百のMemDSノード全てに書き込むpublish MemDSステップがあります。これにより、Partitionのメタデータが並列で利用可能になります。そのテーブルのPartitionをホストするStorage Nodeに通知しますが、この時点ではこれらのキャッシュには何も書き込まれません。
大規模システムにおける予測可能な低レイテンシーの実現
スケールの大きいシステムを構築するのは困難です。私たちには数万のStorage Nodeがあります。2024年初めにAmazon DynamoDBでテーブルを作成した場合、そのテーブルのPartitionは、最初に配置されたStorage Nodeとは異なる場所にある可能性が非常に高いです。ホストは恐らく退役し、Partitionは分割され、様々な理由で移動されているでしょう - 移動は常に発生しています。Partitionが分割または移動される際、あるStorage Nodeが別のStorage Nodeと通信し、分割や移動の操作の一部であることを伝えます。Partitionは移動されましたが、このプロセスでMemDSは更新されません。やがてPartition Publisherがポーリングを行い、状況を把握することになります。 この時点で、MemDSは古くなっている可能性のあるデータビューを持っている状況になります。
Storage Nodeは、自身が保持しているものを本当に把握しており、複数のPartitionを持っています。これが最終的なシステムオブレコードとなります。これは結果整合性のあるコピーで、その上にキャッシュがあります。しかし、移動前と移動後のPartitionを区別するために、バージョン番号を付けています。テーブルを作成すると、おそらくバージョン1が付けられ、Partitionを移動すると、新しいPartitionにはおそらくバージョン2が付けられます。ここにあった時はバージョン1で、今はバージョン2というわけです。
それでは実際の動作を見てみましょう。 Request Routerが「Jorgeのアイテムを取得して」というリクエストを送信し、キャッシュを確認したところ、バージョン20の最後のリクエストからのデータを持っていました。バージョン20を確認し、Jorgeがそこにいるはずだと判断します。ただし、そのデータを取得してから、パーティションが移動していたことを知りませんでした。Storage Nodeはそのデータをもう持っていないことを認識し、「申し訳ありませんが、そのデータはありません。私はバージョン21で、おそらくこちらに行く必要があります」と応答します。 Request Routerはローカルキャッシュを更新します - この時点ではまだMemDSには触れていないことに注意してください。Request Routerは更新したローカルキャッシュを使用して新しいStorage Nodeに接続し、「バージョン21のJorgeのデータをいただけますか?」と問い合わせます。Storage Nodeがデータを返し、リクエストは処理されます。そしてこのRequest RouterはMemDSにバージョン21を取得すべきだと伝えます。MemDSはPartition Publisherに通知し、Partition Publisherがデータを取得して、この結果整合性のあるデータセットが更新されることになります。
このシステム全体は次のように連携しています:コントロールプレーンがあり、テーブルを作成すると、そのデータをMemDSにプッシュします。なぜなら、テーブル作成後の最初の操作(Partition Publisherが何かを実行する前かもしれません)は、テーブルの読み書きだからです。そのため、MemDSはテーブルが存在することを知っている必要があります。それ以外の点では、MemDSには永続的なストレージはありません。Partition Publisherはリージョン内のすべてのStorage Nodeを継続的にスキャンし、MemDSに公開します。これが結果整合性であり、システムの記録となります。これが信頼できるストアです。Storage Nodeは自身が持っているものを把握しており、Partition PublisherはMemDSに対して結果整合性のある読み取りを行います。このシステムにはバージョニングが組み込まれており、結果整合性のあるキャッシュとなっています。
ほとんどの場合、リクエストはこのキャッシュから処理されます。誰も知らないテーブルに対して初めてリクエストを行う場合は、少し時間がかかるかもしれません - これがテールレイテンシーです。しかし、私たちはロックを避けるために、この完全な結果整合性のあるストアを構築し、古いデータがある場合にはそれを検出して対処できるようにバージョニングを組み込みました。より詳細な情報が必要な場合は、昨年ACMで発表した論文をご覧ください。論文のソースコードも公開されており、この仕組みについて詳しく説明されています。
これがMemDSの基本的なアーキテクチャですが、スケールでの構築は難しく、予測可能性が必要だとお話ししました。 データベースから予測可能なレイテンシーを得たい場合、強く推奨されるパターンはリクエストをヘッジすることです - データベースに2つの接続を確立し、2つのリクエストを送信して、最初に返ってきた結果を使用します。レスポンスタイムの分布が正規分布に従う場合、P99以上のレイテンシーに該当するリクエストを処理する必要があるとき、2つのリクエストを送信することで、両方が遅くなる確率は非常に低くなります。そのため、予測可能なレスポンスタイムが必要な場合は、2つのリクエストを送信します。これはHedgingと呼ばれる一般的なパターンです。これは私たちが発明したものではなく、Googleの人々によって発明されました。Dean氏とBarroso氏による「The Tail at Scale」という論文です。この論文では、アプリケーションを構築する際の有用なパターンとしてHedgeリクエストの使用を提案しています。
予測可能な低レイテンシーを求めるお客様の中には、タイムアウトを調整する方法を取る方もいますが、これは非常に危険な方法です。なぜHedgingの方がより安全なのか、詳しくお話しさせていただきたいと思います。
DynamoDBで多くの場面で使用しているもう一つのパターンは、一定の作業負荷(Constant Work)です。システムは常に同じ定常状態、あるいはできるだけ定常状態に近い状態で動作させることが望ましいのです。キャッシュシステムの例を考えてみましょう。Request Routerとキャッシュを強調して説明します。このキャッシュのヒット率が非常に高いと仮定します。もし背後のMemDSシステムがキャッシュミスだけを処理している状態で、MemDSにソフトウェアをデプロイするなどの理由でキャッシュが大量に無効になった場合、このMemDSシステムは過負荷になって停止してしまいます。このような事態は避けなければなりません。
そこで私たちは、Request Routerに以下のような動作をさせています。まずキャッシュを確認し、ヒットした場合はそのキャッシュ値を使用してリクエストに応答します。そしてストレージからデータを取得する際、常に2台のMemDSサーバーに対して2つのリクエストを送信します。つまり、Request Routerのフロントドアトラフィックが1秒あたり数億リクエストの場合、MemDSは通常のCPU使用率で常に1秒あたり2倍の数億リクエストを処理しています。すべてのキャッシュが失敗しても、MemDSはまったく問題なく処理を続けることができます。私たちは安全性、耐久性、可用性を重視すると言いましたが、これが高可用性システムの構築方法なのです。これは過剰に見えるかもしれませんが、キャッシュを使用する高可用性システムを構築するためのコストなのです。Hedgingと一定の作業負荷について考えることが重要です。
これまでPartitionメタデータのキャッシュについて説明してきましたが、認証と認可情報のキャッシュも使用しています。Resource Policyの実装を使用したことがある方はご存知かもしれませんが、Resource Policyを変更した場合、新しいポリシーが反映されるまでに数分かかることがあります。これは、Control Planeが何万台ものRequest Routerに同期的に更新を配信できないためです。 これらのキャッシュにはTTLが設定されており、数分で期限切れになります。新しいポリシーは可能な限り早くロードされます。大規模なシステムでは、結果整合性を受け入れられるシステムを構築する必要があります。同期的なシステムを構築すると、非常に困難になってしまいます。
プレゼンテーションの冒頭で、私たちがどのように構築したかをお話しすると言いましたが、皆さんが構築するアプリケーションにも参考になるポイントがあります。可能な限り、キャッシュを結果整合性にし、それを受け入れて活用することをお勧めします。それが不可能な場合は同期的なキャッシングを構築することになりますが、これははるかに困難で、パフォーマンスにばらつきが出ます。これを認識し、できるだけ避けるようにしてください。ただし、Request Routerが多くのキャッシングを行っていることを知っておくことは重要です。付け加えておきますが、エンジニアはキャッシュの追加は簡単だと考えがちです。これは非常に危険なので、注意が必要です。
Request Routerへの長期接続を維持することには利点があります。TLSハンドシェイクを毎回行う必要がないだけでなく、Request Router上のキャッシュの恩恵も受けられます。Lambda関数を使用してアプリケーションを構築する場合でも、DynamoDBへの長期接続を維持するパターンを使用するよう心がけてください。これは大きな利点となります。レイテンシーが許容できないというお客様からのお問い合わせを受けることがありますが、デバッグしてみると、接続を作成してリクエストを送信し、すぐに接続を閉じているケースが多々あります。これは、毎回TLSハンドシェイクを行うだけでなく、テーブルのキャッシュを持っていない全く別のRequest Routerにリクエストが送られる可能性があることを意味します。可能な限り長期接続を使用するようにしてください。予測可能なレイテンシーが必要な場合はHedgingを検討し、大規模なシステムを構築する際は、一定の負荷で動作していないシステムは予測不可能な障害パターンを示すことを念頭に置いて、一定の作業負荷を維持するよう心がけてください。
Warm Throughput:新機能の紹介と活用法
私が多くの時間を費やしていることの1つは、お客様との対話です。 DynamoDBでは1つのパーティションあたりのトラフィック制限があるため、お客様からよく「1パーティションで1,000回の書き込みと3,000回の読み取りが可能なのか」という質問を受けます。これは現在の制限値ですが、お客様は自分のシステムが実際にどれだけのトラフィックを処理できるのかを知るために、パーティション数を知りたがっています。
例えば、写真共有サイトを開発しているとしましょう。年間で最も写真共有が多い日がいつか想像できますか?大晦日です。また、インターホンを扱うアプリケーションを開発している場合、最もトラフィックが集中する日は?そう、ハロウィンです。アプリケーションには予測不可能なトラフィックが発生することは分かっています。アプリケーション開発者やオペレーターとして私たちが気にしているのは、トラフィックのスパイクが発生した時にDynamoDBが対応できるかということです。そのため、お客様は「パーティション数を教えてほしい」と言ってきますが、本当の問題は「テーブルがどれだけのトラフィックを処理できるか」なのです。
これまで私たちがこの問題の解決方法として提示していたのは、カラフルなフローチャートを必要とする複雑なプロセスでした。まず、テーブルがProvisioned modeかOn-demand modeかを確認し、On-demand modeだとすると、まずProvisioned modeに切り替える必要があります。そして、希望するスループットを設定します。これはテーブルかインデックスのどちらか一方にしか同時に適用できません。その後、Provision capacityを適切な値まで下げ、On-demandに戻す必要があります。お客様はハロウィンや大晦日に向けてこれを試みましたが、Auto scalingが作動してしまい、Auto scalingが停止して問題が発生してしまいました。
この全体的なプロセスが複雑だったため、今年の先月11月に、私たちはWarm throughputという新機能をリリースしました。まだご覧になっていない方は、ぜひチェックしてみることをお勧めします。 この機能は2つの要素で構成されています:テーブルやインデックスが瞬時に処理できるトラフィックを知らせるAPIと、そのしきい値を設定できるAPIです。以前は実際にProvisioned throughputを上げる必要がありましたが、もうその必要はありません。単純にWarm throughputを上げるだけでよいのです。
APIにいくつかの変更を加えました。まず、DescribeTableがWarm throughputを表示するようになりました。UpdateTableでWarm throughputを変更できるほか、テーブル作成時に設定することもできます。いくつか例を見てみましょう。 これがSQLライクな構文が好きな理由なのですが、customersテーブルをdescribeすると、このテーブルのWarm throughputが12,000 RCU、4,000 WCUで、テーブルがアクティブであることを示す出力が得られます。
CreateTableとUpdateTableには、この機能のサポートが組み込まれています。ここで少し宣伝になってしまいますが、DynamoDB Shellについてお話しさせてください。私がDynamoDBを使い始めた時、APIコールの構文を覚えるのが大変でした。そこで、explainという小さな機能を作りました。これは、コマンドの前に付けることで、そのAPIコールがどのようなものになるかを教えてくれます。例えば、テーブルの作成時にPrimary Keyを指定し、Warm Throughputを設定し、さらにGSIを作成するといったコマンドを実行すると、実際のAPIコールの内容を表示してくれます。テーブルを実際に作成するわけではありません。 この例では、インデックスのWarm Throughputを指定していませんが、それも可能です。 では、explainを外して実際にテーブルを作成してみましょう。テーブルの作成後、APIコールを使ってWarm Throughputを再設定することもできます。
explainコマンドは実行内容を表示し、その下に実際のalterコマンドの実行結果が表示されます。Provisioned Modeテーブルのupdate tableとは異なり、Warm Throughputの変更には時間がかかることがあります。ここでは、12,000から20,000に引き上げていますが、これには約20分かかる可能性があります。この間、ステータスは「updating」となります。インデックスにもWarm Throughputがありますが、インデックスのWarm Throughputは変更していないため、インデックスのステータスは「active」のままです。
ここで「updating」と「active」の状態の違いに注目してください。 テーブルとインデックスのProvisioned Capacityを変更していた頃を覚えている方は、一度に1つしか変更できなかったことを思い出すでしょう。しかし、ここでは両方同時に変更できます。テーブルを確認すると、テーブルのBilling Modeが更新中、またはテーブルのWarm Throughputが更新中である一方で、インデックスは更新されていないことがわかります。Warm Throughputとインデックスの変更を同時に行うことができます。これは、Provisioned Modeテーブルでのオートスケーリングの動作と並行して発生することもあります。
多くのお客様が大規模なアプリケーションを構築する方法と同様の使い方を想定しています。例えば、ハロウィンの玄関チャイムや大晦日の写真アップロードなど、トラフィックパターンを予測する容量計画システムがあれば、テーブルが処理する1秒あたりのリクエスト数を把握できます。2週間前でも好きなタイミングでコールを実行できます。実際にテーブルの容量を上げているわけではなく、Warm Throughputだけを上げているのです。Provisioned Modeで運用している場合はProvisioned Capacityを上げているわけではなく、On-Demand Modeの場合は実際にテーブルに対して行われたリクエストに対してのみ課金されます。
しばらくすると、両方とも新しい容量で「active」状態になっています。テーブルがスケールアップされ、トラフィックが来た時に対応できる状態になっています。これをグラフィカルに示してみましょう。 テスト用にトラフィックを生成するLoad Generatorがあります。ここでは、1日に3つのピークがある変動的なトラフィックを生成しました。オレンジの線は、時間スケジュールに従って発動するオートスケーリングを表しています。テーブルはProvisionedで、Warm Throughputは200ユニットに設定されていました。ユーザーが350に引き上げると、オートスケーリングは引き続き変更を行う一方で、Warm Throughputは350のままとなります。これらは完全に独立した操作なのです。
Global TablesとMulti-Region Strong Consistency
これはOn-demandテーブルでも同じシナリオです。 この図では、Warm Throughputは約270から始まっています。On-demandテーブルではAuto Scalingは関係なく、実際に行ったリクエストに対してのみ料金が発生します。トラフィックがWarm Throughputの数値を超えた時、DynamoDBはパーティションの分割を実行しましたが、それには少し時間がかかりました。Warm Throughputは上昇し、その水準を維持しました。このスパイクの前に手動で対応することもでき、その場合はスロットリングや顕著なTail Latencyの変化を避けることができました。例えば、ハロウィン時期のトラフィックであれば、事前にWarm Throughputを引き上げておくことで、必要な時にテーブルが確実に対応できる状態になっていたはずです。テーブルは必要な時にいつでも準備が整っているのです。
ですので、可能な限り従来のやり方から脱却していただきたいと思います。 CloudFormationをお使いの場合、Warm Throughputのサポートは既に利用可能です。現在、Terraformチームと協力して実装を進めているところです。テーブルの作成時や更新時にWarm Throughputを指定してください。可能な限り自動化することをお勧めします。Provisioned Modeのテーブルで使用する場合の素晴らしい点は、同時更新が可能で、Auto Scalingに影響を与えないことです。
今朝のKeynoteをご覧になった方は、 Global TablesでMulti-Region Strong Consistencyのサポートを発表したことをご存知でしょう。そこで、TTLについての話題の代わりに、より多くの方が興味を持っているであろうこの機能の仕組みについてお話ししたいと思います。まずはGlobal Tablesについて説明しましょう。2017年にGlobal Tablesをリリースしましたが、これは非同期のGlobal Tablesでした。2019年には非同期Global Tablesの第2バージョン、GT Version 2をリリースしました。テーブルのレプリカは好きなだけ作成でき、全リージョンに1つずつ配置することも可能です。Active-Activeなので、どのリージョンでも書き込みができます。競合解決は最後の書き込みを優先する方式を採用しています。3つのリージョンにレプリカを持つテーブルで、リージョン1とリージョン2で2つの書き込みが行われた場合、より後に行われた方の書き込みが残ることになります。
このアーキテクチャの結果として、リージョン内では強力なRead-after-Write保証が得られます。書き込みを行ったのと同じリージョンでConsistent Readを実行すれば、確実にその内容を読み取ることができますが、リージョン間ではRead-after-Write保証はありません。レプリケーションは結果整合性なので、リージョン1で書き込みを行い、その書き込みが他のリージョンに伝播する前にリージョン1が分離されてしまうことがあり得ます。この場合、データは1つのリージョンにしか存在しません。このような状況でネットワーク分離が発生し、アプリケーションが別のリージョンに切り替わる必要がある場合、Recovery Point Objectiveはゼロより大きくなります。
図を使って説明させていただきます。 ここにリージョン1から5までの5つのリージョンがあります。最初のリージョンで書き込みを行うと、これはリージョナルな書き込みとなり、2つのAZに書き込まれ、3つ目のAZには結果整合性で書き込まれます。その後、他のリージョンへのレプリケーションが行われます。書き込みを行った後、同じリージョンで結果整合性のReadを実行した場合、これはリージョナルな書き込みで2つのAZに書き込まれているため、このReadが3つ目のAZに向かう可能性があります。書き込みから数ミリ秒以内であれば、古いデータが返される可能性があります。強力なRead-after-Write整合性保証が必要な場合は、リージョン内でConsistent Readを実行する必要があります。
さて、レプリケーションの前に書き込みを行い、ここでConsistent Readを実行したとします。データはまだ転送されていないため、そのデータを見ることはできません。レプリケーションが完了した後にConsistent Readを実行すると、そのデータを取得できます。レプリケーションによってConsistent Readが保証されるわけです。一方、Eventually Consistent Readでは最新のデータが取得できない可能性があります。なぜなら、リージョナルな書き込みは2つのAvailability Zoneに書き込まれるため、Eventually Consistent Readが3番目のAZにアクセスした場合、データが取得できない可能性があるからです。また、レプリケーションが書き込みの前に行われ、Eventually Consistent Readを実行した場合も、データは取得できません。
これまでの例では、特定のアイテムへの書き込みと、同じアイテム(同じPrimary Key)の読み取りを想定していました。
ここでは、特定のPartition Key、つまり特定のPrimary Keyに書き込みを行い、別のリージョンで同じPrimary Keyに書き込みを行う場合を考えます。時間は右に進んでいきます。これは少し後で発生します。最初に起こるのは、このリージョナルな書き込みがレプリケートされることです。その後、もう一方の書き込みもレプリケートされます。後者の書き込みが新しいため、最終的にはこちらが残ることになります。しかし、2番目の書き込みがレプリケートされていない間に最初の書き込みがレプリケートされる短い期間があります。システムの状態は最終的に収束しますが、収束期間中は、読み取りによって古いデータや新しいデータが返される場合があります。
これは多くのアプリケーションにとって十分な動作ですが、リージョン間での強い整合性を必要とするアプリケーションも多くあります。そこで、本日発表されたのがGlobal TablesのMulti-Region Strong Consistencyです。比較してみましょう:両方のケースで書き込みは非同期でレプリケートされます。はい、これは誤植ではありません。書き込みは非同期でレプリケートされます。しかし、最終的な状態は決定論的であり、キーレベルでのユニバーサルな書き込み順序と、グローバルなRead-After-Write整合性が得られます。グローバルに一意のシーケンス番号を生成したいアプリケーションを考えてみましょう。Strongly Consistent Global Tablesを使えば、それが可能になります。2つのリージョンが同じシーケンス番号を生成することはなく、必要に応じて連続したシーケンス番号を得ることができます。
Multi-Region Strong Consistency、つまりmRSCは発音しやすいように、私はこれを単に「Mercy」と呼ぶことにします。非同期のGlobal Tablesでアプリケーションを構築した経験があれば、これが本当に慈悲深い(Merciful)機能だとわかるでしょう。そう、Mercy Global Tablesです。覚えておくべき点が1つあります:Eventually Consistentな非同期Global Tablesでは、好きなだけレプリカを持つことができます。一方、Mercy Global Tablesの場合、現時点では3つのリージョナルコピーまでに制限されています。これは将来変更される予定ですが、現時点では3つです。実装方法としては、下部に新しい構造を追加しました。これはMulti-Regional LogまたはMRJ(Multi-Regional Journal)と呼ばれています。DynamoDBに関する他のプレゼンテーションでも、この同じ構造について聞くことになるでしょう。
Multi-Regional Journalによる強力な整合性の実現
この Multi-Regional Journal は、複数のリージョンにまたがる厳密な順序を持つ、依存関係のみのログです。使用方法としては、最初のリージョンで書き込みを行うと、その書き込みはまずジャーナルに発行されます。ジャーナルがリージョンにコールバックを送信すると、このコールバックが200レスポンスをトリガーします。つまり、書き込みを行うと、最初にジャーナルへの送信を試みます。ジャーナルのコールバックが200レスポンスをトリガーし、同時に他のリージョンにもコールバックが送られます。先ほど書き込みは非同期で送信されると説明しましたが、これがその非同期の動作です。書き込みは元のリージョンからログを経由して3つのリージョンに送られ、そこで200レスポンスを受け取って処理が進みます。 ジャーナル内で実際に起こることを説明すると、同じ3つのリージョンにジャーナルがあり、データが2つのリージョンに保存されるとすぐに、ログはデータが2つのリージョンに永続的に記録されたと判断します。3番目のリージョンへの書き込みが行われる前に、これらのリージョンの1つがオフラインになったとしましょう。データは別のリージョンにあります。これが、シングルリージョンの分離でRPO zeroを提供できる理由です。データが2つのリージョンに永続的に書き込まれ、コールバックが発生し、3番目のリージョンには最終的に書き込まれて、そこで200レスポンスを受け取ります。これが通常のリージョナル書き込みです。リージョナル書き込みとは何でしょうか?2つのAZへの書き込みです。2つのAZで完了すれば、それでOKです。書き込みはリージョン1の2番目のAvailability Zoneから開始されます。
ログに書き込まれ、ここでコールバックが来ます。2つのAZに書き込まれ、その後、3番目のAZには遅延して書き込まれます。先ほど、グローバルな読み取り後の書き込み整合性があると言及しましたので、それについて説明する必要があります。
まず読み取りについて説明しましょう。強整合性のあるグローバルテーブルに書き込みを行い、その後読み取りを実行した場合、最新のデータを取得できることは保証されているでしょうか?いいえ、これはリージョナル書き込みです。まだデータを持っていない3番目のAZにアクセスする可能性があるため、この時点で古いデータが返される可能性があります。このログ全体の意味は何かと疑問に思うかもしれません。 厳密に順序付けられた書き込みを提供すると説明しました。書き込みがABCの順序でログに送信されたとしましょう。私たちが保証するのは、リージョンでのコールバックが全く同じ順序 - Aの後にB、そしてCという順序で行われるということです。それらの間の時間は異なる可能性がありますが、順序は同じであることが保証されています。これが厳密に順序付けられたログです。
では、 強力なクロスリージョンの読み取り後の書き込み整合性を提供するためには、どのようにこれを使用するのでしょうか?新しい概念を導入しましょう:ハートビートです。書き込みを行うと、それはログに送られ、コールバックがあり、200レスポンスを受け取って完了します。整合性のある読み取りを行う場合、リージョンはその読み取りを提供するために、完全に追いついていることを知る必要があります。そこでログにハートビートを送信します。ハートビートはここで永続的に保存されないメッセージですが、コールバックを生成します。整合性のある読み取りのハートビートがここで受信されると、リージョンは全てを受信したことを知り、200レスポンスを提供できます。これが、強い順序付けが価値を持つ場所です。なぜなら、この書き込みがハートビートの前にあることが保証されているからです。
少し難しい例から始めましょう。書き込みを行うと、その書き込みはログに送られ、コールバックが発生します。 これで書き込みは3つのリージョン全てで完了します。 別のリージョンで同じキーに対する別の書き込みがあります。そのリージョンはログエントリを生成し、順序はAの後にBとなります。したがって、コールバックは各リージョンで同じ順序でなければなりません。しかし、コールバックが発生する前に、誰かが整合性のある読み取りを行います。このコールバックはまだ完了していません。
順番としては、1回目の書き込み、2回目の書き込み、そして整合性のある読み取りがあります。Journalはどのように役立つのでしょうか。最初の書き込みがあり、2回目の書き込みがあり、これらは順序が保証されています。コールバックも順序が保証されています。 整合性のある読み取りはハートビートを生成しますが、これはログには永続化されません。しかし、ハートビートのレスポンスが返ってくると、そのリージョンは追いついていることを認識し、整合性のある読み取りを提供できます。したがって、この整合性のある読み取りには書き込みBが含まれることが保証されます - これがリージョン間での強力な読み取り後の書き込み保証です。
その後、新しい書き込みが発生し、その書き込みがログに記録されます。ログ内での書き込みの順序は、各リージョンで実現される順序と完全に一致していることに注目してください。非同期であるため、タイミングは若干異なる可能性がありますが、ハートビートによってリージョン間での強力な読み取り後の書き込み保証を実現しています。プレゼンテーションの前半で、可能な限り非同期で処理を行うことをお話ししました。どんなスケールでも予測可能なレイテンシーを実現したい場合は、非同期で処理を行います。これらは非同期の書き込みですが、順序が保証されています。これが強力な読み取り後の書き込み保証を実現する要因です。
もう一つ例を挙げましょう。書き込みを行い、 別のリージョンで整合性のある読み取りを行います。ここでも、ハートビートによってリージョンが追いついていることが保証され、整合性のある読み取りを提供できます。この整合性のある読み取りは、その時点までのすべてが含まれることが保証されており、つまりこの書き込みがここに反映されています。書き込みの前にここで整合性のある読み取りを行うと、ハートビートが生成されます。ここでの順序保証により、完了がハートビートの前に来ることが保証されます。したがって、この整合性のある読み取りにはこの書き込みが含まれます。リージョンに書き込まれていなくても、ログに記録された順序が各リージョンでの順序となります。
Multi-Region Journal (MRJ)に基づいてマルチリージョンの強力な整合性を構築しました。これは私たちが使用する新しい内部構造で、厳密な順序を提供する追記専用のログです。 お客様がこれを使用すると想定されるのは、強力なマルチリージョンの整合性保証が必要な場合です。この時点で、これらのテーブルには3つのレプリカがあります。任意のリージョンで書き込みを行い、成功レスポンスを受け取ると、データが2つのリージョンで永続的に記録されることが保証されます。おそらくログとして2つのリージョンに記録され、その後非同期でテーブルに書き込まれます。しかし、1つのリージョンが分離された場合でも、RPO保証はゼロのままです。金融取引を扱うアプリケーションを構築している方々は、アプリケーションの一部でこれを使用したいと考えるかもしれません。グローバルに保証された強力な整合性が得られます。整合性のある読み取りを行えば、強力な読み取り後の書き込み整合性がグローバルに保証されます。
以上が、私たちが強整合性のあるGlobal Tablesを構築した方法の概要です。強整合性のあるGlobal Tablesとその想定される使用方法について1時間かけて説明するセッションがあります。確かDAT8425だったと思います。そちらのセッションにご参加ください。私たちは毎年、学んだことを皆様と共有し、私たちが提供している予測可能な低レイテンシーを活用したアプリケーションの構築にお役立ていただけるよう、このようなプレゼンテーションを行っています。こちらは過去の発表の一部です。最初のものは私がAWSに入社する前に視聴したもので、その後のものは私が行った発表です。一つはAlex DeBrieと一緒に、そしてもう一つは昨年行ったものです。Amazon DynamoDBに関するその他の興味深いプレゼンテーションもあります。こちらがこのre:Inventで行われるセッションのリストです。カタログで検索すれば見つけることができるはずです。本日は皆様、ご参加いただき誠にありがとうございます。多くのセッションの選択肢がある中で、このセッションを選んでいただき感謝いたします。最後に、フィードバックフォームへのご記入をお願いいたします。
※ こちらの記事は Amazon Bedrock を利用することで全て自動で作成しています。
※ 生成AI記事によるインターネット汚染の懸念を踏まえ、本記事ではセッション動画を情報量をほぼ変化させずに文字と画像に変換することで、できるだけオリジナルコンテンツそのものの価値を維持しつつ、多言語でのAccessibilityやGooglabilityを高められればと考えています。
Discussion