0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS SAP対策】「大量の小さなレコード」「期限切れデータの自動削除」にRDSが向かないのはなぜか

0
Posted at

はじめに

どーも!shihopowerです。

AWS SAP の勉強をしていると、「世界中のデバイスから大量の小さなレコードを取り込む」「一定期間後にデータを削除する」といった要件のストレージ選定問題によく出会います。
こうした問題では、選択肢に出てくる RDS はたいてい不正解で、解説には「RDSは大量レコードを取り込むとパフォーマンスが著しく低下する」と書かれています。

…でも、「なぜ?」という部分がいまいち腹落ちしませんでした。

  • なぜRDSは大量書き込みに弱いのか?
  • なぜ期限切れデータの削除を cron ジョブでやるのが悪手なのか?
  • そもそも「パフォーマンス低下」って具体的に何が起きているのか?

このあたりを、AWS公式ドキュメントとブログを根拠にしながら、DBの内部動作(WAL、fsync、インデックス更新など)まで掘り下げて整理してみました。
同じところで詰まっているSAA/SAP対策中の方の参考になれば嬉しいです。

この記事の対象読者

  • AWS SAA / SAP の対策中で、ストレージ選定問題に苦戦している方
  • 「RDSが大量書き込みに弱い」と言われても、内部で何が起きているかピンとこない方
  • DynamoDB の TTL がなぜ便利なのか、改めて言語化したい方

結論(忙しい人向け)

  • RDSの「1件のINSERT」は実は WAL書込・データページ更新・インデックス更新・fsync など複数のI/Oに展開される
  • 巨大テーブル + 大量INSERTでは、IOPS上限・インデックス更新負荷・ロック競合がすべて重なる
  • 期限切れデータの削除を cron で実装するのは追加運用負荷。DynamoDB の TTL なら全自動で削除されてキャパシティも消費しない

目次


1. 出題されたのはこんな問題

模擬試験で出てきたのは、IoT/通信系のストレージ選定問題でした。要件をざっくりまとめると次のような感じです。

  • 世界中のデバイスから 大量の小さなレコード を取り込む(毎分ベースで数百万件規模)
  • レコード1件は 数KB程度 の小さなサイズ
  • 低レイテンシでの取得耐久性 が必要
  • データは一時的で、一定期間後(数十日)に削除 したい
  • 全体のストレージ容量は 数TB〜十数TB規模

これらをすべて満たしつつ、最もコスト効率の良いストレージ戦略 を選べ、というのが問われていました。

ここでいう「レコード」とは?

問題文の「世界中のデバイスから」「小さなサイズ」「大量に取り込む」といったキーワードから、これは IoTデバイスやネットワーク機器から送られてくるテレメトリデータ(時系列の小さなイベントデータ) を指していると読み取れます。

具体的には次のようなイメージです。

  • スマートフォンや基地局からの通信ログ(通話開始/終了、データ使用量、シグナル強度など)
  • ネットワーク機器のステータスメトリクス
  • IoTセンサーの計測値
  • アプリケーションのイベントログ

AWS用語では、Kinesis Data Streams などのストリーミングサービスで取り込むデータ単位を「レコード」と呼びます。RDBの「行(row)」というより、1回のイベント/計測を表す独立した小さなデータ単位というニュアンスです。

つまり出題者は「レコード」「小さい」「数百万/分」「世界中」「TTL的な削除」というキーワードで、KVS(Key-Value Store)が得意とするワークロード であることを示唆しています。


2. 不正解の選択肢:RDS + cron で削除

選択肢の中に、こんな方針のものがありました。

  • データの保存先: Amazon RDS for MySQL の単一テーブル
  • 古いデータの削除: cron ジョブを毎晩実行し、保持期間を超えたレコードを DELETE する

これは不正解の選択肢で、解説では大きく2点の理由が挙げられていました。

  1. RDS は大量レコードの取り込みでパフォーマンスが著しく低下する ため、要件のスループットを捌けない可能性がある
  2. 削除用の cron ジョブを別途運用する必要があり、追加の管理オーバーヘッドが発生する

…納得感が薄いですよね。「なんとなくRDSはこの規模に向かない」というイメージはあっても、具体的に何が起きてどう困るのか が見えません。

ここから本題、深掘りに入っていきます。


3. 【深掘り1】RDSが大量書き込みで遅くなる5つの理由

まず、AWS公式の情報源から「RDSが大量書き込みでなぜ遅くなるのか」を整理します。理由は1つではなく、複数のレイヤーに分かれて存在します。

3-1. ストレージのIOPS上限とバースト枯渇

最も典型的なボトルネックが ディスクI/O です。

AWS公式ブログには次のような記述があります。

バーストキャパシティを超えてIOPS使用量が増加すると、ReadLatency、WriteLatency、DiskQueueDepthの増加という形でパフォーマンス低下が顕在化する。
(Making better decisions about Amazon RDS with Amazon CloudWatch metrics)

gp2ボリュームの場合、burstable IOPS の上限(例:3,000)にピーク負荷で到達し、バースト枠が枯渇するとIOPSはベースライン値でスロットルされ性能問題を引き起こす とされています。

大量取り込みのワークロードでは、このバースト枠は早期に枯渇し、以後はベースラインIOPSでのスロットリングが常態化します。

3-2. テーブル肥大化によるDML/DDLコストの増大

一定期間ぶんのデータを保持し続けると、定常状態で常に数TB規模のデータがテーブルに存在することになります。RDS のドキュメントは、これを明確に警告しています。

非常に大きいテーブル(100GB超)は、読み取り・書き込みの両方の性能に悪影響を及ぼす。
(Best practices for Amazon RDS)

つまり、テーブルが大きくなること自体が性能を落とす要因になります。

3-3. インデックスが増えるほど書き込みが重くなる

同じドキュメントには、こんな記述もあります。

大きなテーブルに対するインデックスはSELECT性能を大幅に改善できますが、同時にDML文(INSERT/UPDATE/DELETE)の性能を低下させます。

検索性能のためにインデックスを張ると、INSERT時にすべてのインデックスを更新する必要があり、書き込みが遅くなる。典型的なトレードオフ です。

詳細は後述の「1件のINSERTで何が起きているか」の章で詳しく扱います。

3-4. ロック競合(行ロック・インデックスページロック)

これも見落とされやすいポイントです。CPU・メモリ・IOPSのいずれも上限に達していないのに性能が出ないケースがあります。

システムリソースが上限に近づいておらず、スレッドを追加してもデータベーストランザクションレートが向上しない場合、ボトルネックはデータベース内の競合である可能性が最も高い。最も一般的な形態は 行ロックとインデックスページロックの競合 である。
(Amazon RDS DB instance storage)

時系列データの典型的な落とし穴がこれです。タイムスタンプや自動採番IDを主キーにすると、新規 INSERT は 常にB-treeの末尾ページに集中 するため、複数のセッションが同じインデックスページを奪い合います。

CPUもIOPSも余っているのに性能が出ない、という最悪のパターン を引き起こします。

3-5. 単一リージョンプライマリという構造的制約

問題文の「世界中のデバイスから」も重要な手がかりです。

RDS は基本的に 単一リージョンの単一プライマリ に書き込みを集約するアーキテクチャです。アジアのデバイスが米国のプライマリに書き込めば、毎回100ms超の往復遅延が乗ります。「低レイテンシで取得」要件にも反します。

DynamoDB の Global Tables なら各リージョンに書き込みエンドポイントを持てるので、ここもRDSが構造的に不利な点です。


4. 【深掘り2】そもそも「1件のINSERT」で何が起きているのか

ここからが本記事の本丸です。「RDSは大量INSERTに弱い」と言われても、1件のINSERTで内部的に何が起きているのか を知らないと腹落ちしません。

ナイーブな誤解

最初に持ちがちなイメージはこうです。

数KBのレコードを毎秒数万件INSERTするだけなら、データ量にして数百MB/秒。これくらい余裕でしょ?

これが大きな落とし穴です。RDBの書き込みコストはデータサイズではなく「I/O回数」で決まる からです。

1件のINSERTで実際に起きること

1件のINSERTで発生する物理I/Oはおおよそ次の通りです。

  1. WAL(または redo/binlog)への追記書き込み(耐久性のため fsync 必須)
  2. データページの更新(後でチェックポイント時に書き出し)
  3. インデックスごとのページ更新(B-treeのリーフを書き換える)
  4. 多くの場合 fsync が走る

…という説明だけだと初心者にはチンプンカンプンなので、1つずつ丁寧に解説します。

4-1. WAL(Write-Ahead Log)とは

WAL = Write-Ahead Log(先行書き込みログ) の略です。
PostgreSQLでは WAL、MySQL/InnoDBでは redo log、MySQLのレプリケーション用には binlog、という名前で呼ばれますが、どれも本質は同じです。

仕組み

データベースは、実際にテーブルファイルを更新する 前に、「これからこういう変更をしますよ」という変更履歴を 追記専用のログファイル に先に書きます。

INSERT が来る
   ↓
① まず WAL に「この行をこのテーブルに追加した」と追記
   ↓
② コミット成功をクライアントに返す
   ↓
③ 実際のテーブルファイルへの反映は、後でまとめて行う

なぜこんな回りくどいことを?

ポイントは「追記」と「ランダム書き込み」の速度差です。

  • WAL への書き込み = ファイル末尾に追記するだけ → ディスクヘッドがほぼ動かない → 超速い
  • テーブルファイルへの書き込み = ID順・主キー順など、テーブル内のあちこちのページを更新する → ディスクのいろんな場所に書く必要がある → 遅い

クライアントを待たせるのは①の速い処理だけ。テーブルへの本反映は後回しにします。
さらに、もしサーバーがクラッシュしても、WAL を読み直せば「コミット済みだがまだテーブルに反映されていなかった変更」を再実行できるので、データは失われません。

4-2. データページとバッファプール

「ページ」という単語が出てきました。これを説明します。

ページとは

データベースはデータをバイト単位やレコード単位ではなく、固定サイズの塊(ページ、通常8KBや16KB) で管理します。

  • PostgreSQL → 8KB
  • MySQL/InnoDB → 16KB

なぜページ単位かというと、ディスクI/Oもメモリ管理もページ単位で扱うほうが効率的だからです。

バッファプール(共有メモリ上のキャッシュ)

データベースは、よく使うページをメモリ上にキャッシュしています。これを バッファプール と呼びます。

[ディスク上のテーブルファイル]   ←→   [メモリ上のバッファプール]   ←→   [SQL処理]

INSERT 時の流れ

  1. 挿入先のテーブルページがメモリにあるか確認
  2. なければディスクから読み込んでメモリに載せる(読み込みI/O発生)
  3. メモリ上のページに新しい行を書き加える(メモリ操作のみ・ディスクI/Oなし)
  4. このページに「変更済み(dirty)」のマークをつける

この時点では まだディスクのテーブルファイルは更新されていない のがポイント。「変更済み」とマークされたページが、後でまとめてディスクに書き戻されます。これを チェックポイント と呼びます。

4-3. インデックスごとのページ更新

これがINSERTを重くする最大の犯人です。

インデックスは別の「ファイル」

テーブルとは別に、インデックスは独立したデータ構造として保存されています。普通は B-tree(またはB+tree) という木構造で、これもページ単位で管理されています。

例えば次のようなテーブルを考えます。

CREATE TABLE events (
    id BIGINT PRIMARY KEY,
    device_id VARCHAR(50),
    timestamp TIMESTAMP,
    value INT,
    INDEX idx_device (device_id),
    INDEX idx_time (timestamp)
);

このテーブルには 3つのインデックス があります。

  1. 主キー(id)のインデックス
  2. device_id のインデックス
  3. timestamp のインデックス

1件のINSERTで起きること

1件 INSERT すると、データベースは次のすべてを更新します。

  • テーブル本体のページ(行データを格納)
  • 主キーインデックスの該当ページ(B-treeのリーフに id を追加)
  • device_id インデックスの該当ページ
  • timestamp インデックスの該当ページ

つまり 4つの別々のページを更新する必要がある わけです。
それぞれについて、メモリにあるか確認 → なければディスクから読む(読み込みI/O) → メモリ上で更新 → dirtyマーク、という処理が走ります。

インデックスが3本あるテーブルへの1 INSERTは、インデックスのないテーブルへの4 INSERTと同じくらいの仕事をしている と思ってください。

巨大テーブル(数TB規模)の場合、インデックスもギガバイト〜テラバイト級になり、バッファプールに乗り切らない部分が必ず出ます。すると更新対象のリーフページがメモリにないことが頻発し、毎回ディスクから読み込むI/Oが発生 します。これが「大量レコードを持つRDBのINSERTが遅くなる」根本原因です。

4-4. fsync — 耐久性のための「重い処理」

ここが最も誤解されやすい部分です。

OSのファイル書き込みは「書いた」と言っても本当には書いていない

C言語などで write() システムコールを呼ぶと、データはディスクに書き込まれた…と思いきや、実際には OSのカーネルバッファに置かれただけ で、まだ物理ディスクには書かれていません。OSが「適当なタイミングで」あとからディスクに書きます。

これは性能のための仕組みです。アプリケーションは write() がすぐ返るので速いし、OSはまとめてディスクに書けるので効率的です。

でも電源が落ちたら?

カーネルバッファはメモリ(RAM)上にあるので、電源断やクラッシュで消えます。「DBが COMMIT を返したのにデータが消えた」では困ります。

fsync の役割

fsync() はOSに対して「いますぐカーネルバッファの中身を物理ディスクに書き出して、書き終わるまで戻ってくるな」と命令するシステムコールです。これが完了した瞬間、データは本当にディスク上にあると保証されます。

fsync は遅い

fsync は物理的な書き込み完了を待つので、他のディスク操作と桁違いに遅いです。

処理 速度感
メモリ書き込み ナノ秒オーダー
通常の write()(バッファに置くだけ) マイクロ秒オーダー
fsync(物理書込完了待ち) ミリ秒オーダー

SSDでも fsync は 100マイクロ秒〜数ミリ秒。EBSのようなネットワーク経由のストレージではさらに遅くなります。

なぜこれが大量INSERTの問題になるのか

毎秒50,000件のINSERTを、それぞれ別トランザクションで実行すると、毎秒50,000回の fsync が必要 になります。fsync 1回が1ミリ秒なら、1秒に1,000回しかできない計算で、まったく追いつきません。

4-5. まとめ:1 INSERT で本当に起きていること

整理するとこうなります(インデックス2本のテーブルへの1 INSERT、コミット時)。

ステップ 何が起きるか I/Oの種類
1 テーブルページをバッファプールへ(なければディスクから読込) 読込I/O可能性あり
2 テーブルページに行データを書く メモリ操作
3 インデックスAのリーフページを取得 読込I/O可能性あり
4 インデックスAのリーフにキー追加 メモリ操作
5 インデックスBのリーフページを取得 読込I/O可能性あり
6 インデックスBのリーフにキー追加 メモリ操作
7 WAL にすべての変更レコードを追記 書込I/O
8 WAL を fsync(コミット確定) 物理書込完了待ち(遅い)
9 クライアントに成功を返す
(後で) チェックポイント時に dirty ページを書き戻し 書込I/O

つまり「数KBのレコード1件を入れる」という見た目に対して、内部では 複数ページの読み書きと、最低1回の物理ディスク同期 が走っています。

これが「1 INSERT = 1 I/O ではない」の意味であり、毎秒数万件のINSERTがIOPS要件を爆発させる理由です。


5. 【深掘り3】cron ジョブでの期限切れ削除がなぜ「追加の管理オーバーヘッド」なのか

ストレージの話に戻ります。不正解の選択肢では、保持期間を超えたレコードを削除するために cron ジョブを毎晩走らせる ことになっていました。

5-1. cron ジョブとは

cron は、Unix/Linux系OSに標準で組み込まれている スケジュール実行の仕組み です。指定した日時や間隔で、自動的にコマンドやスクリプトを実行してくれます。

例えば次のように書くと、「毎日午前3時に削除スクリプトを実行する」という意味になります。

0 3 * * *   /usr/local/bin/delete_old_records.sh

選択肢Cはこんな構成です。

[EC2 または別のサーバー]
       │
       │ cron が毎晩 3:00 に起動
       ↓
[シェルスクリプト or Lambda]
       │
       │ MySQL に接続
       ↓
DELETE FROM events WHERE created_at < NOW() - INTERVAL N DAY;

5-2. cron を運用するために必要なもの

「cron で削除するだけ」と聞くとシンプルに思えますが、運用するには次のような追加要素が必要です。

  • cron を動かすインフラ: EC2インスタンスを別途立てる(パッチ・監視が必要)、または Lambda + EventBridge Scheduler を組む
  • 失敗監視: ジョブが失敗したら誰がどう気づくか? CloudWatch アラーム、SNS通知などの設定
  • 多重起動防止: 前回のジョブが終わる前に次が起動したらどうする?
  • 資格情報管理: DBに接続するためのパスワードを Secrets Manager などで管理
  • ログ管理: いつ・何件削除したかの記録

これだけで結構な運用負荷です。

5-3. DELETE 自体が本番ワークロードを圧迫する

これが一番厄介です。RDB の DELETE は INSERT より重い操作です。

毎晩 数千万〜数億行を削除するとなると…

  • DELETE 中に ロックが発生 し、INSERT が遅延する
  • WAL/binlog が大量生成 され、ストレージとレプリケーションラグを圧迫
  • MVCC のゴミが溜まり、PostgreSQL なら VACUUM、MySQL ならフラグメンテーション対応が必要
  • 削除してもディスク領域はすぐには解放されない

実際にAWS公式ブログには、Careem 社が270TBのRDS for MySQLから24TBのデータを削除するために大規模な専用プロジェクトを組んだ事例があります(Scaling Amazon RDS for MySQL performance for Careem's digital platform on AWS)。大量データのDELETEは、専門プロジェクトを組まないと運用できないレベル であることがわかります。

5-4. DynamoDB の TTL なら全部解決する

一方、DynamoDB には TTL(Time To Live)機能 があります。

  • 各アイテムに「いつ消えるべきか」のタイムスタンプを1つ属性として持たせるだけ
  • AWS が バックグラウンドで自動的に削除
  • 削除は 書き込みキャパシティを消費しない(本番ワークロードに影響しない)
  • cron も EC2 も Lambda も不要
  • 失敗監視も不要

cron ジョブで自前運用する場合に発生する 「インフラ・監視・本番への影響・障害対応」というオーバーヘッドがぜんぶ消える わけです。


6. まとめ:なぜこの問題でRDSが不正解なのか

整理するとこうなります。

コスト面の不利

  • 毎秒数万件のINSERTを捌くには、io2 Block Express クラスのストレージを常時張り付かせる必要がある
  • 数TB規模を恒常的に保持するRDSストレージ料金はDynamoDBより高単価
  • 書き込み負荷に耐えるため大型インスタンスが必要
  • Multi-AZ で2倍、リードレプリカを足せばさらに増える

性能面の不利

  • 1 INSERT が複数 IOPS を消費する(WAL + データ + 各インデックス + fsync)
  • 数TB規模の巨大テーブルでは INSERT/DELETE/DDL すべてが遅くなる
  • インデックスページ競合で並列性がスケールしない
  • 単一リージョンプライマリのため地理的レイテンシも乗る

運用面の不利

  • 期限切れデータの削除を実現するために cron ジョブの自前運用が必要
  • 大量DELETEが本番のINSERTを圧迫する
  • DynamoDB なら TTL で全自動

これらはすべて 「RDB を、本来 KVS が得意とするワークロードに使っている」 ことから生じる構造的な不一致です。

だから出題者は「小さなレコード」「大量取り込み」「グローバル」「期限切れ削除」といった、KVSへの誘導語を散りばめている わけです。RDS を選んだ瞬間に、これらすべての不一致を抱え込むことになります。


おわりに

「RDSは大量レコードでパフォーマンスが低下する」という一文の裏には、IOPS・ストレージ・インデックス・ロック・WAL・fsync という、これだけの内部メカニズムが詰まっていました。

SAP の問題は表面的な知識だけでも解けることが多いですが、こうして一段深く理解しておくと、初見の問題でも「これはRDBに不利なやつだな」と即座に見抜けるようになります。

同じところで詰まっていた方の参考になれば嬉しいです。指摘や補足があればコメントください!


参考リンク(AWS公式)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?