RDS運用の実践ガイド:スナップショット・パラメータ設定・スロークエリ対策
この記事でわかること
- 自動バックアップと手動スナップショットの違い、そして PITR(Point-in-Time Recovery) をどう設計に組み込むか
- パラメータグループでチューニングすべき本当に重要な少数のパラメータ(
innodb_buffer_pool_size等)と、その反映タイミング(pending-reboot / immediate)の落とし穴 - Performance Insights と
aws pi get-resource-metricsでトップSQLを CLI から取り出す方法 - gp3 ストレージへの移行で IOPS を切り離してコストを下げる手順(既存 gp2 からのオンライン変更)
- Blue/Green Deployments を使ったメジャーバージョンアップグレード時の事故防止
- Multi-AZ フェイルオーバー時の挙動と、アプリ側のリトライ実装の現実解
この記事を書いた経緯
ある日、本番の MySQL 8.0 RDS が「夜間バッチ後に異常に重くなる」という相談を受けました。原因を切り分けようと Performance Insights のダッシュボードを開いた瞬間、**「Performance Insights は有効化されていません」**の表示。「CPU 80%」という CloudWatch メトリクスはあるのに、何のクエリが CPU を使っているかは誰も知らない状態でした。マネージドだから設定不要、というよくある誤解です。
その案件で実際に打ち手は、派手な構成変更ではありませんでした。
-
slow_query_log = 1,long_query_time = 1を有効化し、CloudWatch Logs にエクスポート - Performance Insights を有効化(無料枠で7日保持で十分)
- gp2 → gp3 に切り替え(IOPSを別途指定できるので I/O が支配的なワークロードでスループットが安定)
-
innodb_buffer_pool_sizeを{DBInstanceClassMemory*3/4}に修正(**「数字で書かれていた」**ので、インスタンスタイプ変更後に追従していなかった)
「Performance Insights 有効化」と「gp3 化」と「数式に直す」だけで体感が変わったケースで、運用の差はだいたい "可視化されているか" に尽きるな、と再確認しました。
実務での背景
RDSは「マネージドだから設定不要」と思われがちですが、デフォルト設定のまま使うと性能問題やバックアップのトラブルが発生します。よくあるのは次の3パターンです。
- スロークエリが発生しているのに気づかなかった(slow_query_log がデフォルトOFFなので)
- スナップショットから復元しようとしたら、保持期間1日でその時点のものが残っていなかった(デフォルト保持期間が短い)
- メンテナンスウィンドウが業務時間に設定されていて、マイナーバージョン更新で接続断
どれも「コード」ではなく「設定」の問題で、知っていれば防げます。
解決方法:RDS運用の3つの柱
| 柱 | 目的 | 主な機能 |
|---|---|---|
| バックアップ戦略 | データ消失を防ぐ | 自動バックアップ + PITR + 手動スナップショット + クロスリージョン |
| パラメータチューニング | 性能を引き出す | カスタムパラメータグループ + slow_query_log + innodb_buffer_pool_size |
| 監視 | 異常を早く検知し原因を特定する | Performance Insights + Enhanced Monitoring + CloudWatch Logs/Alarms |
ここから順に具体的な手順を見ていきます。
具体的な手順
Step 1: バックアップ設定の最適化(PITRを意識する)
「バックアップ取ってる?」と聞かれて「自動バックアップが効いてます」だけだと不十分です。保持期間が1日のままだと、土日に発生した障害を月曜に気づいた時点で復旧できません。
# バックアップ保持期間を7日に設定(デフォルト1日)
# 時刻指定はすべて UTC。JST に直すと +9h で日付が繰り上がる点に注意
# 17:00-18:00 UTC = 翌日 02:00-03:00 JST
# Mon:15:00-Mon:16:00 UTC = 火曜 00:00-01:00 JST
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--backup-retention-period 7 \
--preferred-backup-window "17:00-18:00" \
--preferred-maintenance-window "Mon:15:00-Mon:16:00" \
--apply-immediately
# 手動スナップショット(リリース前に必ず取る)
aws rds create-db-snapshot \
--db-instance-identifier prod-mysql \
--db-snapshot-identifier "before-release-$(date +%Y%m%d-%H%M)"
設定値の判断基準は次の通りです。
| 項目 | 推奨値 | 理由 |
|---|---|---|
backup-retention-period |
本番は 7〜35日(最大35日) | RPO(復旧目標時点)の長さで決める。週次レビューで戻したいなら最低7日 |
preferred-backup-window |
業務時間外(例 17:00-18:00 UTC = 02:00-03:00 JST) | I/O が瞬間的に増えるので業務時間と被らせない |
preferred-maintenance-window |
業務時間外 + バックアップウィンドウと重ねない | OS/DBエンジンの自動パッチが走るタイミング |
自動バックアップ vs 手動スナップショットの違い
両者は似ているように見えて、消える条件が違います。
| 項目 | 自動バックアップ(PITR含む) | 手動スナップショット |
|---|---|---|
| 作成方法 | RDSが毎日自動 |
create-db-snapshot で都度 |
| 保持期間 |
backup-retention-period(最大35日) |
明示的に削除するまで残る |
| DBインスタンス削除時 | 一緒に消える(注意) | 残る |
| PITR(任意時点復元) | 可能(5分粒度) | スナップショット時点のみ |
| クロスリージョン複製 | Automated Backups Replication で PITR ごと複製可能 | スナップショットを都度コピー(copy-db-snapshot) |
ここで重要なのは「DBインスタンスを削除すると自動バックアップも消える」点です。インスタンス削除時に --final-db-snapshot-identifier で最終スナップショットを残すか、DeletionProtection を有効にしておくのが安全です。
# 削除保護の有効化(本番は必須)
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--deletion-protection \
--apply-immediately
DR用にクロスリージョン自動コピーを設定する
東京リージョン障害に備えて、スナップショットを大阪等にコピーします。RDS には Automated Backups Replication という機能があり、自動バックアップごとクロスリージョン複製できます。
# 自動バックアップを大阪リージョン (ap-northeast-3) に複製
aws rds start-db-instance-automated-backups-replication \
--source-db-instance-arn arn:aws:rds:ap-northeast-1:123456789012:db:prod-mysql \
--backup-retention-period 7 \
--region ap-northeast-3
「DR は手動スナップショットを cron でコピーしている」というケースが多いですが、Automated Backups Replication なら PITR も大阪側でできるので、近年はこちらが標準です。
Step 2: パラメータグループの設定
デフォルトのパラメータグループは変更不可です。本番では必ずカスタムパラメータグループを作って割り当てます。
# カスタムパラメータグループ作成
aws rds create-db-parameter-group \
--db-parameter-group-name prod-mysql-params \
--db-parameter-group-family mysql8.0 \
--description "Production MySQL parameter group"
# 主要パラメータの設定
aws rds modify-db-parameter-group \
--db-parameter-group-name prod-mysql-params \
--parameters \
"ParameterName=slow_query_log,ParameterValue=1,ApplyMethod=immediate" \
"ParameterName=long_query_time,ParameterValue=1,ApplyMethod=immediate" \
"ParameterName=log_output,ParameterValue=FILE,ApplyMethod=immediate" \
"ParameterName=innodb_buffer_pool_size,ParameterValue={DBInstanceClassMemory*3/4},ApplyMethod=pending-reboot" \
"ParameterName=max_connections,ParameterValue=350,ApplyMethod=pending-reboot"
ApplyMethod の違いに注意
immediate か pending-reboot かで挙動が大きく違います。
| ApplyMethod | 反映タイミング | 主な用途 |
|---|---|---|
immediate |
即時(再起動不要) |
slow_query_log, long_query_time, log_output などの動的パラメータ |
pending-reboot |
次回再起動時 |
innodb_buffer_pool_size, max_connections などの静的パラメータ |
innodb_buffer_pool_size のような重要なパラメータは静的なので、変更しただけでは効果がありません。再起動するまで「変更したつもり」のまま放置されているケースが本当に多いです。再起動が必要なパラメータは下記で確認できます。
# パラメータグループの「Pending」状態を確認
aws rds describe-db-instances \
--db-instance-identifier prod-mysql \
--query 'DBInstances[*].DBParameterGroups'
# 再起動(Multi-AZ 構成なら --force-failover でスタンバイ側に切り替えてダウンタイム短縮)
aws rds reboot-db-instance \
--db-instance-identifier prod-mysql \
--force-failover # シングルAZでは指定不可
innodb_buffer_pool_size は必ず数式で書く
メモリの 75% 程度を割り当てるのが定石ですが、固定値(例: 12884901888)で書くとインスタンスタイプを変更した後に追従しないという事故が起きます。AWS のパラメータ式を使うと、インスタンスサイズに合わせて自動で計算されます。
innodb_buffer_pool_size = {DBInstanceClassMemory*3/4}
DBInstanceClassMemory は「インスタンスクラスのメモリ量(バイト)」が自動で代入されます。インスタンスタイプ変更時も再計算されるので、固定値より数式が常に正解です。
max_connections をデフォルトのままにしない
デフォルトの計算式は {DBInstanceClassMemory/12582880} で、メモリに対しては余裕があるのですが、アプリケーション側のコネクションプール × ワーカー数が増えてくると枯渇します。本番では以下のように見積もります。
必要数 = (APIサーバー台数 × プールサイズ) + バッチ用 + 監視用 + 余裕分(20%)
例: APIサーバー10台 × プール20本 + バッチ30 + 監視10 + 余裕 = 約 290。なので max_connections = 350 のように余裕を持って設定します(上の例の 350 はこの想定値)。
Step 3: スロークエリをCloudWatch Logsで確認
slow_query_log = 1 を有効化しただけではログがどこに出るか分かりません。CloudWatch Logs にエクスポートする設定が必要です。
# スロークエリログをCloudWatch Logsに送信する設定
# ※ general ログは量が爆発するので本番常用は非推奨。必要時のみ追加
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--cloudwatch-logs-export-configuration EnableLogTypes=slowquery,error,audit \
--apply-immediately
# CloudWatch Logsでスロークエリを確認(Linux/GNU date)
# macOS では date -v-1H を使う
aws logs filter-log-events \
--log-group-name "/aws/rds/instance/prod-mysql/slowquery" \
--start-time $(date -d "1 hour ago" +%s000) \
--filter-pattern "Query_time" \
--query 'events[*].message' \
--output text | head -50
CloudWatch Logs Insights でスロークエリの発生回数とピークを集計
ログをただ眺めるのではなく、Logs Insights で集計するのが現代の標準です。RDS のスロークエリログは1イベントが複数行(# Query_time: ヘッダーと SQL本文)になっているため、@message 全体に対して parse で時間を取り出します。
fields @timestamp, @message
| parse @message /Query_time:\s+(?<query_time>[\d.]+)/
| filter ispresent(query_time)
| stats count() as cnt, max(query_time) as max_time by bin(5m)
| sort @timestamp desc
「重いクエリ別」に集計したい場合は、Performance Insights(後述)の方がクエリの正規化(リテラル除去)まで自動でやってくれるので楽です。Logs Insights は時系列の発生回数やピークを見たい時に使うのが向いています。
CLIから一発で叩くなら次の通り。
aws logs start-query \
--log-group-name "/aws/rds/instance/prod-mysql/slowquery" \
--start-time $(date -d "1 hour ago" +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, @message | filter @message like /Query_time/ | sort @timestamp desc | limit 20'
log_output は TABLE か FILE か
log_output = TABLE だと mysql.slow_log テーブルに書き込まれます。FILE を選ぶと OSファイルに書かれて、それが CloudWatch Logs にエクスポートされます。CloudWatch Logs で集計するなら FILE 一択です(TABLE は CloudWatch Logs にエクスポートされず、mysql.slow_log の定期ローテーションを自分で運用する必要があります)。Step 2 のパラメータ例も FILE にしてあるのはこの理由です。
Step 4: Performance Insightsで問題クエリを特定
Performance Insights は 「DB負荷の正体を可視化する」 ためのほぼ必須機能です。7日保持なら無料なので、本番では必ず有効化します。
# Performance Insights を有効化(7日保持は無料)
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--enable-performance-insights \
--performance-insights-retention-period 7 \
--apply-immediately
# 上位SQLをCLIで取得(db.load.avg を db.sql 単位でグルーピング)
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-XXXXXXXXXX \
--start-time $(date -u -d "1 hour ago" +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period-in-seconds 60 \
--metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.sql","Limit":5}}]'
identifier には db-XXXXXXXXXX(DBI Resource ID)を指定する必要があります。db-instance-identifier ではないので注意。次のように取得します。
aws rds describe-db-instances \
--db-instance-identifier prod-mysql \
--query 'DBInstances[0].DbiResourceId' \
--output text
「待機イベント別」で見ると本当のボトルネックがわかる
GroupBy を db.wait_event に切り替えると、CPU待ち・I/O待ち・ロック待ちのどれが支配的かが見えます。
aws pi get-resource-metrics \
--service-type RDS \
--identifier db-XXXXXXXXXX \
--start-time $(date -u -d "1 hour ago" +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period-in-seconds 60 \
--metric-queries '[{"Metric":"db.load.avg","GroupBy":{"Group":"db.wait_event","Limit":7}}]'
| 待機イベント | 意味 | 主な対処 |
|---|---|---|
CPU |
CPU待ち | クエリ改善 / インスタンスタイプ上げ |
io/aurora_redo_log_flush (Aurora) |
redoログのフラッシュ待ち | 書込みTPSを下げる / インスタンス上げ |
wait/io/table/sql/handler |
テーブルI/O待ち | バッファプール拡大 / gp3でIOPS増 |
synch/cond/sql/MYSQL_BIN_LOG::COND_done |
binlog 待ち | レプリ遅延 / writer集中の見直し |
wait/synch/mutex/innodb/... |
InnoDB内部ロック | ホットスポット行の見直し |
「とりあえずCPU上げる」をやる前に、何で待っているのかを見るのが事故防止の鍵です。
Step 5: ストレージは gp3 に移行する(既存環境はオンライン変更可能)
gp2 を使っている既存環境はほぼ全て gp3 に切り替えるメリットがあります。gp2 は容量と IOPS が連動するため、IOPS を増やすには容量も増やす必要がありますが、gp3 は IOPS とスループットを容量と独立に指定できます。
# gp2 → gp3 にオンライン変更(無停止)
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--storage-type gp3 \
--iops 3000 \
--storage-throughput 125 \
--apply-immediately
| 項目 | gp2 | gp3 |
|---|---|---|
| IOPS | 容量×3(最低100)。バーストあり | ベースで 3,000 IOPS(独立指定可能) |
| スループット | 容量に応じて自動 | ベースで 125 MB/s(独立指定可能) |
| コスト | やや高い | gp2 比で 約 20% 安い |
| 変更 | オンライン可能 | オンライン可能 |
切り替えは数時間かかりますが、ダウンタイムなしで行えます。「容量を増やさないと IOPS が足りない」という gp2 の苦しさから解放されるので、未移行なら最優先で対応する価値があります。
Step 6: メジャーバージョンアップは Blue/Green Deployments で
MySQL 5.7 → 8.0 のようなメジャーアップグレードは、従来は「メンテナンス時間を取って modify-db-instance --engine-version 8.0.x --apply-immediately を叩く」という綱渡りでした。
2022年末に登場した Blue/Green Deployments を使うと、本番(Blue)と並行して新バージョン(Green)を立ち上げ、レプリケーションで同期させ、最終切り替えはわずか1分以下で完了します。
# Blue/Green Deployment 作成
aws rds create-blue-green-deployment \
--blue-green-deployment-name prod-mysql-upgrade-80 \
--source arn:aws:rds:ap-northeast-1:123456789012:db:prod-mysql \
--target-engine-version 8.0.35 \
--target-db-parameter-group-name prod-mysql80-params
# 状態確認( STATUS が AVAILABLE になったら切替可能)
aws rds describe-blue-green-deployments \
--query 'BlueGreenDeployments[*].[BlueGreenDeploymentName,Status,SwitchoverDetails]'
# 切替(実行すると Blue と Green のエンドポイント名が入れ替わる)
aws rds switchover-blue-green-deployment \
--blue-green-deployment-identifier bgd-xxxxxxxxxxxx \
--switchover-timeout 300
切替後、Blue(旧バージョン)はすぐには消されないので、問題があれば即座にロールバック可能です。ロールバック判断の余裕を持つために、Blue は数日残しておくのが安全です。
構成図
ハマりポイント
❌ メンテナンスウィンドウが業務時間に設定されている
デフォルトのメンテナンスウィンドウは、作成時のランダム値です。気付かないうちに業務時間に設定されていて、マイナーバージョンアップで接続断、というのは本当によくある事故。
# 全RDSのメンテナンスウィンドウを一覧
aws rds describe-db-instances \
--query 'DBInstances[*].[DBInstanceIdentifier,PreferredMaintenanceWindow]' \
--output table
# 業務時間外に変更(例: Mon 18:00-19:00 UTC = 火曜 03:00-04:00 JST)
# AWS の指定は UTC 固定。JST に直すと日付が1日繰り上がる点に注意
aws rds modify-db-instance \
--db-instance-identifier prod-mysql \
--preferred-maintenance-window "Mon:18:00-Mon:19:00" \
--apply-immediately
❌ Multi-AZ フェイルオーバー時の接続断を想定していない
Multi-AZ 構成でも、フェイルオーバー時には 30〜120秒程度の接続断 が発生します。これは「アプリ側でリトライ実装」が必須で、コネクションプール側に接続検証(pre-ping)と短いタイムアウトを入れておきます。
# SQLAlchemy の例
engine = create_engine(
DB_URL,
pool_pre_ping=True, # コネクション利用前にPing
pool_recycle=300, # 5分でリサイクル
connect_args={
"connect_timeout": 5, # TCP接続タイムアウトを短く
},
)
アプリ層でリトライしないと、フェイルオーバー時の数十秒間は全リクエストが500エラーになります。Multi-AZ は「無停止」ではなく「短時間停止で済む」構成だと理解しておくのが重要です。
❌ innodb_buffer_pool_size を固定値で書いている
前述の通り、固定バイト値で書くとインスタンスタイプを上げても追従しません。
# NG(固定値)
innodb_buffer_pool_size = 12884901888
# OK(数式)
innodb_buffer_pool_size = {DBInstanceClassMemory*3/4}
❌ コネクションが詰まる → RDS Proxy を検討
Lambda などサーバーレスからの接続では、接続/切断のコストとコネクション数の爆発が問題になります。max_connections を引き上げてもメモリ不足で詰まります。
そういう場合は RDS Proxy を間に入れます。Proxy 側でコネクションプールを管理し、DB側のコネクション数を一定に保ちます。
aws rds create-db-proxy \
--db-proxy-name prod-mysql-proxy \
--engine-family MYSQL \
--auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:...","IAMAuth":"REQUIRED"}]' \
--role-arn arn:aws:iam::123456789012:role/rds-proxy-role \
--vpc-subnet-ids subnet-aaa subnet-bbb \
--require-tls
Proxy の追加料金(vCPU 単位の時間課金)はかかりますが、Lambda やコンテナワークロードで Too many connections に悩んでいるなら導入価値があります。
❌ スナップショットからの復元で「同じ設定」にならない
スナップショットからの復元は 「新規DBインスタンスとして起動」 されます。元のインスタンスの設定(パラメータグループ・セキュリティグループ・サブネットグループ)は引き継がれません。復元後に必ず確認してください。
# スナップショットからの復元(パラメータグループ等を明示)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier prod-mysql-restored \
--db-snapshot-identifier before-release-20260504-1200 \
--db-parameter-group-name prod-mysql-params \
--db-subnet-group-name prod-db-subnet \
--vpc-security-group-ids sg-xxxxx \
--multi-az
まとめ
| 設定項目 | 推奨値・設定 |
|---|---|
| バックアップ保持期間 | 7日以上(DR 要件次第で 35日まで) |
| クロスリージョン自動バックアップ複製 | DR 必要なら有効化(PITRごと複製可能) |
| メンテナンスウィンドウ | 業務時間外。バックアップウィンドウと重ねない |
削除保護 (DeletionProtection) |
本番は必ず有効 |
| スロークエリログ |
slow_query_log=1, long_query_time=1, log_output=FILE
|
| Performance Insights | 7日保持で無料。本番は必ず有効化 |
| ストレージ | gp3 (既存gp2はオンライン移行) |
innodb_buffer_pool_size |
{DBInstanceClassMemory*3/4} (必ず数式) |
| メジャーバージョンアップ | Blue/Green Deployments で短時間切替 |
| CloudWatchアラーム | Storage / Connections / Latency / CPU / ReplicaLag |
RDSはマネージドサービスですが、パラメータとモニタリングの設定は自分で行う必要があります。 特に Performance Insights と slowquery ログのエクスポートは「問題が起きてから入れる」のでは遅いので、新規構築の段階でテンプレート化しておくのがおすすめです。Terraform / CloudFormation で全環境に標準適用するのが現実解です。
