大規模サービスのAmazon Aurora MySQLのテーブル変更で直面した3つの課題

Advent Calendar 2024, AWS, DB, MySQL, RDS

この記事は レコチョク Advent Calendar 2024 の4日目の記事となります。 

はじめに

私は主に音楽サブスクリプションサービスのバックエンド開発を担当しています。
今回は、大規模サービスならではのAmazon Aurora MySQLのテーブル変更で直面した3つの課題についてご紹介します。

背景・環境

対象のサービスは、会員数が100万人規模の音楽サブスクリプションサービスです。
2024年10月中旬に大型アップデートに伴うAPIをリリース予定でした。
APIをリリースするにあたり、事前に新規テーブルの作成や既存テーブルのカラム修正を行う必要がありました。
これまでもAPIのリリースやテーブル変更は幾度となく行っていましたが、今回のリリース規模は前例がありませんでした。
このリリースを行うにあたり、いくつかの課題があったためご紹介します。
同じ問題を抱えている方の参考になれば嬉しいです。

稼働中の環境

  • EC2
  • RDS
    • エンジンバージョン:5.7.mysql_aurora.2.11.3
  • 数千万から数億レコードのテーブルが複数存在しています。

当初の実行方針

当初は、以下のスケジュールを想定していました。

1. サービス全体をメンテナンス状態にする
2. ユーザからのアクセスが来なくなったことを確認
3. 稼働中のDBに対して直接CREATE TABLEやALTER TABLEを実行

  • CREATE TABLE:約150テーブル実施
  • ALTER TABLE:約200テーブル実施

4. APIのリリース
5. 動作確認
6. メンテナンスを解除

課題1: メンテナンス時間が長すぎる

事前にメンテナンス時間をユーザに告知する必要があったため作業の見積もりを行いました。
本番同等のスペックやデータを用意し、テーブル作成やテーブルのカラム変更を行いました。
しかし、予想以上にカラム変更に時間がかかり、最終的に約8時間もの間メンテナンスを行う必要があることが判明しました。
ユーザ影響が大きすぎると判断し、スケジュールを見直すことに決めました。

課題1の解決策

作業時間を短縮できる部分を検討しましたが、頭打ちの状態でした。
メンテナンス方法を抜本的に変える必要があると判断し、サービス内での作業実績がない
Blue/Green Deploymentsを検討することにしました。

検討した結果、メンテナンス時間は約3時間で済むと判断しました。
そして、新たに決定したスケジュールが以下です。

1.リリース当日までに

  • Blue/Green環境構築
  • Green環境のDBに対してCREATE TABLEやALTER TABLEを実行

2. リリース当日

  • サービス全体をメンテナンス状態にする
  • トラフィックを切り替え
  • APIのリリース
  • 動作確認
  • メンテナンス解除

Blue/Green Deploymentsの概要

詳しい内容はたくさんの記事があるのでここでは簡単にご紹介するまでに留めます。

Blue/Green Deploymentsは、稼働中のDBに対して直接CREATEやALTERを行うのではなく
あらかじめ新しい環境を用意しCREATEやALTERを行った後、トラフィックを新しい環境に切り替えます。
これによりダウンタイムを最小限に抑え、問題が発生した場合はすぐに古い環境に戻すことができます。

一般的な流れは以下の通りです。
1. Blue環境を作成
2. Green環境を作成
3. Green環境上でテスト
4. テストが問題なければトラフィックをGreen環境に切り替え
5. Blue環境を削除

課題2: Blue/Green環境作成後、環境間のデータ同期が取れていない

Blue/Green環境を無事構築し、Green環境のDBに対して新規テーブル作成とカラム変更を行っていました。
まず1テーブルのみint型をbigint型に修正するDDLを実行し、どのくらい時間がかかるか計測しました。

しかし、上記DDLを実行完了後30分経過してもBlue環境のデータがGreen環境に同期されていない問題が発生しました。

課題2の原因と解決策

Green環境で以下のコマンドを実行し、スレーブの状態を確認しました。
(MySQL 8.0.22 以降では、SHOW SLAVE STATUS は非推奨になり、かわりに SHOW REPLICA STATUS の使用が推奨されています)

MySQL 5.7:https://dev.mysql.com/doc/refman/5.7/en/replication-administration-status.html
MySQL 8.0:https://dev.mysql.com/doc/refman/8.0/ja/replication-administration-status.html

(出力結果は一例)

多数の項目がある中で特に次の項目に注目しました。

  • Slave_IO_Running / Slave_SQL_Running
    • Slave_IO_Running: Yes は、IOスレッドが正常に動作していることを示す
    • Slave_SQL_Running: Yes は、SQLスレッドが正常に動作していることを示す
    • 通常、レプリケーションを開始していない場合や、明示的にSTOP SLAVEで停止した場合を除き、この項目は「Yes」であるべき
  • Seconds_Behind_Master
    • この値が「0」であれば、レプリカで処理中のイベントがない(=同期している)状態
  • Last_Error / Last_IO_Error / Last_SQL_Error
    エラーが発生している場合、ここにエラー内容が表示される

上記の一例の場合、
Slave_IO_Running と Slave_SQL_Running が Yes であるが
Seconds_Behind_Masterの値が775であることから、同期がまだ終わっていないことがわかります。

最終的にGreen環境にもBlue環境のデータが反映され、
Seconds_Behind_Masterの値が0になったのはALTER TABLEの実行が完了した約1時間後でした。

なぜ同期に1時間もかかったのか、

で実行中のプロセスを確認したところ、ALTER TABLEの実行中にバッチが実行されて大量のINSERT処理が動いていました。
バッチが実行されていたことが原因で同期が遅れてしまった可能性が高いです。

以降の作業はバッチが実行されていないことを確認したうえで
他のテーブルに対してもint型をbigint型に修正するDDLを実行しました。
すると同期は遅くとも5分程度で完了しました。
また、Blue環境にデータが登録/修正された時、Green環境にも同期されていることを確認できました。

課題3: Blue/Green環境間のデータの同期が急に取れなくなった

課題2が発生してから数日後のことです。
「同期されていないのではないか?」という報告があがりました。
そこで確認のために show slave statusコマンドを実行すると
“違う型に変換できない”という旨のエラーが出力され、同期が取れていないことが判明しました。

以下にエラーメッセージの一部を示します。

データベースへの接続自体には問題がなかったこともあり、このコマンドを実行するまで同期が止まっていることに気づきませんでした。このまま気づかずに進めていた場合、一部のデータが欠損した状態でデータベースの切り替えを行う可能性がありました。

課題3の原因と解決策

調査を進めたところ、Green側に設定されているパラメータグループのslave_type_conversionsパラメータの値がエラーの原因となっている可能性があることが分かりました。

以下は、MySQLドキュメントから抜粋した説明を機械翻訳したものです。
https://dev.mysql.com/doc/refman/5.7/en/replication-features-differing-tables.html

エラー発生時のslave_type_conversionsの設定値はALL_LOSSYのみでした。
そのため、INT から BIGINT のような損失のない変換の許可がされずエラーが発生してしまったと考えられます。
対策として、ALL_NON_LOSSY を指定する必要があると考えました。

しかしここで一つ疑問が残りました。

なぜ、課題2の時にはエラーが発生せず、同期が完了したのか?

課題2の時点でslave_type_conversionsにALL_LOSSYのみが設定されていたため、
Blue環境のデータが更新されると何らかのエラーが発生していてもおかしくないはずです。
ですがこの日レプリケーションされていることは確認済みでした。

この疑問を解決するため再現検証を行いました。

再現検証

slave_type_conversionsにALL_LOSSYのみが設定されている状態で、Blue環境に対し以下のDDLを実行しました。
カラム名は仮です。

  1. B/G で型の異なるカラムが含まれているテーブルを対象に全件更新
  2. B/G で型の異なるカラムを対象に1件更新
  3. INNER JOINを使用した、B/G で型の異なるカラムが含まれているテーブルを対象に1件更新
  4. SYSDATE()を使用したUPDATE文 (毎日深夜に実行されるバッチ)

その結果、
1、2はGreen環境にも問題なく同期されましたが、
3、4は課題3と同内容のエラーが発生し、同期がストップしました。

この結果をもとにさらに調査を進めました。

今回パラメーターグループに設定していたbinlog_formatの値はMIXEDでした。

MIXED形式では、デフォルトでSTATEMENT形式で同期が行われますが、特定条件下ではSTATEMENT形式だと安全でないと判断され、自動的にROW形式に切り変わることがあります。

https://dev.mysql.com/doc/refman/5.7/en/binary-log-mixed.html から抜粋

https://dev.mysql.com/doc/refman/5.7/en/replication-rbr-safe-unsafe.html から抜粋

どのようなケースでSTATEMENT形式が安全でないと判断されるかは以下のドキュメントに記載されています。
https://dev.mysql.com/doc/refman/5.7/en/replication-sbr-rbr.html

今回は次の条件に該当し、自動的にROW形式に切り変わったと考えられます。

また、ROW形式でのレプリケーションは
損失のある変換と損失のない変換をどのように扱うか設定しないといけない旨が記載されていました。
https://dev.mysql.com/doc/refman/5.7/en/replication-features-differing-tables.html

以上を踏まえ、事象発生までの過程と原因は次のように整理できました。

  • エラー発生時、binlog_formatの値は MIXED、slave_type_conversions = ALL_LOSSY を設定していた
  • MIXEDの場合、デフォルトはSTATEMENT形式で同期が行われるが、特定の条件下ではROW形式に切り変わる
    • ROW形式に切り替わる特定の条件一例
      • 特定の関数を使用するステートメント (LOAD_FILE(),UUID(), UUID_SHORT(), USER(), FOUND_ROWS(), SYSDATE() etc..)
      • 複雑なステートメント
  • 毎日深夜に実行されているバッチでSYSDATE()を使用していたため、自動的にROW形式に切り変わった
  • ROW形式での同期は損失のある変換と損失のない変換をどのように扱うか決定しないといけないが、正しい設定が行われていなかった
  • 損失のない型違いの処理ができず、“違う型に変換できない“旨のエラーが出てしまった

slave_type_conversions の値を ALL_LOSSY,ALL_NON_LOSSY に設定することで
ROW形式での同期時に損失のない型違いの処理を許可することができ、同期のエラーが表示されることはなくなりました。

更に同期不具合の早期発見のために
数時間おきにSHOW SLAVE STATUS\Gを実行し、その結果をメールで送信するシェルを作成しました。

その後、特にトラブルが発生することなくリリースすることが出来ました。

まとめ

今回得られたこと・伝えたいことは以下です。

リリース計画時

  • メンテナンス時間を短縮するためにBlue/Green Deploymentsを検討する
  • 時間を見積もる際は同期時間も考慮することが重要

Green環境にDDLを実行する前後

  • Green環境でDDLを実行する前に、バッチなどの実行状況を確認すること
    • バッチなどの実行中にDDLを実行すると、同期が遅れる可能性がある
    • SHOW PROCESSLISTを使用して実行中のプロセスを確認可能
  • DDL実行完了後には以下を確認すること
    • DESC {テーブル名}を実行し、修正箇所が反映されていることを確認する
    • SHOW SLAVE STATUSを実行し、次を確認する:
      • Slave_IO_Running と Slave_SQL_Running がYesになっていること
      • Seconds_Behind_Master の値が0になっていること
      • Last_Error、Last_IO_Error、Last_SQL_Errorに何も表示されていないこと

Green環境にDDLを実行以降〜リリース当日

  • 定期的に同期確認を行い、エラーが発生していないか確認すること

  • binlog_formatの値がMIXEDの場合、特定の条件下で自動的にROW形式に切り替わることがあるため、注意が必要

    • slave_type_conversionsの値をALL_LOSSY,ALL_NON_LOSSY に設定することで、型違いの処理に関するエラーを回避できる

明日の レコチョク Advent Calendar 2024 は5日目「ネイティブアプリ開発チームの開発生産性の指標を定義してみた」です。お楽しみに!