RDS スナップショット復元時の Lazy Loading に対するウォームアップはどれほど効果的なのか

記事タイトルとURLをコピーする

こんにちは。テクニカルサポート課の森本です。

先月、RDS がスナップショットからストレージを復元した際の初期化ステータスを可視化するアップデートがリリースされました。

Amazon RDS がストレージボリュームの初期化ステータスを可視化 - AWS

スナップショットから復元直後に Lazy Loading (遅延読み込み) が発生するというのは何となく知っていても、実際にどのくらい遅いのか、ウォームアップ (全テーブルスキャン) でどのくらい改善するのか、数字で見た経験がある方は少ないのではないでしょうか。今回は約 100GB のデータを用意して、ウォームアップの有無で挙動がどう変わるのかを実際に計測してみました。

3 行まとめ

  • 復元後に何もしないと、初期化はめちゃくちゃ遅い
  • ウォームアップ (全テーブルスキャン) をすると初期化は劇的に速くなる
  • 実は DB インスタンスを停止していても初期化処理は進む

Lazy Loading とは

RDS では、DB スナップショットから DB インスタンスを復元すると、ステータスが available になった時点ですぐに利用を開始できます。しかし、この時点ではデータはまだすべて S3 から EBS ボリュームに読み込まれておらず、バックグラウンドで順次読み込まれていきます。これを Lazy Loading (遅延読み込み) と呼びます。

公式ドキュメントにも以下のように記載があります。

You can use the restored DB instance as soon as its status is available. The DB instance continues to load data in the background. This is known as lazy loading. If you access data that hasn't been loaded yet, the DB instance immediately downloads the requested data from Amazon S3, and then continues loading the rest of the data in the background.

Restoring to a DB instance - Amazon RDS

まだ読み込まれていないブロックに初めてアクセスすると、その都度 S3 から取得するため I/O レイテンシが増加します。これを緩和する方法として、公式ドキュメントでは全テーブルスキャン (SELECT * など) を実行してあらかじめ全データを読み込ませておく (=ウォームアップする) ことが推奨されています。

To help mitigate the effects of lazy loading on tables to which you require quick access, you can perform operations that involve full-table scans, such as SELECT *. This allows Amazon RDS to download all of the backed-up table data from S3.

また、今回のアップデートにより、復元後の初期化の進捗は DescribeDBInstances の StorageOperationStatus / StorageOperationPercentProgress で確認できるようになりました。今回はこの値を定点観測しました。

検証環境

今回は以下の環境で検証しました。

  • エンジン: MySQL 8.4.9
  • インスタンスクラス: db.m8g.large (2 vCPU / 8GB)
  • ストレージ: gp3 200GB (3000 IOPS / 125 MBps)
  • リージョン: 東京 (ap-northeast-1)

検証データは sysbench を使って、10 テーブル × 5000 万行 = 5 億行、約 98GB を投入しました。

sysbench oltp_common \
  --db-driver=mysql \
  --mysql-host=<エンドポイント> \
  --mysql-user=<ユーザー名> --mysql-password=<パスワード> \
  --mysql-db=snaptest \
  --tables=10 --table-size=50000000 --threads=10 \
  prepare

このデータからスナップショットを取得し、そこから 2 台のインスタンスを復元して比較しました。

  • warmup-off ... 復元後、何もせず放置
  • warmup-on ... 復元後、全テーブルを並列スキャンしてウォームアップ

ウォームアップは、10 テーブルを並列にフルスキャンする形で実施しました。インデックスに含まれない列 (c, pad) も読ませることで、データ本体のブロックまで取得されることを期待しています。

ウォームアップに使ったスクリプト

#!/bin/bash
HOST_ON=<warmup-on のエンドポイント>
DBUSER=<ユーザー名>
DBPASS=<パスワード>

# 接続可能になるまで待機
until mysql -h "$HOST_ON" -u "$DBUSER" -p"$DBPASS" -e "SELECT 1" >/dev/null 2>&1; do sleep 15; done

echo "=== warmup start: $(date '+%F %T') ==="
time (
for i in $(seq 1 10); do
  ( mysql -h "$HOST_ON" -u "$DBUSER" -p"$DBPASS" -D snaptest \
      -e "SELECT COUNT(*),SUM(k),SUM(LENGTH(c)),SUM(LENGTH(pad)) FROM sbtest$i;" ) &
done
wait
)
echo "=== warmup end:   $(date '+%F %T') ==="

なお、比較を公平にするため両インスタンスはインスタンスクラス・ストレージ・AZ をすべて揃えています。

進捗の記録には、以下のようなシェルスクリプトを EC2 インスタンス上でバックグラウンド実行し、10 分ごとに StorageOperationStatus / StorageOperationPercentProgress を CSV に追記しました。

初期化の進捗計測に使った記録スクリプト

#!/bin/bash
REGION=ap-northeast-1
INSTANCES=("warmup-off" "warmup-on")
OUT=~/initialization_log.csv
[ -f "$OUT" ] || echo "timestamp,instance,status,storageOp,percent" > "$OUT"
while true; do
  TS=$(date '+%F %T'); done_count=0
  for db in "${INSTANCES[@]}"; do
    RAW=$(aws rds describe-db-instances --db-instance-identifier "$db" --region "$REGION" \
      --query "DBInstances[0].[DBInstanceStatus,StorageOperationStatus,StorageOperationPercentProgress]" \
      --output text)
    ST=$(echo "$RAW" | awk '{print $1}')
    OP=$(echo "$RAW" | awk '{print $2}')
    PCT=$(echo "$RAW" | awk '{print $3}')
    echo "$TS,$db,$ST,$OP,$PCT" | tee -a "$OUT"
    [ "$OP" = "None" ] && done_count=$((done_count+1))
  done
  echo "---"
  [ "$done_count" -eq 2 ] && break
  sleep 600
done

検証その1: ウォームアップあり vs なし

復元直後 (両方とも pct 0%) からウォームアップを開始し、10 分ごとに進捗 (StorageOperationPercentProgress) を記録しました。ウォームアップ側 (warmup-on) の全テーブルスキャンは約 110 分で完了しています。記録した全ポイントは以下の通りです。

経過時間 warmup-off (放置) warmup-on (ウォームアップ)
0分 (開始) 0% 0%
10分 0% 0%
20分 3% 13%
30分 3% 13%
40分 4% 25%
50分 5% 37%
60分 5% 37%
70分 6% 49%
80分 6% 59%
90分 6% 59%
100分 7% 71%
110分 (スキャン完了) 8% 83%
120分 8% 83%
130分 9% 85%
140分 9% 85%
150分 9% 86%
... ... ...
約302分 (この時点で両インスタンスを停止) 17% 93%
... (停止中) ... ...
440分 (warmup-on 完了) 23% 100% (完了)

ウォームアップした側は、全テーブルスキャン (約 110 分) の完了時点で 83% 付近まで進みました。一方、放置した側は同じ時間でわずか 8% です。ウォームアップによって初期化処理が劇的に加速することが数字で確認できました。

また、コスト都合で途中で DB インスタンスを停止しましたが、停止中も初期化処理が進むことを偶然発見できました。

なお、ウォームアップ実施中の進捗を示す StorageOperationPercentProgress の値は、10 分間の計測頻度としている点を抜きにしろ漸増ではなくある程度まとまって更新されているように見受けられました。

ボトルネックはどこか

ウォームアップ実行中に CloudWatch メトリクスも確認しました。

メトリクス 実測値 上限 使用率
CPUUtilization 4〜6% 100% 約5%
ReadIOPS 1000〜1700 3000 最大57%
ReadThroughput 15〜28 MB/s 125 MB/s 最大22%
ReadLatency 73〜116 ms - -

CPU・IOPS・スループットはいずれも上限に余裕がある一方、ReadLatency (73〜116ms)が比較的大きい状況 でした。この高いレイテンシはまさに「まだ読み込まれていないブロックを S3 から取得している待ち時間」= Lazy Loading そのものと考えられました。

2回目のウォームアップではボトルネックが移動する

一度初期化が完了した状態で同じスキャンをもう一度流すと、ボトルネックが別の場所に移りました。

メトリクス 初回 (Lazy Loading 中) 2回目 (初期化済み)
CPUUtilization 4〜6% 16〜24%
ReadIOPS 1000〜1700 約 3000 (gp3 上限に張り付き)
ReadThroughput 15〜28 MB/s 約 49 MB/s
ReadLatency 73〜116 ms 29〜38 ms

2回目は ReadLatency が 30ms 前後まで下がり、代わりに ReadIOPS が gp3 のプロビジョンド IOPS (3000) に張り付きました。つまり、

  • 初回 (Lazy Loading 中) ... 未ロードのブロックを S3 から取得するため ReadLatency が律速
  • 2回目 (初期化済み) ... ブロックは既に EBS 上にあるため、純粋な gp3 の IOPS 上限が律速

という切り分けができます。「復元直後は遅く、2回目以降は速い」という Lazy Loading の挙動が、レイテンシとボトルネックの移動という形で数字にも表れました。

なお、EBS の実効性能は「インスタンスの性能上限」と「アタッチされたボリュームの性能」の小さい方で決まります。今回は gp3 ボリューム側 (3000 IOPS) が小さい方だったため、IOPS 性能にて律速が発生しました。db.m8g.large 自体は最大 20,000 IOPS 程度の EBS 性能を持つので、さらに高速化したい場合は gp3 のプロビジョンド IOPS を引き上げるとさらにボトルネックは別の場所に移ると想定されます。

検証その2: なぜ フルスキャンを行っても100% とならないのか

ウォームアップした warmup-on のスキャン完了後の初期化スピードは warmup-off と同等となり、フルスキャンのみでは 100% にはなりませんでした。

これは、全テーブルスキャンで温められるのはテーブルデータのブロックだけだからです。実際にインスタンス内で InnoDB のサイズを確認すると、以下のようになっていました。

SELECT ROUND(SUM(data_length)/1024/1024/1024,2) data_gb,
       ROUND(SUM(index_length)/1024/1024/1024,2) idx_gb
FROM information_schema.tables WHERE table_schema='snaptest';
+---------+--------+
| data_gb | idx_gb |
+---------+--------+
|   98.35 |   0.00 |
+---------+--------+

論理的なデータは約 98GB です。一方、このインスタンスの復元元スナップショット全体のサイズ (FullSnapshotSizeInBytes) を確認したところ、約 136GB でした。つまりテーブルのデータ以外にも、割り当て済みだが未使用のページや、redo ログ・システムテーブルスペースなどの領域がボリューム上には存在すると推察されます。これらの領域はテーブルへの SELECT のみではアクセスできません。そのため、これらのブロックは単純な SELECT によるウォームアップでは温まらず、バックグラウンドの自然な初期化頼りになります。

つまり SELECT * によるウォームアップは、アプリケーションが実際にアクセスを行う重要なデータ領域を温める手段として有効である一方、ボリューム全体を 100% 初期化することはできないであろうという点は、実運用でも押さえておきたいポイントです。

小ネタ

検証中に気づいた細かい点をいくつか記載します。

FreeStorageSpace は初期化処理の進捗指標にならない

「ウォームアップでデータが読み込まれるなら FreeStorageSpace が減るのでは?」と思うかもしれませんが、初期化処理が進んでも FreeStorageSpace は一切変化しません。(これは今回のアップデート前からそうでしたね)

これは、復元時点でファイルシステムの構造 (どこに何 GB のファイルがあるか) は既に復元済みで、Lazy Loading は「既に存在するファイルのブロックの中身を裏で S3 から EBS に移す」処理であるからと推察されます。新規の書き込みではないので空き容量は動きません。初期化処理の進捗はあくまで StorageOperationPercentProgress で確認しましょう。

停止中も初期化処理は進む

これは AWS 公式ドキュメントには明示的な記載がなく、あくまでも検証で判明したことですが、停止した DB インスタンスでも引き続き初期化処理が進んでいました。これは初期化処理が DB エンジン側の処理ではなく、ストレージ (EBS) 層の処理のため、インスタンスを停止していてもバックグラウンドで進行するためと推察されます。そのため、DB インスタンスをコスト都合などで一時停止していても、次に起動したときには進捗が進んでいることになります。

まとめ

RDS スナップショット復元時の Lazy Loading について、ウォームアップの有無で挙動がどう変わるかを検証しました。 どのようにすれば初期化処理が早く完了するかを事前に確認し、手順として準備しておくことで、緊急時等にリストアを行った際により迅速な復旧が可能となると考えられました。

この記事がどなたかのお役に立てば幸いです。

森本 晃大(執筆記事の一覧)

テクニカルサポート課

2023年10月入社。絶賛仕事と子育てに奔走中です。