⛅️

DynamoDBはバッチ処理よりストリーム処理との相性が良いという話

2023/07/09に公開4

この記事について

本記事は、筆者が普段AWSの各種サービスを使って感じた感想・気づきをもとに、クラウドアーキの設計やサービスのより良い使い方Tipsを考察するシリーズです。
第一弾は、日頃DynamoDBを使っていて思った感想「DynamoDBをうまく活用するにはストリーム処理・リアルタイム処理のアプローチを取ると良いのでは?」について深く掘り下げたいと思います。

使用する環境・バージョン

  • 2023/7/9時点で提供されている機能に基づき考察

読者に要求する前提知識

  • RDS, Aurora, DynamoDBのサービス概要を理解していること

TL;DR

  • DynamoDBに対して、ある特定の時刻に処理をまとめて実行するようなバッチ処理をするには現状以下のようなネックがある
    • データのinput・複製という観点では、DynamoDBの既存のテーブルに大量のレコードを一度にPUTする手段が乏しく、リードレプリカの機能もない
    • テーブル内のデータを分析処理するという観点では、複雑な検索クエリがしにくく、かつバッチ処理となると事実上のテーブル全Scanとなることが多い
  • そのため、データの発生や変換処理をストリームと捉えて、都度処理するリアルタイム的なアプローチを取るのが良い

考察するケース

まずは、DynamoDBを利用して以下のような処理を行うことを考えてみましょう。

ケース1: 投票システム

ウェブページ経由でユーザーに投票してもらった結果が、以下のような投票テーブルに入っているとします。

UserID vote Date
user1 A 2023-06-01
user2 B 2023-06-05
user3 B 2023-07-01
user4 A 2023-06-20

これに対して、1日ごと、1週間ごとといったある一定周期で投票結果の集計を行うことを考えます。

  • Aへの票数: xxx票
  • Bへの票数: yyy票

各候補の得票数を数え上げるためには、集計バッチ処理で投票テーブルの中身を全Scanする必要が出てきますが、これはコスト・パフォーマンス両面の観点から望ましくありません。
特に、投票テーブルへのCRUDが遅くなる・キャパシティユニットが枯渇するといったアクシデントはそのままユーザー体験の悪化に直結してしまうため、ユーザーが使う投票システムに連動しているテーブルを直接バッチ処理で参照することは危険と言えるでしょう。

ケース2: テーブル内データを利用したバッチ処理

システムやアプリを利用しているユーザー情報が、以下のようなユーザーテーブルに格納されているとします。

UserID UserName Email
1 A-san aaa@example.com
2 B-san bbb@example.com
3 C-san ccc@example.com

テーブル内に格納されているメールアドレスのデータを使って、1日ごと、1週間ごとに全ユーザーに対してメールを送信したいというバッチがあったとしましょう。
とある1人のユーザーのメールアドレスを調べること自体はQuery操作で可能ですが、バッチ処理の性質上それを全ユーザーに対してやると考えると、実質的にはテーブル全Scanと同等の処理が要求されてしまいます。
システムを利用しているユーザーから登録情報の参照・変更を随時受け付けるたびに、このテーブルへのCRUD処理が行われます。そのため、このテーブルへの全Scanはユーザー体験を損なう可能性が高いです。

解決策の模索

「とあるテーブルに対してバッチで大量アクセスするのを防ぎたい」という要件に対して、考えられるアプローチを挙げてみます。

  • リードレプリカの作成
  • コピーテーブルの作成

リードレプリカの作成

RDSやAuroraの場合は、同じデータを持つリードレプリカを簡単に作成することができます。

  • RDSの場合、マスターに書き込まれたデータは非同期でリードレプリカインスタンスに反映されます。そちらに対してアクセスすることで、マスターに影響を与えることなく、読み取り負荷が高い分析用・バッチ用処理を走らせることが可能になります。
  • Auroraクラスターの場合、リードレプリカが作れるのはもちろんですが、カスタムエンドポイントを利用することで、オンラインクエリでアクセスするリードレプリカと分析クエリでアクセスするリードレプリカを分離するといったことも可能です。

しかしDynamoDBの場合、リードレプリカに相当する機能は現状提供されていません。
近いものですとグローバルテーブルがありますが、これは

  • マルチリージョン必須
  • 全てに対して書き込みが可能

ということで、やりたいことに対してtoo muchなところがあります。

コピーテーブルの作成

同じデータを持つ別テーブル・別クラスタを作るという手段です。
リードレプリカとの違いは、オリジンとなる元のテーブルとのデータ連携機能がない点です。

そのため、

  • 必要な時にコピーテーブルを作り、不要になったら削除を都度繰り返す
  • オリジナルに加えられた変更をキャプチャし、コピーテーブルに反映させる機構をカスタムで作りこむ

のアプローチのうちどちらかを取ることになります。

必要な時に作成→不要になったら削除

既存のテーブルの内容をもとに新しいテーブルを作るには、バックアップやスナップショットからの復元を主に行うことになります。

  • RDSの場合、DBスナップショットからインスタンスを復元することができます。
  • Auroraの場合
    • バックアップやスナップショットからクラスターを復元することができます。
    • Aurora特有の機能として、クローンクラスターというものを作ることができます。
  • DynamoDBの場合
    • バックアップから新しいテーブルを復元することができます。
    • 既存のテーブルのデータをS3にexportして、新規テーブル作成時にそのデータをRCU消費なしにimportする機能が存在します。

ただし、ここで挙げた機能は全て「復元時に新インスタンス・クラスター・テーブルを作る」という挙動になります。
そのため、バッチ実行のたびにこの作業を繰り返すことを想定するのであれば、使い終わったコピーテーブルは削除するという作業が必要になります。
もし前日のバッチ処理のために作ったコピーテーブルTableXXX-Copyが存在する状態で、次の日のバッチ処理にて「現時点でのデータを新規テーブルTableXXX-Copyに復元する」という処理を試みると、「TableXXX-Copyという名前のテーブルは既に存在する」というエラーが出て処理に失敗してしまうからです。

「コピーの作成→必要無くなったら削除」という処理をバッチ処理のたびに都度繰り返すという条件下では、コピーのテーブルをIaC管理下に置くことは難しいでしょう。
IaCのapplyは本来インフラ構成に変更が加わったときのみに行うものであり、「元テーブルとそれのコピーテーブルが存在する」という構図が変わっていないのにも関わらずバッチ処理のためだけに都度コピーテーブルの作成・削除のapplyを行うべきではありません。
しかし、視点を変えてバッチ処理を実現するアプリコードの立場からすると、処理が走る時にコピーテーブルはTableXXX-Copyという名前で常に存在するという前提のもとで動くわけですから、その常に存在していて欲しいTableXXX-CopyテーブルがIaC管理下にないというのは違和感があります。

そのため「必要な時に作成→不要になったら削除」という方式は、機能面では問題ないように見えても非機能面では少しイマイチな部分があるのです。
RDSやAuroraに関してはリードレプリカの機能が整っていますので、分析バッチ処理のためだけにコピーテーブルを作るという回りくどい方法をとらなくても問題ありません。
しかし、DynamoDBの場合はリードレプリカはありませんので、「分析バッチのために別テーブルを作りたい」という要件に対しては残る最後のアプローチ方法を取ることになります。

変更をコピーテーブルに都度反映

変更をコピーテーブルに反映するためには、まずオリジナルのテーブルにどのようなデータ変更が入ったのかを把握する仕組みが必要になりますが、ここに関しては向き不向きがはっきりと分かれる印象です。
RDS/Auroraに用意されている変更キャプチャはあくまで監査目的のものが主であり、加わった変更に応じて追加で処理をすることに関してはDynamoDB Streamsが一番得意です。

  • RDSの場合、監査ログをCloudWatchに送る仕組みがあるのみです。
  • Auroraの場合、データベースアクティビティストリームを利用することで実行クエリをKinesisに送信することができますが、あくまで監査ログ的な使い方を想定されているものであり、ここで抽出したクエリを別クラスターにそのまま流すといった利用には不向きです。
  • DynamoDBの場合、DynamoDB Streamsによってテーブルデータに加わった変更をキャプチャすることができ、それをLambdaで受け取って追加処理を行うことも容易に実現できます。

また、DynamoDB Streamsで抜き出した変更キャプチャを別テーブルに反映させる際には、元テーブルと全くそっくりのスキーマ構造にしなくても良いという柔軟さがあります。
例えば冒頭のケース1の場合、投票テーブルにレコードが増えるたびに、集計用にスキーマを最適化した得票サマリテーブルのレコードを更新するといった仕組みにすることで、日時の投票集計バッチの際に投票テーブルへの全Scanを避けつつ、パフォーマンスの良い処理をすることが可能になります。

<投票テーブル>

UserID vote Date 備考
user1 A 2023-06-01
user2 B 2023-06-05
user3 B 2023-07-01
user4 A 2023-06-20
user5 A 2023-07-08 このレコードが増えた

<得票サマリテーブル>

vote num 備考
A 2→3 vote=Aのストリームレコードを受信したため、numを+1する
B 2

また、DynamoDBに複数個の書き込みクエリを実行させるBatchWriteの機能でまとめることができる処理は最大25個までであり、1回のBatchWriteで16MBしか扱うことができません。
そのため「ある程度データが溜まったらまとめて処理」ではなく「データが発生したら都度処理」といったこまめなPUTが必要となり、その点でもDynanoDBとストリームとの相性はいいと言えます。

まとめ

DynamoDBに格納されたデータに対して

  • 複雑な分析をしたい
  • 様々な条件で検索を行えるようにしたい

という要件を見据えるのであれば、オリジナルのデータが入った元テーブルとは別に要件に合わせた別テーブルを用意することが望ましく、それを実現するためにはDynamoDB Streamsで変更をキャプチャして都度処理することになります。

DynamoDBの性質上、

  • リードレプリカの機能が現状提供されておらず、同様の機能を望むのであれば別のテーブルを作成し、DynamoDB Streamsを利用して都度データ変更を反映する必要がある
  • パーティションキーとソートキーのみの構成では多種多様で複雑な検索を実現することができず、検索要件に合わせてデータを集計・変換した別テーブルを作りたいというケースが珍しくない
  • BatchWriteにもアイテム数の上限があり、かつS3からのデータimportは新規テーブルに限られるという現状では、既存テーブルに大量のデータをPUTするのは不得意

であるので、なおさらDynamoDB Streamsを用いたリアルタイム処理のアプローチが有効かつ相性がいいと言えるでしょう。

DynamoDB Streamを有効化すること自体にはコストはかからず、実際にストリームの内容を参照するGetRecordsAPIコールに対して課金されます。[1]
また、DynamoDBテーブルを用いてユーザーに対するリアルタイムサービスを提供するチームと、テーブル内データを利用して検索・分析を行うチームが別であると、分析のためだけにストリーム有効化を頼みにいくことにハードルがある場合もあると思います。
そのため、DynamoDB Streamsは「使うかわからなくてもとりあえず有効化しておく」という設計ルールにしておくのも一つの選択肢なのかなと思います。

脚注
  1. https://aws.amazon.com/jp/dynamodb/pricing/on-demand/ ↩︎

Discussion

えんぶんえんぶん

DB初学者なので教えてください。

しかしDynamoDBの場合、リードレプリカに相当する機能は現状提供されていません。

DynamoDBのセカンダリインデックスはこれに該当しないのでしょうか?

zyakezyake

正しいです、実質的に非同期レプリカとして利用できます。

sakojunsakojun

複雑なクエリに向いてないのでバッチや分析にあまり向かないのは同意です。

リードレプリカに関しては、リードレプリカという仕組みに頼らずとも(書き込みも含めて)柔軟にスケールできるのはメリットですし、バッチの時だけキャパシティを増やしてもいいと思います。

重い・優先度の低いクエリをどう分離するかという点だと、AuroraのカスタムエンドポイントやRedshiftのWLMとの比較になるかと思います。
GSIを設定して読取りキャパシティを適切に割り当てることでDynamoDBでも似たような効果が期待できそうです。(私自身はこのような使い方をしたことがないため未検証です)
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-indexes-gsi-replica.html