S3とDynamoDBを二重の正本にしない

この記事は、筆者が構成を決め、内容と参考資料を確認したうえで、構成の整理と本文の作成にLLM(OpenAI Codex)を使用しています。記事内容の責任は筆者にあります。

動画ストリームを解析する、小さなプロトタイプが欲しくなったとします。

カメラから届く映像を処理して、その結果をあとから確認できるようにします。解析した動画は短いファイルに分けて保存します。最初は、生成した動画をAmazon S3へ置くだけでもよさそうです。S3のコンソールを開けばファイルは見つかりますし、必要なファイルをダウンロードすれば再生もできます。

プロトタイプが動くと、次はもう少し使いやすくしたくなります。

  • どの日に動画があるのか、カレンダーで確認したい
  • 指定した時刻の動画を続けて再生したい
  • 動画と同じ時刻のセンサー情報や解析結果を重ねて見たい
  • 機器ごとの保存量も知りたい

こうなると、S3にファイルを並べただけでは画面を作りにくくなります。S3のオブジェクト一覧を毎回すべて調べるより、動画の開始時刻、長さ、機器、保存先などをAmazon DynamoDBへ登録しておく方が、画面やAPIから検索しやすそうです。

動画本体をS3へ、検索情報をDynamoDBへ保存する構成です。ここまでは自然に思えます。ところが、この二つのどちらを正しいデータとして扱うかを決めずに作り始めると、保存や削除の失敗処理が急に難しくなります。

S3とDynamoDBへ同時には書けない

このプロトタイプで、短い動画ファイルを保存するときに、次のように処理するとします。

  1. 動画ファイルをS3へ保存する
  2. 動画の開始時刻、長さ、S3のキーをDynamoDBへ保存する

両方とも成功すれば問題ありません。ですが、S3への保存に成功したあと、DynamoDBへの書き込みに失敗することがあります。このとき、動画はあるのに検索結果には出てきません。

順序を逆にしても同じです。DynamoDBへ先に登録してからS3への保存に失敗すると、検索結果にはあるのに、再生しようとすると動画がありません。

DynamoDBには複数の項目をまとめて更新するトランザクションがあります。しかし、そのトランザクションにS3へのファイル保存を含めることはできません。S3とDynamoDBを一つの操作として、両方成功するか両方失敗するかにする仕組みではないからです。

AWSのDynamoDBトランザクションの説明: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html

保存だけなら、失敗した方を再試行すればよさそうです。ですが削除まで考えると、だんだん複雑になります。

削除しようとすると、さらに難しくなる

動画を削除するときも、S3のファイルとDynamoDBの検索情報を消す必要があります。

先にDynamoDBから消すと、S3の削除に失敗した場合に、検索できない動画が残ります。先にS3から消すと、DynamoDBの削除に失敗した場合に、存在しない動画が検索結果に残ります。

これを厳密に扱おうとすると、例えば次のような状態が欲しくなります。

  • 削除受付済み
  • S3削除中
  • DynamoDB更新中
  • 一部失敗
  • 再試行待ち
  • 削除完了

さらに、削除した動画が機器側の再送によって復活しないように、削除済みであることを一定期間記録する必要が出てくるかもしれません。大量の動画を削除するなら、どこまで処理したかを示すチェックポイントも必要です。

もちろん、このようなワークフローをきちんと作ることはできます。ただ、小さなプロトタイプで動画を保存して検索したいだけだったのに、二つのストレージを同じ状態に保つためのシステムが大きくなっていきます。

ここで、そもそもS3とDynamoDBの両方を正本として扱う必要があるのかを考えた方がよさそうです。

再生成できないものを正本にする

動画本体が失われると、DynamoDBに開始時刻やファイル名が残っていても動画を再生できません。一方、動画本体がS3にあり、そのS3キーから機器や撮影時刻を読み取れるなら、検索用の情報は作り直せます。

この場合は、次のように役割を分けると具合がよくなります。

  • S3: 動画が存在するという事実を持つ正本
  • DynamoDB: 画面やAPIから検索しやすくするためのread model

read modelは、正本を利用目的に合わせて読みやすい形へ変換したデータです。例えば、次のようなものです。

  • どの日に動画があるかを示すカレンダー
  • 連続して再生できる時間範囲
  • 機器ごとの最新データ
  • 保存件数や保存量の集計

これらは表示を速くするために必要ですが、動画本体そのものではありません。壊れたり消えたりしても、S3を調べて再生成できるようにします。

S3のキーをデータ構造として使う

再生成できるようにするには、S3のオブジェクトを見ただけで、最低限の情報が分からなければなりません。

例えば、次のような情報をS3キーやオブジェクトのメタデータへ持たせます。

  • データ形式のバージョン
  • データを生成した機器の識別子
  • 記録を開始したUTC時刻
  • 実際の記録時間
  • 同じデータを識別する安定したID

ファイル名に現在時刻と乱数だけを入れてしまうと、あとで一覧を取得しても、そのファイルが何を表しているのか分かりません。別のデータベースを読まないと意味が分からないS3キーでは、そのデータベースを正本から外せなくなります。

同じ入力から同じS3キーを作れるようにしておくことも大切です。保存処理を再試行したときに別の動画として増殖せず、すでに同じものがあれば同じ結果として扱えるようになります。

S3では、既存オブジェクトを上書きしない条件付き書き込みも利用できます。 https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html

イベントで早く反映し、あとから正本で確認する

S3へ動画を保存したあと、S3 Event NotificationsやAmazon EventBridgeを使って処理を起動し、DynamoDBのread modelを更新できます。

この方法なら、動画を保存する処理はS3への保存成功だけを確認すれば済みます。DynamoDBの更新が一時的に失敗しても、動画保存まで失敗したことにはしません。

ただし、イベントを新しい正本にしてはいけません。S3のイベント通知は少なくとも1回届くように設計されているため、同じイベントが複数回届くことがあります。また、発生した順番に届くことも保証されません。

AWSのS3 Event Notificationsの説明: https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html

そのため、イベント処理は同じ通知を何度処理しても結果が変わらないようにします。更新前に現在のS3オブジェクトを確認したり、同じキーについて新しいイベントだけを採用したりする方法もあります。

さらに、定期的にS3の一覧やinventoryを確認して、read modelとの差を修復できるようにします。

イベントは素早く反映するために使い、正しい状態はS3から再構築できるようにする、という分担です。

画面も多少の遅れを受け入れる

read modelはS3から少し遅れて更新されることがあります。そのため、画面側もread modelが常に完全だという前提にはしません。

例えば、カレンダーの「この日にデータがある」という表示は、日付を選ぶための手がかりです。それを押したあとに実際の動画を取得するときは、S3に現在存在する動画から結果を決めます。

反対に、カレンダーに表示されたあとで保存期間が終わり、動画が削除されることもあります。検索結果を返した時点で、将来もその動画が存在することまで保証する必要はありません。取得時に存在しなければ、最新状態を読み直します。

最近追加されたデータだけはS3を直接確認し、過去の確定した期間はread modelを使う方法もあります。すべてを毎回S3から探すのではなく、更新頻度と必要な正確さに応じて読み方を変えます。

S3のPUT、DELETE、その後のGETやLISTには強い整合性があります。現在のオブジェクトを確認する場所として使いやすくなっています。 https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#ConsistencyModel

DynamoDBが不要になるわけではない

ここで言いたいのは、DynamoDBを使わない方がよいということではありません。

日時や機器を指定した検索、カレンダー表示、処理状態の確認などにはDynamoDBが便利です。利用者が入力した説明や承認状態のように、S3の動画から再生成できない情報は、DynamoDB側がその情報の正本になります。

大切なのは、データの種類ごとに正本を決めることです。

  • 動画が存在するか: S3
  • 動画から計算できるカレンダーや集計: 再生成可能なread model
  • 利用者が入力した名前や判断: その情報を保存したデータベース

一つのシステムに正本が一つしかあってはいけない、という意味ではありません。同じ事実について、二つの正本を作らないということです。

最初に失敗時の組み合わせを書いてみる

S3とデータベースを組み合わせる必要が出たら、実装を始める前に、保存と削除の順序を書き出してみるのがよさそうです。

  1. 最初の書き込みだけ成功したらどうなるか
  2. 再試行すると重複しないか
  3. 削除の途中で停止したら、どちらから再開するか
  4. 検索用データをすべて失っても、再構築できるか
  5. 最後に正しい状態を判断する場所はどこか

この問いに答えるために、たくさんの処理状態や補償処理が必要になるなら、二つのストレージを二重の正本として扱っているかもしれません。

大きなデータをS3へ、検索情報をDynamoDBへ保存する構成は、よくあるものです。だからこそ、「両方へどう書くか」より先に、「どちらからもう一方を作り直せるか」を決めておくと、保存、削除、障害復旧の仕組みをかなり簡単にできると思います。