GENIEE TechBlog
📚

Cloud SQLでの安全なMySQLアップグレードを行うためのアーキテクチャー改造とそこから得た学び

に公開

こんにちは。デマンド開発部の岩井と申します。 以前、2024年7月に新卒研修(bootcamp)の感想記事を投稿しました。
https://zenn.dev/geniee/articles/6952e13cf5c5d7
2025年の1月末、私がいたDOOH開発チームでMySQLのバージョンアップを行いました。DOOHとはDegital Out Of Homeの略で、屋外ビルボードやサイネージを利用した広告です。開発チームでは、スクリーン情報管理や広告登録、配信管理システムを提供しています。

背景

MySQL5.7はMySQL公式ではすでにサポートが終了しており、Cloud SQLでも2025年2月1日から延長サポートに入っており、2028年2月1日にはサポート終了が予定されています。
DOOHチームでは、2024年11月からMySQL8.4に向けてのアップグレードに着手し始め、2025年1月27日にインプレースアップグレードを用いて、8.4へのアップグレードを終了することができました。
一番知りたい内容かと思われる、実際に行った手順を最初に紹介し、その後、その計画に至る経緯と学びをお話しします。

記事の目的

MySQL5.7のアップグレード記事としては遅いですが、この記事では、主に以下の3つを目標にしています。

DBアップグレードに関する知見の共有や議論を促進する

今回私たちが行ったインプレースアップグレードの知見を共有するとともに、この記事を読んだ方が、GCP上でできるさらにより良い方法を投稿し、DBアップグレードの議論を促進したいです。

ジーニーの若手の裁量について知ってもらう

若手の裁量が大きいという話は就活生にとって重要な尺度かと思いますが、面談時間の都合などで詳細まで聞ける機会は少ないと思います。この記事で1年目でも任せてもらえる範囲を知ってもらえたら嬉しいです。

挑戦を通じることで多くの学びが得られることを伝える

新卒1年目でも大規模なタスクに挑戦することで、技術的知識だけでなく、計画作成、他部署との連携など、多面的なスキルを身につけられることを共有し、未知の領域への挑戦が、想像以上の成長機会になることを伝えたいと思います。

前提

なぜ新卒1年目がデータベースアップグレードをしているのか

ジーニーでは、社員一人一人がGOKRという目標を決めています。これはある程度自分で決めることが可能です。
学部卒業前、「GitHub、1200台以上のMySQL 5.7を8.0へアップグレード サービス無停止のまま成功させる」という記事を読みました。その後DOOHに配属されてから、チームで使われているデータベースも MySQL 5.7 であることを知り、「この課題を自分が解決できれば、大きな貢献になるのではないか」と思い、自分のGOKRに「データベースアップグレード」という項目を入れました。

使用技術について

Google Cloudを使っています。
https://cloud.google.com/?hl=ja

  • Cloud SQL ( MySQL )
  • Compute Engine
  • App Engine
  • Cloud Run / Cloud Run Functions
  • Cloud DNS

制限について

  • DBを停止できるのは深夜の3時間のみ
    • 前倒し/延長はビジネス的理由で不可

アップグレードの弊害

データベースアップグレードを行うにあたって以下の点が合わさって弊害となりました。

  1. データベースには多数のアプリがつながっている
  2. Cloud SQLはインスタンス名を変更できない
  3. GCPパッケージ経由でDBに接続をしている = インスタンス名をキーにして接続している
  4. インプレースアップグレード時に作られる旧バージョンのバックアップは、アップグレード後のインスタンスで復元させることができない。(なぜならバージョンが違うから…らしい。空のインスタンスを建てて確かめました。)

ここで、Go言語でGCPパッケージを使ってCloud SQLに接続するサンプルコードを読みます。
参考先は公式リポジトリです。( https://github.com/GoogleCloudPlatform/cloud-sql-go-connector)

    db, err := sql.Open(
        "cloudsql-mysql",
        "myuser:mypass@cloudsql-mysql(project:region:instance)/mydb",
    )

接続情報に、 project:region:instance とあります。 プロジェクト名:リージョン名:インスタンス名 で接続先を決定しているのです。TCP経由のときのIPアドレスやFQDNの役割です。
この何が弊害かというと、ロールバックに時間がかかることです。では、 old-instance インスタンスをアップグレード後になにかあり、ロールバックが必要になったケースを考えてみます。

  1. 予期せぬエラーが発生したためロールバックを決定する。
  2. バージョン5.7のバックアップで復旧する。この際、別名インスタンス old-instance-2 で立ち上がってしまう。
  3. インスタン名が変更できないため、多数のアプリの接続先情報を old-instance-2 に変更してすべてデプロイする。
    • こういった情報は立ち上げ時に読み込むため、変更するにはデプロイが必要。

当然こんな計画が通るわけありません。特に3が危険すぎますし、元の状態に戻すというロールバックの役割を満たせていません。

目標

前提となる制限下で、2025年2月1日までに安全にアップグレードを行う。(当時2024年11月)

実際の手順

当日に行われた手順は次のようなものになりました。

前日までの準備

  1. Cloud SQLインスタンスのプライベートIPをCloud DNSに登録します。
  2. 多数のアプリの接続方法をGoogle Cloudパッケージ経由からFQDNを使ったTCP接続に変更しました。
    ステップ1,2
  3. リードレプリカを作り、リードレプリカだけ8.0にアップグレードしておきました。
    ステップ3

当日の作業

  1. サービスを停止
  2. 5.7のインスタンスをread_onlyモードに
  3. 8.0のリードレプリカをプライマリに昇格
    ステップ3-1
    これを
    ステップ3-2
    こうする
  4. 5.7のインスタンスを停止(これをしないとアプリが古いDBにつながったままになります)
  5. 8.0を8.4にアップグレード
    ステップ4
  6. 8.4のインスタンスのプライベートIPをCloud DNSに登録し直す
    ステップ5
  7. サービスを再開

手順を作成するまで

私は特段MySQLに詳しかったり、InnoDBに詳しかったりするわけではないので、まず何をすべきなのかの調査を開始しました。

調査

Google Cloudのドキュメントを読む

まずはGoogle Cloudのデータベースのメジャー バージョンをインプレースでアップグレードするを読みました。基本的に詳しくはMySQLドキュメントを読むように促されますが、主に以下を知ることができました。

  • MySQLは次のメジャーバージョンまでしかアップグレードができない。つまり、MySQL 5.7 から一気にMySQL 8.4に自動アップグレードさせることはできない。
  • MySQL 8.4 にアップグレードするにはMySQL 8.0.37 以降のバージョンにアップグレードしてから 8.4にアップグレードする必要がある。
  • Upgrade Checker Utilityを使うと良い。
  • ユーザー認証や権限が変わるらしい

MySQLの公式ドキュメントを読む

この2つのドキュメントを読みました。特に気をつけたのがSQLの変更です。使えなくなるテーブル定義やストアドプロシージャ定義(トリガーなど)は、アップグレード時にエラーが出るため検証環境で発見できます。しかし、アプリから叩かれて初めてエラーが出るSQLは、全作業が終わったから初めてエラーが出る可能性もあるので最も注意すべきことだと思っていました。
SQLの変更で影響がありそうなものは、「 GROUP BY 句の非推奨の ASC 修飾子または DESC 修飾子は削除されます。 以前に GROUP BY ソートに依存していたクエリーでは、以前の MySQL バージョンとは異なる結果が生成される場合があります。 特定のソート順序を生成するには、ORDER BY 句を指定します。」でした。どうやら以前は GROUP BY 句に ASC, DESC をつけられたそうです。
まずチームのリポジトリで ASC, DESC が使われている周りを一通り見てみましたが、特にGROUP BYにつながっているようなところはありませんでした。また、チーム内に確認したところ、「そんな構文は使ったことがない」と言われ、SQLの影響はないとして進みました。

Upgrade Checker Utilityでのチェック

Cloud SQL Auth Proxyを使用し、DBとlocalhostで接続できる状態を作り、その上で https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-utilities-upgrade.html を参考にして、Upgrade Checker Utilityで本番インスタンスがアップグレード可能なのかをチェックしました。
結果、非推奨はいくつか出るものの、可能であることが分かりました。
Upgrade Checker Utilityを使うときの軽い注意点としては、Mac & brew環境でmysqlshをバージョン指定せずに実行すると、MySQL 9 との比較チェック結果が返ってくることです。

検証環境でのアップグレード調査

ここまで、インプレースアップグレードが可能な証拠が集まってきたので、実際に動かして試すフェーズに入りました。

  1. ステージングのDBインスタンスをクローン
  2. テーブルスキーマのdumpを取得
  3. MySQLのユーザーテーブルのSELECT結果を保存
  4. アップグレードしてまたテーブルスキーマ、ユーザーテーブルのSELECT結果を保存
  5. アップグレード前後でdiffを確認
    この結果、すべての外部キー制約に ON UPDATE RESTRICT が追加されていました。

ON DELETE RESTRICT は親レコードを削除できないようにする制約なので、これは親レコードをUPDATEできないようにする制約だ!インプレースアップグレードはできない!」…と思ってしまいました。(検索エンジン用ヒント:これは間違いです。)

調査によって作った初期案

  • インプレースアップグレードはできなそう
    しかし
  • テーブル定義などはそのまま使えそう
    だったため、Cloud SQLをdumpして移行しようと思いました。
    そのときの計画は、以下のようでした。現在のプライマリインスタンス名を instance-1 とします。
  1. instance-2 という名前でMySQL 8.4 インスタンスを建てる
  2. instance-2instance-1 を同じ権限でユーザーを作っておく
  3. instance-1 から作業日前にdumpを取り、GCSに置く
  4. 作業日に bin_log を参照しながら1日分のデータをdumpする
  5. instance-2 にデータを流す
  6. instance-1 を消す
  7. instance-2instance-1 という名前でクローンする
  8. instance-2 を消す

チーム内に共有したところ、問題ないならばこのまま進めていくという方針になり、インフラ・SREチームにレビューをしていただくことになっりました。

SREチームにレビューをもらう

SREチームに見ていただいたところ、「安全性が低い」ということで却下&指摘をいただきました。
指摘をいただいた箇所は主に以下の点です。

  1. dumpとdumpを流す時間はとても長いはずなので、制限時間内で終わらない
  2. インスタンスを消してしまうのでロールバックができない
    いま、作業の安全性の面でこれらを考えないなんてあまりに雑な計画だと感じますが、当時の私にとっては非常に新しい視点だと感じました。
    また、 「親レコードのアップデートができなくなる」などというかなり破壊的な制約がかかるとは考えにくいので、詳細に調べ直すようにもアドバイスを貰いました。

インプレースアップグレード調べ直し

ON UPDATE RESTRICT とは と検索するだけでインプレースアップグレードは問題ないことが分かりました。

結局 `ON UPDATE RESTRICT` とはなんだったのか

親レコードのキーを変更できないだけです。

以下のようなuserとprofileのような関係があったとき、userのidを変更できなくします。

「できなくします」と言ってもRESTRICTはデフォルトのため、本当に何一つ変わっていません。(正確には、MySQLでのデフォルトは NO ACTION 制約であり、 NO ACTIONRESTRICT が同じ制約として機能しています。ご参考までに。https://dev.mysql.com/doc/refman/8.0/ja/create-table-foreign-keys.html

インプレースアップグレードが使えそうだということで、クローンしたMySQLインスタンスで時間を測ってみたところ、メジャーバージョンを1つ上げるのに15~20分ほどかかることが分かりました。

SREチームに相談

相談前は、チームでも大きな反対はなかったので、ちょっとした作業手順の順番替えや準備項目を増やすよう指摘されるだけだと思っていました。
ほぼすべて変更になりました。
そもそもDBのアップグレードを2段階( 5.7 → 8.0, 8.0 → 8.4 )も行える時間はない ので、以下の手順方針をもらいました。

準備

  1. DNSへのDBのIPアドレス登録
  2. FQDNでアプリがMySQLに接続する構成に変更
    ステップ1,2
  3. カスケード(多段)レプリケーションを使って、MySQL 5.7 のプライマリインスタンスから、8.0にアップグレードしたインスタンスを生やし、さらにそのインスタンスから8.4にアップグレードしたインスタンスを生やしておく



実行日

  1. サービスを停止
  2. 8.4 のリードレプリカインスタンスをプロモートしてプライマリに昇格

    これを

    こうする
  3. DNSに登録するIPを8.4のインスタンスに変更する
  4. サービスを再開

方針の元になったのはこのドキュメントでした。相談後に「MySQLの公式ドキュメントにある方針を使って~」という文章が来て、スペシャリストとしての強さを知りました。

相談後案の可否調査後の妥協

ところで、冒頭で説明したとおり、最終的にはMySQL 8.0 から 8.4へのアップグレードはアップグレード作業時にそのまま待つことになりました。それは、相談で方針が決まった後、本当にできるのかを確かめるためにいくつかの調査をした結果、できないことがわかったからです。
相談後、主に2つの調査をしました。

  1. 現在使用している全環境でFQDN+プライベートIPでCloud SQLに接続できるのか
  2. 1段ずつアップグレードされたカスケードレプリカのトポロジを組めるのか

1. 現在使用している全環境でFQDN+プライベートIPでCloud SQLに接続できるのか

当時のDOOH開発の環境は、 GCE , GAE , Cloud Run Service , Cloud Run Functions でした。空のインスタンスと、 SELECT 1 を行うだけのプログラムを各環境でデプロイして調査しました。

2.カスケードレプリカのトポロジを組めるのか


この構成は、1段目のリードレプリカ名を PARENT_REPLICA_NAME に入れ、 gcloud sql instances patch --enable-bin-log PARENT_REPLICA_NAME を実行するとGUIから1段目のレプリカからレプリカを作れるようになりました。https://cloud.google.com/sql/docs/mysql/replication/create-replica?hl=ja#cascading-read-replicas
これに書いてありました。
しかし、いざ次の構成を作ろうとすると

「プライマリインスタンスから2バージョン離れたメジャーバージョンにはアップグレードできない」という理由で3段目のインスタンスのアップグレードができなかったのです。
現在は、もっと手前でこのような警告が出るようです。

ここから先は、どのくらいの時間を許容するか、作業を分けるのか1度でやるかを工数との兼ね合いで、1アップグレードは多くても30分ほどだということもあり、8.4へのアップグレードは1回の深夜作業内で達成させることをチームで決めました。
結果、最初 の計画になったのでした。

ふりかえり

チーム成果

  • 事故0件 : このあとDBが原因の事故は一度も起こりませんでした。
  • 当日作業時間2時間 : 1時間のバッファを持たせることができました。
  • 次回以降のデータベースアップグレードの基盤になるアーキテクチャの構築 : 今回、FQDNで接続できるようになったおかげで、次1段階アップグレードするときは、本当にDNSの登録のみで変更ができるようになりました。サービスの停止・復旧を入れても、おそらく30分で終了できるようになるはずです。

個人的な成果

  • 純粋にMySQLやGoogle Cloudに詳しくなれたこと
    MySQLの文字コードや照合順序に詳しくなりました。
    また、dumpを流す方法を考えたときに、utf8mb3のテーブルをutf8mb4で定義し直せば、MySQL9にもアップグレードできるDBを作れると思い、GCSにバックアップを置いたまま、Go言語でストリーム処理をしてローカルに落とすことなく中身を編集するプログラムを作ったりしたときもあり、データベースと全く関係のないGoogle Cloudの知識を得られました。

  • タスクに対する視野が広がった。
    データベースアップグレードという大規模保守タスクの中で、さらにデータベースとの接続方法変更という大規模保守タスクを行うという選択肢もあるのだと学べました。今まで、タスクには大きめの目標が1対1で紐づいていると思っていましたが、それすらも固定概念だったことに気がつけ、より広い計画を作れるようになったと思います。

恵まれた点

アプリがGo言語かPythonで書かれていたこと

Go言語はgo.mod、Pythonはrequirements.txtが必ずリポジトリに置いてあり、各アプリで使用しているパケージが一目瞭然でした。そのためどのアプリがデータベースにつながっているか = どのアプリに変更(接続方法の変更)を加えるのかを調べることが簡単にできました。

マネージャーやリーダーも情報収集に参加してくれたこと

個人開発のときはIP直接、就職後はCloud SQL Auth Proxyを使ってでしかMySQLにアクセスしたことがなかったので、FQDN経由でのデータベース接続は、かなり時間をかけてしまう予定でした。しかし、ここに関してマネージャーとリーダーが調べてくれたことで、早くSREチームとの相談を始めることが出来ました。

かなり任せてもらえたこと + 必要な補助をもらえたこと

調査・インフラチームとの相談・実行手順をまとめたドキュメントとスクリプトの作成が任されました。
町中で動いているシステムのデータベースのアップグレードは失敗すれば翌朝から連携する全ビジョンが真っ黒画面を出してしまいます。これは今まで感じたことのない危機感でした。これに立ち向かわせてもらった経験と、成功させた達成感は今後の私の支えになると思います。
また、インフラチームへの相談や、スクリプトをドキュメントに書くときの方法など、私の知識量ではたどり着けないフローには指示で導いてもらいました。
DOOHチームではクラウドインフラもほぼ開発チームで見ていたため、インフラチームに相談するという発想は自分では一度も浮かびませんでした。
また、ドキュメントに書くスクリプトも、焦って実行したときに動かないように # を先頭に入れるという工夫は思いつきもしなかったもので、非常に学びになりました。

これらの点は、本当に恵まれたと感じています。

反省点

早とちり

ON UPDATE RESTRICT について、よく調べないまま引き返してしまいました。これがなければ、1~2週間は早く計画完了にできたのではないかと思っています。よく聞く「人はサンクコストを気にしてやり方に固執してしまうが、大事なのは目標を達成することだ」ということを意識しすぎるあまり、コストの投げ捨てをしかけてしまいました。今後うまいバランスを身につけたいです。

今後の展望

データベースのアップグレードはプロダクトが続く限りつきまとう問題だと思います。次回こそ、GitHubのような利用者の誰にも気づかれないサービス無停止アップグレードができるよう、サーバー構成を進化させていきたいです。

もしCloud SQLで無停止アップグレードをやった方がいらっしゃいましたらコメントをいただけると幸いです。

GENIEE TechBlog
GENIEE TechBlog

Discussion