サーバーサイドエンジニアの田実です!
Yappliはノーコードでネイティブアプリを作れるプラットフォームで、システム構成としてマルチテナントアーキテクチャを採用しています。 一言にマルチテナントアーキテクチャと言っても様々な実装方式があり、各プラットフォームの要件に応じて適切にアーキテクチャを選択していくことが重要です。
本記事ではYappliにおけるマルチテナントアーキテクチャの実現方法をYappliが抱える課題や特性を踏まえてご紹介します。
- 前提: Yappliの機能要件
- SQLite3によるマルチテナントアーキテクチャ
- SQLite3の運用における課題
- データ集計の課題解決: ETL
- スケールアウト・冗長化の課題解決: マイクロサービス化とAmazon EFS
- 現行アーキテクチャの課題について
- まとめ
前提: Yappliの機能要件
Yappliには「再構築」と呼ばれる、下書きの状態(プレビュー)を本番に公開する機能があります。
この機能によって、編集・保存したコンテンツをすぐに公開するのではなく、各画面のデザイン・全体的な構成を確認してからコンテンツを公開できるようになります。コンテンツ運用におけるステージング環境を本番環境上に用意しているようなイメージです。データはプレビューと本番の2種類用意されることになり、プレビューを本番に公開する場合はプレビューのデータを本番にコピーすることで実現できます。
また、スナップショット機能というコンテンツのバックアップをプラットフォーム上で取得・復元できる機能もあります。 この機能によって、お客様が何か間違ったコンテンツを入稿した場合にスナップショットから復元できるため、安心な運用ができるようになっています。 こちらも原理としては再構築と同じで、プレビューからスナップショットのデータを作ったり、バックアップからプレビューにデータをコピーする必要があります。
このように1つのアプリでも複数の状態のデータが混在するのがYappliのプラットフォームの特性になります。
SQLite3によるマルチテナントアーキテクチャ
SQLite3は1ファイル1DBのファイルベースのRDBMSです。
Yappliでは各アプリ・プレビュー・本番・バックアップごとにSQLite3のデータベースを作って管理しています。 アプリごとにDBファイルを分けることで、マルチテナントの要件を満たしつつコンテンツ情報のプレビュー・本番の切り替えもアクセス先のファイルを切り替えるだけで実現できるようになります。
また、再構築・スナップショット機能に関してもファイルコピーで実装可能です。
運営者であるヤプリが問い合わせ対応で利用するようなバックアップを取りたいケースもあり、この場合も定期的にS3などのストレージにDBファイルをアップロードするだけで実装できます。 また、DBファイルをダウンロードして適切な場所に設置するだけで状況再現ができるので、本番に近いデータで開発しやすいメリットもあります。
SQLite3以外にもXMLやJSONのようなシリアライズ化されたファイル形式もありますが、Yappliの場合はコンテンツ量が多いアプリがあったり柔軟な検索を求められるケースも多いため、RDBMSであるSQLite3を選択しています。
このように各機能の実現のしやすさと開発・運用の点で、SQLite3はメリットが多く妥当な選択だったと思います。
SQLite3の運用における課題
様々なメリットがある一方で、運用面での課題もあります。
この方式だとアプリ数分のDBファイルを管理することになるため、SQLを使った一括のデータ集計・変更ができません。 例えば全アプリにおけるコンテンツの設定状況を見るためには、以下のような擬似コードで各DBファイルにアクセスして集計していました。
for 全アプリ { for プレビュー、本番 { open("sqlite3:/path/to/{アプリ}{ステータス}") records = exec("SELECT * FROM コンテンツ") print records } }
そのため、不具合や変更に対する影響範囲の調査がしづらく、手軽にデータ分析することもできませんでした。
また、ファイルベースが故のスケールアウト・冗長化の課題もあります。 読み込みのスケールアウト・冗長化のために、APIサーバーの各ホストにSQLite3ファイルをコピーすることによって実現していました。
しかしながら、コピーのタイミングや失敗したときの制御を考慮しなければならないため、本番反映の処理が複雑化していました。
本体サーバと呼ばれる1台のAPIサーバーでSQLite3を一元管理しているので、書き込みの冗長化もできません。
さらに、複数のDBファイルを管理しているため、スキーマのマイグレーションがしづらい課題もあります。 実際の運用でどうしているかというと、ロジックの中にマイグレーションのコードが入れたり、新旧両方対応できるように調整していたりします。
// テーブルが存在しない場合は作成 if ok, err := s.IsExistTable(ctx); err != nil { return nil, err } else if !ok { if err := s.CreateTable(ctx); err != nil { return nil, err } }
データのマイグレーションもロジックでデフォルト値を設定して運用しているケースが多いです。
// JSONカラムに特定の属性が存在しない場合にデフォルト値を設定 dict := map[string]interface{}{} err = json.Unmarshal(src.([]byte), &dict) if err != nil { return err } if _, ok := dict["type"]; !ok { a.Type = "image" }
データ集計の課題解決: ETL
全データ走査ができるように日次で各DBファイルのデータを集計してPostgreSQLにコピーするようにしました。 全アプリでDBスキーマは同一であるため、1つのテーブルにアプリの識別子や状態を入れてデータをコピーしています。 これにより、全アプリの設定データに対して柔軟にクエリを打つことができるようになりました。 また、YappliのコンテンツデータはJSONで管理されることが多いのですが、PostgreSQLのJSON関数によりデータ分析もしやすくなりました。
スケールアウト・冗長化の課題解決: マイクロサービス化とAmazon EFS
スケールアウト・冗長化の課題は2つの手段を使って解決しています。
1つはDBアクセスのマイクロサービス化、もう1つはAmazon EFSの導入です。
DBアクセスのマイクロサービス化をすることでDBアクセス以外の処理(以下の図のAPIサーバー)をスケールアウト・冗長化することができます。 今後、SQLite3から別の方式への乗り換えを検討するときに取り外しをしやすくする意図もあります。
2つ目のAmazon EFS導入ですが、これによりファイルコピーの処理をEFSに一任することでコピーのタイミングや失敗時の制御を考慮しなくても良くなりました。 DBアクセスのマイクロサービスにEFSをマウントすることで、このマイクロサービス自体もスケールアウト・冗長化することができました。 詳細は以下の記事を参照ください。
この2つのアプローチによりDBアクセス以外+DBアクセスのスケールアウト・冗長化ができるようになりました。
現行アーキテクチャの課題について
現状、EFSによって読み込みの冗長化はできたものの、書き込みの冗長化ができていません。 原理的にはEFSで書き込みの冗長化ができそうではあるものの、SQLite3のようなファイル読み書きが多い状況でどの程度パフォーマンスが出るかが若干不安が残ります。 実際、EFSによる冗長化を試したときもSQLite3のreadonlyモードやnolockのフラグを入れないと十分なパフォーマンスが出ませんでした。 WriteのリクエストはReadよりも少ないのでリクエストを捌ける可能性はありますが、いずれにせよ銀の弾丸とはならない印象でした。
ETLもスナップショットを含めた全データを移行するにはかなり時間がかかりそうで、ETL自体の管理・運用コストも発生します。 また、日次処理なので即時反映ではなく影響調査のタイムラグが発生します。現状の仕組みだと、ETLをリアルタイムに実現するのも難しそうです。
DBアクセスをマイクロサービス化したことによる開発・運用面の課題もあります。 SQLite3へのあらゆるDB操作をAPIに切り出すため、DB操作の種類(テーブル、単数/複数CRUD、WHERE条件、トランザクション有無)が増えるたびにAPIやパラメータも増えることになります。 APIテストなどの結合テストを行う場合、マイクロサービスを含めた状態でテストをすることになるのでテスト運用も煩雑になりがちです。
まとめ
Yappliにおけるマルチテナントアーキテクチャとその課題についてご紹介しました。
SQLite3の管理・運用やマルチテナントに関する議論は継続的に行われています。 Amazon EFSの導入など技術の進歩によって解消する課題もあり、新しい技術を積極的に取り入れる姿勢も大事になっています。 プロダクト開発チームではYappliにおける運用状況を考慮して、日々アーキテクチャの改善を続けています。
今回紹介したYappliの例がアーキテクチャ検討の参考になれば幸いです!