👍

PlanetScaleで無停止で巨大テーブルのスキーマ更新した

2023/10/19に公開

PlanetScale は MySQL ベースのサーバーレスデータベースです。

https://planetscale.com/

巨大なテーブルのスキーマ更新どうするか困っていたのですが PlanetScale の標準機能でぱぱっと無停止で負荷もなく可能だったのに感動したので記事を書きました。

背景

AI バトラーというサービスを運営しています。

https://ai-battle.alphabrend.com/

キャラクターの能力を自由にテキストで入力し、OpenAI の AI にバトルをジャッジしてもらうというゲームです。

今までにないゲームということもありそこそこ使ってもらえるようになり、現在は 1 日に 3 万~ 4 万回バトルが行われるようになりました。

問題

そこで問題が。バトル結果自体は AI が出力したものを表示するだけなのですが、あとで見返せるように DB に保存しています。つまり、1 日にその量のバトル結果が追加されていくのです…。

リリース当初はそこまでではなかったので困ることはなかったのですが、容量も PlanetScale 上で 20GB 行っていて追加費用がかかっていますし、段々と個人では手に負えない代物になってきました。

負荷が高くなってくる

単に結果を表示するだけではなく、挑まれたバトルの一覧などもあり、ユーザーごと、キャラクターごとに挑まれたり挑んだ履歴を見ることができます。この履歴がかなりおもくなってきはじめており、問題ない人は問題ないのですが問題ある人は履歴ページを表示するのに 40 秒くらいかかってしまうユーザーさんも現れ始めました。

PlanetScale に Insights というページがありクエリの傾向などを見れるのですが、そこでもダントツでユーザーの履歴一覧クエリが重くなっていることがわかってきていました。

インデックスがつけれない

インデックスつければいいじゃん、と思いますよね。僕もそう思うのですが、データ量が増えすぎてちょっと危険になってきました。「あれ、なんか結果が残ってないぞ…」みたいになったりバトルできなくなるとまずいですし。

だいぶ前から気づいてはいたけど忙しすぎて後回しにして結局できないままになっていたので、早くからやっておけばよかった…と後悔の日々でした。

メンテナンス入れればいいじゃん、とも思いますが、周知も難しいしちょっと面倒くさいんですよね…。

PlanetScale では無停止、負荷無しで巨大テーブルのスキーマ変更も可能

それとは別で容量も増えてきたことで費用も増えてきており、PlanetScale の場合は Scaler プランより Scaler Pro プランの方が基本使用料は高いけど追加 DB の費用が安く、Pro にしても変わらない感じになってきたのでプランをアップグレードすることにしました。

そのタイミングで色々何か便利機能はないか調べてみていたのですが、よくよく色々説明を読み返していると PlanetScale のデプロイリクエストという機能があります。GitHub のプルリクエストのようにブランチを切ってあれこれし、マージすることで本番反映するやつです。

その機能自体は知っていたのですが、ちゃんと調べると、なんと無停止、負荷無しで巨大テーブルもスキーマ変更できるとのこと。まじか…!? 思いがけないところで解決策が飛び込んできてびっくりしました。詳しくは割愛しますが下記の記事に仕組みが書かれています。

https://zenn.dev/tak_iwamoto/articles/b27151d22d9e6a

これでできるんならこれで解決するじゃん…! ということで実際にやってみました。

実際にやったこと

Production ブランチへ

PlanetScale には Development ブランチと Production ブランチがあります。Production ブランチはデプロイリクエストをつけることができ、性能的にも高可用性のものです。はずかしながら適当に使っていたのでずっと Development ブランチを使っていました。

そのため今回の下準備としてそれを Production ブランチにきりかえました。念のため事前に問い合わせしたのですが、とくに接続情報なども変わらずそのまま使えるようです。

切り替えは問題なく行えました。ただし、1,2 分 DB に接続できなくなりました。この辺はご注意を。

デプロイリクエストを受け付ける設定にする

Safe migrations という設定があるのでそれを有効にします。これを有効にすることでスキーマの変更はデプロイリクエストのみで行うようになります。SQL を実行して直接変更はできなくなります。

API

PlanetScale には API があります。

https://api-docs.planetscale.com/reference/getting-started-with-planetscale-api

今回はこれで CI に組み込み自動化することにしました。

デプロイリクエストを作る

事前準備として、Prisma を使っていますがマイグレーションは先に自動で実行させるようにしておきます。このときは別途作っておいた派生ブランチに対してマイグレーションを行います。

デプロイリクエスト作成用の API があるのでそれを利用します。

https://api-docs.planetscale.com/reference/create-a-deploy-request

デプロイリクエストをデプロイする

実際にこのリクエストを受理して Production ブランチに反映させます。この辺 API のドキュメントの説明が不足していてよくわからないので色々試しました。

まずは Approve します。この工程は必要か不要か試してないのでもしかしたらしなくても大丈夫かもしれません。下記 API で Approved にします。

https://api-docs.planetscale.com/reference/review-a-deploy-request

どのリクエストを対処するかというのは number を利用します。これはデプロイリクエストを作ったときのレスポンスに含まれますのでそれを利用します。

続いて実際にデプロイします。デプロイの API はこれでした。

https://api-docs.planetscale.com/reference/queue-a-deploy-request

ただし、Approve した直後なのか作った直後なのかはわかりませんが、連続で一気に実行するとまだプロセス中だからできないよ、とエラーになります。そのため僕は 10 秒ほど待ってから実行するようにしています。

基本的にはこれで完了です。デフォルトで自動反映になっていると思いますので放置しておけば Production に反映されます。

CI に組み込む方法の検討

元々 CloudBuild の途中でマイグレーションをやっていました。しかし前述の通りデプロイのコマンドは Queue なのです。つまり即時実行されるわけではありません。ということはシステムのデプロイは完了しているのに DB だけまだ反映されてない、ということがあり得るのです。そのため既存の CI に組み込むのはやめました。

特定のブランチに PUSH された場合に行うようにする

公式のブログに、GitHub Actions で PlanetScale の CLI を使うという記事がありました。

https://planetscale.com/blog/using-the-planetscale-cli-with-github-actions-workflows

ただ記事を見た感じ面倒くさそうだったのと、すでにデプロイリクエストを処理するための js ファイルを作っていたので上記のアイデアだけ参考にすることにしました。

具体的には、db/**というブランチに PUSH された場合のみにデプロイリクエストを実行するというものです。これであれば事前に自分でタイミングをコントロールできるので良さそうかなと思いました。main に PUSH で全部やってくれるわけではなくなるので面倒臭さとのトレードオフではありますが。

こんな感じです。js ファイルはほぼ API のドキュメントが生成してくれるサンプルコードを順次実行しているだけです。(prisma generate はもしかしたら不要かも?)

name: Migration
on:
  push:
    branches:
      - db/**

jobs:
  migration:
    runs-on: ubuntu-latest
    env:
      PSCALE_API_ENDPOINT: ${{ vars.PSCALE_API_ENDPOINT }}
      DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
      PSCALE_API_ID_AND_TOKEN: ${{ secrets.PSCALE_API_ID_AND_TOKEN }}
    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: setup-node
        uses: actions/setup-node@v3
        with:
          node-version: 18.14
          cache: yarn
      - run: yarn
      - run: npx prisma generate
      - run: npx prisma db push
      - run: node migrate.js

合わせて CloudBuild の方のマイグレーション実行部分を削除しました。

ちなみにしばらく運用してみた結果、デプロイリクエストを作るだけで十分なのではという気がしています。というのも、気になって必ずデプロイリクエストが生成されてマージされるまで PlanetScale を眺め続けているからです…。どうしても怖くて気になっちゃう。

実施にやってみたらどうなったか

さて、準備やテストは終わったので実際にやってみました。この時点で該当テーブルには 350 万件のデータが入っていました。すでに PlanetScale のコンソールで直クエリを実行しても1ヶ月分の集計などはタイムアウトでできないレベルになっています。

やったのは削除したいカラムがあったのでそれに向けての事前準備としてデフォルト値の設定、および 4 つのインデックスを複合インデックスに交換する作業です。

実際にやってみたものがこちらです。

30 分程で完了しました。その間試してみましたがバトルは通常通りできますし、履歴表示も問題ありませんでした。

おかげで表示に 40 秒かかっていたページも一瞬で表示されるようになりました。(いろんな検索パターンがあるのでまた色々調整が必要そうではありましたが)

まとめ

とにかく一つずっとネックになっていてこれからもっと酷いことになるであろうと思われていたことがあっという間に簡単に解決できて助かりました。今後も変更し放題なので安心感がすごいです。

PlanetScale ほんと神がかっています、もうやめられる気がしません。

Discussion