データベースをFirebaseからMySQLに移行した話
データベース移行に至った理由
自社サービスの開発をおこなっていた際に発覚したことがきっかけでした。
データが増えたときに、パフォーマンス著しく悪くなる画面があることがわかりました。
開発しているアプリケーションの、非機能要件が曖昧でスケール前提で開発されていませんでした。
そのためデータベースから設計を見直し、より高可用性のあるアプリケーションになるように見直しをしたのがきっかけでした。
使用しているデータベースはfirestoreを採用していました。
そもそも開発しているアプリがfirestoreが苦手としている検索、絞り込みを多用しているため、firestoreでは不適切と判断しました。
その結果別のデータベースに移行することを決定しました。
開発環境
- 開発環境 NodeJS 18
- 言語 Typescript 5.1.6
- インフラ Google Cloud Platform
Step1.それぞれのデータベースの特徴を知る。
移行先のDB技術選定
移行先のDB候補
- Elastic Search
- Mongo DB
- MySQL
-
Mongo DB
-
メリット
- Mongo DBはfirestoreと同じ、ドキュメント指向のDBのためDB設計をほとんど見直さずに移行できる。
- firestoreと違い、正規化することができる。
-
デメリット
- 日系企業では採用事例が少ないため、MongoDBは情報が少ない。
-
メリット
-
Elastic Search
-
メリット
- 検索性能の向上が行える。
-
デメリット
- テーブル結合が難しい。
- トランザクションができず、ACIDに基づいたDB設計には向いていない。
-
MySQL
-
メリット
- 複雑なクエリや組み込み関数を使用することで、柔軟な検索が可能。
-
デメリット
- 容量を気にしないといけない。- フェイルオーバーを意識したインフラ設計が必要になる。
- カラムの追加が移行したとに難易度が高くなる。
-
メリット
-
メリット
以上のメリデリの考慮し、さらに他の事業部でもMySQLを採用していたためノウハウがあったため、MySQLに移行先を決めました。
Step2.現状のデータを知る
幸い前任者が正規化したデータ構造で構築していただいたため、カラムを崩さずに移行することができました。
Step3.データベース設計
アプリケーションがGCPのApp Engineで開発されていたため、同じGCPサービスであるCloud SQLを採用しました。
他のRDBであるCloud Spinnerという選択肢もありましたが、見送りにしました。
Step4.アプリケーションの実装
firestoreで開発していたインフラ層の関数を、MySQLに置換作業を行いました。
またDBのやり取りはPrismaというORMを採用してデータのやり取りを行いました。
Step5.データベース移行にあたって苦労した点
一般的にグレーノウハウを使用しないといけない場面があった。
-
1.テーブルのキーにnull許容を採用した。
- できるだけデータ欠損されたくないため、firestoreで保存していた型のままデータを移行したかった。元のデータがnull又は文字列を入れるキーがあったためそのまま引継ぎました。
- アプリケーション側としては問題ないのですが、支障が出る可能性があります。
-
MySQLの
null
は扱いが難しく[1]、SELECT ~ WHERE
構文で検索する際に気をつけなければなりません。
-
2.Json型を採用した。
- できるだけデータ欠損されたくないため、firestoreで保存していたデータ構造をそのまま引き継ぎたかった。
- 第五正規化まですると、データの整合性は堅牢になるが、テーブル結合の実装,作成、更新、削除のコストが膨大になってしまいます。もちろん運用面でデータを検索する際に複雑なクエリを打たないといけないのでコストが増加します。
元のデータがデータクレンジングされていなかった。
firestoreには外部キー制約というものがなく、手作業でデータを削除すると削除漏れが発生することがあります。
データ移行の際に付随するデータがなく、外部キー制約でエラーになってしまうことがありました。
サービスを運用しているとアプリケーションのデータ作成のロジックも、当然変わってきます。
その度に、データの型に後方互換性を保たないといけません。
それが難しいのであれば、データベースの整合性を保つためにデータベースマイグレーションを実施しなければありません。
マイグレーション漏れで移行が失敗したことがありました。
元のfirestoreがスキーマレスなデータベースのため、完全にデータを調べ切らないと移行ができないというのも苦労した点でした。🥹
やってよかったこと
クラステーブル[2]を意識して設計できたこと。
Javaでいうクラス継承の概念を,SQLではテーブル結合と使って再現することができます。
厳密に分けた訳ではなく、クラステーブル継承とシングルテーブル継承の中間をとってテーブル設計を行いました。
データ全体を見て、特殊キーが多い場合は継承元のテーブルのキーに含めていました。
テーブル結合のコストが低くなるかと考えました。
Cloud SQLをTerraformで環境構築したこと。
開発環境、検証環境、本番環境で構築する際に同じような設定をしないといけません。
インフラのコード化を行い、環境ごとの差分管理を楽にするためにTerraformで構築しました。
このような同じ作業をコードでまとめることができるので、Terraformでまとめることで工数削減ができました。
設定項目も漏れがなくなるのでよかったです。
MySQLにして運用面で大変だったこと。
-
メンテナンスコストが増えたこと。
Cloud SQLの特性上、メンテナンスでアプリを使えなくなる時間が発生するようになりました。
GCPで定期的にCloud SQLの自動メンテナンスを行います。
メンテ中はデータの登録、閲覧ができなくなります。
そのため強制的にアプリをメンテナンスを行わないといけないことになりました。
firestoreの時では考える必要がありませんでした。 -
DBのコストが膨大になったこと。
金銭的コスト
今回はパフォーマンス重視で金額面でのコストが大きくなりました。
というより Firestoreが運用料金が破格の安さということもあり。。。
当分のユーザー数を見込んでSQLインスタンスをグレードをアップデートしなくてもいいように、性能選定をしました。
その結果、一ヶ月あたりの運用コストがかなり上がりました。
今後のユーザー数増加に向けての先行投資というやつです。
認知的コスト
メンバー間で知見を共有する必要があります。自分のいた現場ではSQLの経験者ほぼいませんでした。
またFirestoreの場合はGUIからデータが操作できたのですが、今度はクエリを叩いてデータを操作しないといけません。
(2024/09現在はCloudSQL Studio[3]というPCにクライアントツールを入れなくても、GCPのWeb上からクエリーを叩けるサービスができました。)
個別に勉強会を開いたり、Notionで基本文法や障害時に役に立つクエリー集などをドキュメント化をしました。
-
DBの容量を気にしないといけないようになった。
firestoreの時は一切容量を気にせずに、使用することができました。
しかし、MySQLにしてから容量の上限値を設定するため、気にしないといけません。
幸いCloud SQLには使用容量が増えると容量を自動的に増やす設定ができます。
デメリットとしてこの設定で増やした容量は、減らすことができないため気をつける必要があります。
大量データを削除した際に、無駄に空き容量が大きくなってしまうってことですね。
容量x稼働時間に対して課金されるのでコストが無駄になってしまいます。
-
ログの監視が必要になったこと。
firestoreはマネージドサービスなので、エンジニア側で細かい設定やエラーの調整などが不必要でした。
MySQLではWebアプリごとに適切な設定ができるため、柔軟なアプリ開発ができます。
裏を返せば、エンジニアたちで細かい調整をしなければなりません。
その一つがログ管理です。インフラでのエラーログなどもこちらで管理するようになりました。
Cloud MonitaringでCloud SQLインスタンスのモニタリングを設定しSlackで通知するような仕組みを作りました。
終わりに
NoSQLであるFirestoreは費用も安く、柔軟なスキーマで構築できることが魅力です。
管理画面アプリのような、ドキュメント間の相関関係、統合関係の強いアプリケーションには向いていません。
このプロジェクトが現場リーダーデビューでした。
たくさんの壁に当たりましたが、堅実なプロダクトマネージャー、プロジェクトマネージャー、
実装、テストに協力していただいたメンバーには感謝いたします。
Discussion