この記事は CyberAgent Developers Advent Calendar 2024 9日目の記事です。
こんにちは、Serveice Reliability Group(SRG)の鬼海 雄太(@fat47)です。
SRGは主に弊社メディアサービスのインフラ周りを横断的にサポートしており、既存サービスの改善や新規立ち上げ、OSS貢献などを行っているグループです。
本記事ではアメーバブログを中心としたサービスの大規模Aurora MySQLクラスタを、サービス無停止でアップグレードするためにどのような取り組みをおこなってきたかをお伝えします。
Amebaの大規模Aurora MySQLクラスタについて
アメーバブログは2004年にオープンしたブログサービスであり、今年20周年を迎えた長期間運用されているサービスです。Amebaは、このアメーバブログを中心に様々なサービスで構成されています。
2021年頃よりAmebaのシステム刷新が開始され、2023年にプライベートクラウドからAWSへの移行が完了しました。
システム刷新の詳細については、CyberAgent Developer Conference 2022の資料をご覧ください。
事業と歩むAmebaシステム刷新の道 / the-road-to-ameba-system-renovation-cadc
Amebaではマイクロサービスアーキテクチャを採用しており、各マイクロサービスごとにデータベース(Aurora MySQL version2)が存在しています。
環境としては、開発環境、ステージング環境、本番環境などがあり、それぞれの環境において複数のマイクロサービスが稼働しています。
そのため、アップグレードの対象となるAuroraクラスターはマイクロサービスの数 × 環境の数となり、百数十クラスターに達していました。
Aurora MySQL version2の延長サポート料金の発表
2023年9月にAWSより、Aurora MySQL version2の延長サポートサービスとその料金が発表されました。
詳細は以下のリンクをご参照ください。
- 2024年10月31日以降、Aurora MySQL version2の標準サポートは終了します。
- 2024年11月1日以降は、延長サポートにより最大3年間のサポートが提供されます。
- 東京リージョンでの延長サポートの料金は、vCPU1つにつき1時間ごとにUSD 0.12です。
仮に合計50vCPUが存在しているAuroraクラスター環境の場合、以下のような費用がかかります。
50vCPU * 24h * 30day * USD 0.12 = USD 4,320
1ドル150円で計算すると、月額648,000円、年額換算で約777万円となります。
この費用はインスタンス費用とは別に必要となり、無視できないコストとなります。
Amebaのサービス要件整理と課題
開発チームとアップグレード対応のキックオフミーティングを行い、Amebaでのメンテナンスに関するサービス要件について認識を共有し課題を洗い出しました。
- 特定のサービスのみをメンテナンスモードに切り替える機能が存在しない場合がある。
- そのため、メンテナンスを実施する際にはAmebaブログ周辺の全てのサービスをメンテナンスモードに切り替える必要がある。
- メンテナンスの時間中に本番環境すべてのAurora MySQLのアップグレードを完遂しなければならないため、長時間に及ぶことが予想される。
以上の課題により、サービスを無停止でアップグレードを行える方法を検討していくこととなりました。
サービス無停止でアップグレードが実施可能と判断
Amebaブログなどのサービスは、ユーザーがブログを書き込む頻度よりも、読む頻度のほうが圧倒的に多い特徴があります。
あるマイクロサービスでは、参照クエリが99%以上で更新が1%以下という極端な例も存在しています。
これは、特定のAurora MySQLクラスターにおけるライターインスタンスとリーダーインスタンスのクエリ量の差にも表れています。
さらにAmebaブログではCDNを活用しており、キャッシュに載っているブログ記事の場合は参照クエリすら発生しません。特に著名人のブログなど、アクセス数が多いブログ記事ほどキャッシュで返される確率が高くなります。
このため、Aurora MySQLのアップグレードによるダウンタイムをできるだけ減らすことができれば、サービスへの影響を最小限に抑えることが可能と考えました。
RDS Blue/Green Deploymentsとは
RDS Blue/Green Deploymentsは2022年12月に発表された機能で、マネージメントコンソール上から操作することで、Greenクラスタ環境の作成、Blue/Green間のレプリケーション、Blue/Greenの切り替えを簡単に実施できる機能です。
この機能を活用することで、ダウンタイムを1分以内に抑えることが可能となり、サービスを無停止でアップグレードすることができると判断しました。
補足:実際にはサービスの責任者に説明をおこない、ダウンタイム最大1分を許容するという合意を得て対応を進めています。
Blue/Green Deployments機能のBlue/Green切り替え動作の流れ
本機能の動作の流れは以下の通りです。
- 稼働中のクラスター環境(Blue)からクローン環境(Green)が作成されます。
- GreenクラスターをAurora MySQL version3にアップグレード済みの状態で作成することが可能です。
- Blue/Green間は論理レプリケーションが設定されているため、Blueクラスターで更新されたデータも常に反映されます。
- コンソール上からBlueからGreenへの切り替えが可能です。
- アプリケーションが参照するエンドポイントは変更不要で、エンドポイント内部の向き先がBlueからGreenに切り替わります。
- 切り替え中はBlue/Greenクラスターの両者で書き込みがブロックされます。
- 切り替え後の旧Blue環境はスタンドアロン環境として残ります。
- 旧Blue環境のクラスター名やインスタンス名には-old1が追加されます。
- Green環境はBlue環境としてクラスター名などもすべてBlueと同名にリネームされます。
機能検証と切り戻しの課題
本機能の検証を進めた結果、Blue/Green切り替え時のダウンタイムは1分以内で済むことを確認しました。
しかし、当時は大きな課題がありました。
それは「Blue/Green切り替え後に元のBlue環境に切り戻す機能がない」ということです。
Blue/Green切り替え後にクリティカルな問題が発生し、元のversion2である旧Blue環境に戻したい場合でも、すぐに戻せる手段がありませんでした。
旧Blueクラスター環境自体は存在していますが、サービスを無停止で切り替えた場合、旧Blue環境と新Blue環境とではデータの差異が発生してしまうため、そのまま旧Blue環境を切り戻し先として利用することはできませんでした。
補足:2024年12月現在のBlue/Green Deployments機能では、Blue/Green切り替え時に切り替えた瞬間の新Blue環境のbinlogファイルとpositionがイベント欄に表示されるようになりました。
これによってサービス無停止でも新Blueクラスタと旧Blueクラスタ間でレプリケーションを貼れるようになるため、切り戻し環境として活用しやすくなりました。
ただし切り戻しはあくまで最後の手段であるので、できるだけBlue/Green切り替え前に安全を確かめた上で進めることが重要です。
より安全にアップグレードを行うためにやったこと
容易に切り戻しができないため、Blue/Green切り替え前にできるだけ安全であることを確認してから進める必要があります。
今回の事例では以下のツールや機能を活用しながら、慎重にアップグレードの確認を進めました。
MySQL Shellのアップグレードチェッカーの実行
MySQL Shellとは公式の高機能なMySQLクライアントで、MySQL Clientと別の存在です。
多数の便利な機能があり、その中にアップグレードチェッカユーティリティであるutil.checkForServerUpgrade()
関数があります。
https://dev.mysql.com/doc/mysql-shell/8.0/ja/mysql-shell-utilities-upgrade.html
この関数を使用することでMySQL5.7の環境に対して、MySQL8.0の予約語とバッティングしたテーブルやカラムがないか、UTF8(utf8mb3)の文字コードがないかなど約20項目程度のチェックを実施できます。 利用の注意点として、このツールはあくまでもOSSであるMySQL5.7からMySQL8.0のアップグレードのチェックツールとして提供されています。
そのためAurora MySQL環境で実行した場合、Aurora MySQL特有の仕様で特定部分がチェックに引っかかることがあります。
Aurora MySQLでチェックにひっかかる部分もありますが、他のチェック自体はちゃんと確認ができますので、このツールを活用しアプリケーションの修正が必要な箇所がないかを確認しました。
pt-upgradeを活用して検証環境で本番クエリの再現検証
Aurora MySQLアップグレードにおいて一番懸念していたのが参照性能の劣化でした。これは他社の事例などの情報を収集する中で、最も多い事例として認識していました。
参照性能の劣化を事前に検知し修正することができるかが重要となってきます。
開発環境やステージング環境での動作検証では、DBのデータ量が少なく性能劣化が再現しない可能性が高いです。本番環境相当のデータを用意したとしても、本番に流れているクエリをすべて網羅的に実行できるような負荷試験シナリオの作成が必要になります。
今回はAurora MySQLアップグレード対象の数が非常に多く、すべてを網羅した負荷試験を期間内に実施することは難しく、別の手段が必要となりました。
そこで今回採用したのが、
「本番環境のクエリログを一定期間収集し、そのクエリログを検証環境A(Aurora MySQL version2)と検証環境B(Aurora MySQL version3)でリプレイし、クエリ性能の劣化がないか比較する」という方法です。これを実現するために、Percona社が公開しているPercona Toolkitに含まれるpt-upgradeというツールを活用しました。
pt-upgradeは2つの異なるMySQLサーバーに対してクエリを実行し、その結果や実行速度を比較してくれるツールです。
まず本番環境クラスターでクローン機能を使い、検証環境Aと検証環境Bを作成します。検証環境BだけをAurora MySQL version3にインプレースアップグレードします。
pt-upgradeは以下のコマンドで実行します。
下記コマンドは、検証環境Aと検証環境Bのエンドポイントに対して、query.logに記録されたクエリを実行するものです。
pt-upgrade h=検証環境Aエンドポイント h=検証環境Bエンドポイント query.log
結果はこのように表示されます。
#-----------------------------------------------------------------------
# Logs
#-----------------------------------------------------------------------
File: query.log
Size: 1255998
#-----------------------------------------------------------------------
# Hosts
#-----------------------------------------------------------------------
host1:
DSN: h=検証環境Aエンドポイント,P=3306
hostname: ip-172-23-1-123
MySQL: MySQL Community Server (GPL) 5.7.12
host2:
DSN: h=検証環境Bエンドポイント,P=3306
hostname: ip-172-23-1-234
MySQL: Source distribution 8.0.28
########################################################################
# Query class C9BF7E7CXXXXXXXX
########################################################################
Reporting class because there are 1 query diffs.
Total queries 3
Unique queries 3
Discarded queries 0
select * from test where id = ?
##
## Query time diffs: 3
##
-- 1.
0.100970 vs. 3.212145 seconds (31.8x increase)
SELECT * FROM test where id = 110003
-- 2.
0.233584 vs. 7.357769 seconds (31.5x increase)
SELECT * FROM test where id = 229478
-- 3.
0.102299 vs. 3.821666 seconds (37.4x increase)
SELECT * FROM test where id = 221111
#-----------------------------------------------------------------------
# Stats
#-----------------------------------------------------------------------
failed_queries 0
not_select 0
queries_filtered 0
queries_no_diffs 5
queries_read 26
queries_with_diffs 3
queries_with_errors 0
検証環境Aに対して、検証環境B(Aurora MySQL version3)はSELECT * FROM test where id =?
のクエリが30倍程度遅くなっている。ということがわかります。
低速になるクエリを洗い出して一つずつ解決策を考えていくことが可能となります。
Route53のレコードの加重ルーティング
Blue/Green Deployments機能を利用してGreenクラスタを作成すると、Blue/Green切り替え実施までの動作確認を目的としたGreenライターエンドポイントとGreenリーダーエンドポイントが生成されます。
このGreenリーダーエンドポイントに対してアプリケーションの参照クエリの向き先を変更することで、参照クエリのみアップグレード後の環境で実行することが可能です。この際、パフォーマンス問題などが発生した場合には、アプリケーションの向き先を元のエンドポイントに戻してデプロイし直すことで、迅速に切り戻すことができます。
私達はアプリケーションから直接Greenリーダーエンドポイントを指定するのではなく、
Route53のプライベートホストゾーンにレコードを作成することにしました。
BlueリーダーエンドポイントとGreenリーダーエンドポイントを同名のレコードで作成し、加重ルーティングでそれぞれ重みを10:1に設定をしました。
これにより、アプリケーションからの参照コネクションをBlue環境10、Green環境1の割合でコントロールすることが可能となります。仮にGreen環境への参照クエリでパフォーマンス劣化が発生したとしても、影響範囲は抑えられますし、Green環境への重み付けを0にすることで迅速に切り戻すことが可能です。
Aurora MySQLアップグレード中に起きたトラブルの一例の紹介
ここでは私たちがAurora MySQLのアップグレード中に経験した、様々なトラブルの中からいくつかの事例をご紹介いたします。
異なるインデックスが選択されたことによるSELECTスローダウン
これは最も多く発生したトラブルの一例です。
Aurora MySQL version 2(MySQL 5.7)とversion 3(MySQL 8.0)では、オプティマイザの挙動が異なるため、同じクエリで同じテーブルを参照していても、異なるインデックスが選択されることがあります。この結果、応答速度が極端に悪化するケースがありました。
以下に、悪化した参照クエリをAurora MySQL version 2とversion 3でEXPLAINした結果を示します。
異なるインデックスが選択されてしまっていることがわかります。
FORCE INDEXとINVISIBLE INDEXによる対応
この問題への対応として参照クエリにFORCE INDEXを付加し、使用したいインデックスを強制的に指定する方法を採用しました。
ただし、すべてのクエリにインデックス指定を付加することは現実的ではなく、運用コストも高くなります。そのため、極力インデックス指定はせず、やむを得ない場合のみ指定するようにしています。
特定のテーブルではセカンダリインデックスの数が異常に多く、それによりオプティマイザが正しいインデックスを選択できない場合がありました。このような場合、必要最低限のインデックスのみを残し、オプティマイザがインデックス選択でブレないようにする必要があります。
今回はMySQL 8.0から利用可能な機能であるINVISIBLE INDEX
を活用しました。既存のインデックスに対して以下のようなALTER TABLEコマンドを実行することで、INVISIBLE、つまり不可視なインデックスとして扱うことができます。
ALTER TABLE 【テーブル名】 ALTER INDEX 【INDEX名】 INVISIBLE;
従来のDROP INDEXでは、インデックスを削除した後に何らかの影響が出た場合、再度インデックスを作成する必要がありましたが、INVISIBLE INDEXの場合は、ALTER TABLEコマンドでVISIBLEに変更するだけで、再び利用可能なインデックスに戻すことが可能です。 このアプローチによりインデックスの管理が柔軟になり、オプティマイザの挙動を制御しやすくなりました。
use_invisible_indexesヒント句による対応
INVISIBLE INDEXを活用し最低限のインデックスにしたところ、ほとんどのクエリは安定したパフォーマンスを発揮するようになりました。
しかし、特定のSELECTクエリだけは不可視にしたインデックスを使用しないといけない場面が発生しました。そのインデックスを可視化すると、今度は別のクエリのパフォーマンスが悪化してしまうという状況になりました。 そこで活用したのがuse_invisible_indexes
というヒント句であり、対象のSELECTクエリに付加して使用します。
SELECT /*+ SET_VAR(optimizer_switch = 'use_invisible_indexes=on') */ COUNT(*) \
FROM テーブル名 FORCE INDEX (不可視化されたインデックス名);
このヒント句が付加されたクエリは、不可視状態のインデックスも利用することが可能となります。
これにより他のクエリに影響を与えることなく、問題のクエリだけを改善することができました。
MySQL8.0の仕様によるSELECT COUNTのスローダウン
MySQL8.0には、5.7と比べてSELECT COUNTが低速になる場合があるという問題がありました。同じインデックスを利用していても数十倍遅くなるケースに遭遇しました。
これはMySQL8.0のバグであり、MySQL8.0.37では解消されています。 しかし、今回のAurora MySQL version3ではサポート期間の長いLTSであるversion3.04(MySQL8.0.28互換)を選択していたため、この修正は適用されていません。
この低速になる問題をなんとか回避するために、MySQL8.0.37のリリースノートを確認したところ、以下のバグフィックスが適用されていることがわかりました。
InnoDB:
MySQL no longer ignores the optimizer hint to use a secondary index scan, which instead forced a clustered (parallel) index scan. In addition, added the ability to provide an index hint that forces use of a clustered index. (Bug #100597, Bug #112767, Bug #31791868, Bug #35952353)
ヒント句をつけてもセカンダリインデックスが使用されずにクラスタ化インデックス(PKのインデックス)が利用されてしまうというバグです。
EXPLAINの結果ではセカンダリインデックスが使われるように見えるが、実際は使われておらず、WHERE句を追加したときのみ使用されたという報告がされていました。
並列読み取りスレッドのページを事前に読み込む機能が、クラスタ化インデックスにおいて必要以上にページを読み込んでしまい、過剰な読み取りI/Oを引き起こしてSELECT COUNTのパフォーマンスが悪化しているというバグです。
つまり、低速化したクエリにヒント句(USE INDEXやFORCE INDEX)でセカンダリインデックスを指定し、WHERE句で条件付けを必須にすれば低速化を回避可能である。ということがわかりました。
実際の動作検証でCOUNTクエリの速度が改善したことを確認ができました。
詳しくは弊チームブログで記事に書いていますのでご参照下さい。
MySQL8.0で低速になったSELECT COUNTを高速化する – CyberAgent SRG #ca_srg
特定クエリだけが照合順序エラーになる
Aurora MySQL version3に対してクエリを実行するとある特定のGoアプリケーションだけ、異なる照合順序(Collation)を比較しようとしたエラーが出ました。
Error 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_0900_ai_ci,IMPLICIT) for operation '='
MySQL5.7でのデフォルトの照合順序はutf8mb4_general_ci
ですが、MySQL8.0からutf8mb4_0900_ai_ci
に変更されています。
今回エラーが出ていたテーブルとカラムでは、MySQL5.7との互換性を重視し、utf8mb4_general_ci
を指定していました。
なぜエラー文にはutf8mb4_0900_ai_ci
が出てきたのかと言うと、go-sql-driver/mysql の接続オプションとMySQL8.0の仕様が原因でした。
今回エラーになったコードのSQLクエリのサンプルがこちらです。
SQLのユーザー定義変数を利用し、カラムの値と比較するような処理をおこなっていました。
このSQLが実行されるGoのアプリケーションではgo-sql-driver/mysqlが利用されており、接続オプションにはcharset=utf8mb4
だけが指定されていました。
go-sql-driver/mysqlでCOLLATION未指定の場合のデフォルト値は、utf8mb4_general_ci
となっており、これは内部的にはcollation=utf8mb4_general_ci
と同一となります。
しかし、今回のように文字セットであるcharset=utf8mb4
だけを指定しているとMySQL接続時にSET NAMES utf8mb4;
だけが実行されます。
SET NAMES による文字コード指定だけが実行されると、MySQLの仕様上システムのデフォルトのCOLLATIONが設定されてしまいます。
つまり、MySQL5.7ではutf8mb4_general_ci
がセットされますが、MySQL8.0の環境では、utf8mb4_0900_ai_ci
がセットされてしまいます。
今回のエラーでは実際のカラムに設定されているCollationがutf8mb4_general_ciであるが、ユーザー定義変数にはデフォルトのutf8mb4_0900_ai_ciがセットされてしまい、その2つを比較するような処理をおこなっていたため、異なる照合順序によるエラーが出ていました。
この現象についても下記のブログにて再現コードと共に説明していますのでご参照ください。
Aurora MySQL version3(MySQL8.0)にアップグレードしたら、ある特定のクエリだけ照合順序エラーになった話とその解決策 – CyberAgent SRG #ca_srg
最後に
Amebaでは、複数の機能やツールを駆使し、無事にすべてのAurora MySQLクラスタのアップグレードをサービス無停止で期限内に完了することができました。
この成功は、AWSのソリューションアーキテクトやサポートチームの皆様のご協力によるものです。心より感謝申し上げます。
今回のアップグレードによりAurora MySQL version 3が導入されたことで、Aurora I/O OptimizedやAurora Serverless v2といった新たな機能が利用可能になりました。
今後はこれらの機能を検討しながら、Amebaのデータベースの改善に取り組んでいきます。
また、今回選択したversion3.04の標準サポート期限は2026年10月までとなっており、次期アップグレードに向けて最新の情報を継続的に追い続ける所存です。