Java のレガシーなサービスで使ってきた Aurora MySQL v1 を v3 にバージョンアップした話

2022 年 2 月に Amazon Aurora MySQL 互換版のバージョン 1（以降「Aurora MySQL v1」と表記）の EoL 発表を受けて、Java のレガシーな Web サービスで使ってきた Aurora MySQL v1 を v3 にバージョンアップした記録です。

※ ~~後日 Zenn で本にまとめる予定なので、~~ この記事は要約版となります。

**2022/10/28 追記：**
Zenn で本にまとめました。

- **[Aurora MySQL v1 → v3（3.02.1 移行事例編）](https://zenn.dev/hmatsu47/books/aurora-mysql-do-book)**

https://zenn.dev/hmatsu47/books/aurora-mysql-do-book

## 対象サービスの概要

- Java の 15 年 over モノのレガシーなシステム
  - その上に Web アプリケーションが複数
- 5 年前 AWS に Lift & Shift したのを機に DB を MySQL 5.5 から Aurora MySQL v1 に移行
- DB の接続には MySQL Connector/J 5.1 または 8.0（一部）を使用

## なぜ Aurora MySQL v2 ではなく v3 に移行したのか？

いくつか理由はありますが、主に以下の 2 点が大きいです。

- 本家 MySQL 5.7 の EoL が迫る中、**一旦 v2 を挟むと最悪の場合 2 年で 2 回バージョンアップが必要になる**
  - Web フレームワークやミドルウェアなどの EoL 対応も予想される中それは厳しい
- 私個人が **[MySQL 8.0 の薄い本](https://github.com/hmatsu47/mysql80_no_usui_hon)** の製作・頒布を MySQL 8.0.24 の頃まで続けていたので**バージョン間差分の情報をそこそこ把握していた**
  - Aurora MySQL v3 のバージョン 3.01.x・3.02.x は MySQL 8.0.23 + α ベースでリリースされたのでほぼカバーできる

https://github.com/hmatsu47/mysql80_no_usui_hon

## バージョンアップの流れ

ざっくり箇条書きするとこんな流れでした。

- MySQL 5.6 / Aurora MySQL v1 と MySQL 8.0 / Aurora MySQL v3 の差分洗い出し
- Aurora MySQL のバージョンアップ手順に関する情報収集
- Zenn に記事を書き連ねていき、本に集約・整理
  - **[Aurora MySQL v1 → v3（3.02.1 移行計画編）](https://zenn.dev/hmatsu47/books/aurora-mysql3-plan-book)**

https://zenn.dev/hmatsu47/books/aurora-mysql3-plan-book

- アプリケーションコードの修正点を調査し仮修正
- 仮修正したアプリケーションの動作確認
  - 小規模なチームで実施
  - 問題が見つかったら追加で修正
- 修正・確認のポイントをまとめて開発チーム全体に展開
  - 仮修正した部分の確認と、より詳細な動作確認を実施
- バージョンアップ方法について計画
  - v1 → v3 レプリケーション併用でシステムの停止時間を短縮
  - プレリハーサルを実施して手順を確認
- リハーサルを実施
- 性能試験を実施
  - 移行後のインスタンスタイプ（ファミリー・サイズ）調整を計画
- 修正したアプリケーションを事前デプロイ
- バージョンアップ実施

## 困ったこと

### 出力結果のソート順変化

MySQL 8.0（の途中のマイナーバージョン）で`GROUP BY ... ASC/DESC`が廃止されましたが、

- `ORDER BY`のない`GROUP BY`
  - 従来バージョンでは`GROUP BY ... ASC`相当
- `ORDER BY`の列指定が非ユニーク
  - 従来バージョンも仕様上は「非ユニークな部分の順序は不定」
    - でも実際には一定の法則性があったのでそれに合わせてコードを書いていた

などの**出力結果のソート順がことごとく変わり**、

- すぐに修正すべきものとそうでないものの振り分け（判断）
- すぐに修正すべきと判断したものの修正

がかなりのボリュームになり、予定より作業期間が延びてしまいました。

### Aurora MySQL v3（MySQL 8.0）での挙動変化

こちら↓の仕様変更が原因で、それまでエラーなく処理されていた SQL 文（日付比較）でエラーが発生しました。

- **[MySQL 8.0.15 の前後で変わった文字列と DATE 型の比較について](https://rabbitfoot141.hatenablog.com/entry/2020/07/01/215728)（それが僕には楽しかったんです。／けんつさん）**

https://rabbitfoot141.hatenablog.com/entry/2020/07/01/215728

### MySQL Connector/J 5.1 → 8.0、あるいは 8.0 のマイナーバージョン間の挙動変化

仮修正の段階で動作確認したときには↓の 1 つ目の項目が見つかっただけでしたが、最終的にはかなり出てきました。

- `NOT NULL`かつ`DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`の`TIMESTAMP`・`DATETIME`列に対して`null`で`UPDATE`するとエラーになる
  - パラメータグループでデフォルトどおり`explicit_defaults_for_timestamp`に`1`（有効）を指定した状態で実行
  - MySQL Connector/J 5.1 の頃は`UPDATE`時のタイムスタンプで更新
  - MySQL Connector/J 8.0 では「`NOT NULL`列に`null`で`UPDATE`することはできない」旨のエラーが発生
- 行`INSERT`直後に`ROW_COUNT()`を取得すると常に`0`が返る
  - MySQL Connector/J 8.0.29 のバグ（？）
    - MySQL Connector/J 8.0.28 にバージョンダウンして回避
- `java.sql.ResultSet.getObject()`で一旦取得してから`java.sql.Timestamp`型に値を変換するコードが`Exception`に
  - フォーマット無指定の状態（デフォルト）で渡ってくる日付のフォーマットが変わったため（秒以下が無くなった）
  - `java.sql.ResultSet.getTimestamp()`で取得するよう変更
- `FLOAT(M,D)`・`DOUBLE(M,D)`で定義された列を`java.math.BigDecimal`型に取り込むとスケールがずれることがある
  - MySQL Connector/J 5.1 の頃は`D`で定義された値が自動的にスケール指定されていた
  - MySQL Connector/J 8.0 では明示的に`setScale(D)`する必要がある
- Collation（照合順序）が文字セットと不一致でエラーになることがある
  - MySQL Connector/J 8.0.25 以前は接続パラメータ`connectionCollation`が無指定の場合、システム変数`character_set_server`で指定された文字セットのデフォルト照合順序が採用されていたが、8.0.26 以降で文字セットと照合順序の決定ルールが変わった結果、当該アプリケーションでは文字セットに合わない照合順序が指定されてしまった

これら以外にも、MySQL Connector/J 8.0.20 以降では特に

- 日付（日時）データの処理方法の変更
  - `TIMESTAMP`型の 2038 年問題に関連する仕様変更も影響？
- TLS 1.0/1.1 廃止

などマイナーバージョン間の差異がかなり出ており、**迂闊にバージョンを上げられない**印象です。

### DMS レプリケーション使用不可

この問題に当たってしまいました。

- **[AWS DMS（CDC）で MySQL to MySQL 移行時の TIMESTAMP 不一致問題](https://zenn.dev/hmatsu47/articles/mysql-dms-cdc-timestamp-mismatch)**

https://zenn.dev/hmatsu47/articles/mysql-dms-cdc-timestamp-mismatch

無保証（自己責任）になりますが、やむなく Aurora MySQL v1 → v2 → v3 の binlog（ROW 形式）レプリケーションに切り替えて乗り切りました。

## 意外と困らなかったこと

調査の中でいくつか見つけていた「問題になりそうな点」のうち、対象サービスではあまり問題にならなかった点もいくつかありました。

### 大量`IN()`の失速

MySQL 5.6 → 5.7（Aurora MySQL v1 → v2）のバージョンアップでよく話題になる、**「`IN()`で数千・数万の値を列挙するとフルスキャンになる」** 問題は生じませんでした。

ただしこれは「v3 にバージョンアップしたから問題にならなかった」のではなく、アプリケーションで処理する`IN()`の列挙数がそれほど多くなかったのが理由です。

### 全件`COUNT(*)`の失速

これはデータの新旧比較用 SQL 文を用意している中で見つけた問題でした。

- **[MySQL 8.0 で SELECT COUNT(*) が失速する](https://zenn.dev/hmatsu47/articles/mysql80-count-slowdown)**

https://zenn.dev/hmatsu47/articles/mysql80-count-slowdown

アプリケーションではテーブル（またはパーティション）全件の`COUNT(*)`をしていなかったので、バージョンアップ作業時のデータ新旧比較が若干遅くなる以外の影響はありませんでした。

### 実行計画の変化（その他）

わずかに生じた程度でしたが、データ量などの状況を見ながらオプティマイザヒントで対応しました。

- **[小ネタ／MySQL 8.0（Aurora MySQL v3）にバージョンアップしたときの実行計画調整にオプティマイザヒントを使う](https://qiita.com/hmatsu47/items/9b1090111f2683d386bc)**

https://qiita.com/hmatsu47/items/9b1090111f2683d386bc

## 全体を通しての感想

概ね事前予想どおりではあったものの、やはり大変でした。

まず、収集した情報をまとめて開発チーム全体に展開したまでは良かったのですが（そこまでは順調）、DB の知識が少ないメンバーにもタスクが割り振られた結果、

- 作業の手が止まる
- 間違った作業をする（例：一時テーブルと内部一時テーブルを混同して動作確認）

などの無駄が発生しました。

このあたりは、開発チーム全体に展開する前に考慮しておくべきでした。

また、対象システム上にあるアプリケーションは複数に分割されているものの、（歴史的経緯により）**「開発するチームの境界とアプリケーションの境界が異なる」**（それぞれが複雑に入り組んでいる）状態で、チーム間で作業がバッティングして効率的に進められない問題も発生しました。

その結果、当初は「修正が終わったものから（DB のバージョンアップ前に）順次デプロイ・リリースしていく」予定を立てていたのにそれができず、DB のバージョンアップ直前に全ての修正をまとめてデプロイすることになってしまいました。

そして侮れなかったのが **MySQL Connector/J の挙動変化**です。

後から考えると、一旦 **MySQL Connector/J のバージョンアップ対応だけ先に実施しておいたほうが良かった**かもしれません。

今回は Java で MySQL Connector/J 使っていたからこそ当たってしまった問題かもしれず、他言語向けのライブラリでは必ずしも同様の状況になるとは限りませんが、ある意味それが安全にバージョンアップする上で大きなポイントだったと思います。

## 関連資料（発表スライド）

- **[Aurora MySQL v1 → v3 の移行を準備する](https://speakerdeck.com/hmatsu47/aurora-mysql-v1-nil-v3-falseyi-xing-wozhun-bei-suru)（JAWS-UG 朝会 #32）**

<script async class="speakerdeck-embed" data-id="fbc12a221bd94b35a7f1d5c6b886cccc" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script>

- **[Aurora MySQL v1 → v3 の移行（調査・計画編）](https://speakerdeck.com/hmatsu47/aurora-mysql-v1-nil-v3-falseyi-xing-diao-cha-ji-hua-bian)（JAWS-UG 浜松 AWS 勉強会 2022#4）**

<script async class="speakerdeck-embed" data-id="e3cc8bd44ded4a0e80e839a8770ae643" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script>

- **[Aurora MySQL v1 → v3 移行で 気を付けたほうが良いこと （7 つ + α）](https://speakerdeck.com/hmatsu47/aurora-mysql-v1-nil-v3-yi-xing-de-qi-wofu-ketahougaliang-ikoto-7-tu-plus-a)（JAWS ミート 〜Re:Born 東海道〜）**

<script async class="speakerdeck-embed" data-id="f91871514c9d4817814a827b108d8ddf" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script>

- **[Aurora MySQL v1 → v3 移行の経過報告](https://speakerdeck.com/hmatsu47/aurora-mysql-v1-v3-yi-xing-nojing-guo-bao-gao)（JAWS-UG 名古屋 オンラインフリーテーマ会）**

<script async class="speakerdeck-embed" data-id="a0be0e9ffcd44d208846773902ff306a" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script>

**2022/10/28 追記：**

- **[Aurora MySQL v1 → v3 の移行完了報告](https://speakerdeck.com/hmatsu47/aurora-mysql-v1-v3-noyi-xing-wan-liao-bao-gao)（JAWS-UG 浜松 AWS 勉強会 2022#10）**

<script async class="speakerdeck-embed" data-id="226b81e2ae7b47afaee062772aef1fdf" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script>