2023 · Nudge Healthcare

毎分1,500件のリクエストを受けるサービスのリファクタリング

背景

先のマイグレーションで時間を稼いだ直後から、次の作業の準備に入りました。まず、アプリで ID を整数として扱っていたのを String に変え、そのリリースがユーザーの99%に行き渡るまで待ちました。ちょうど運営チームから、ランダムボックスの確率調整機能がわかりにくいのでリニューアルしてほしいという要望があり、代表の承認も得られたので、ようやくリファクタリングに着手できました。ボックスサービスの /luckybox エンドポイントは、当時毎分1,500件前後のリクエストを受けていました。

ボックスをどこに保存するか

まず、ボックスをどこに保存するかを考えました。以前の設計ではボックスをすべて MySQL に保存していたため、ボックスを作るたびに INSERT が、開けるたびに UPDATE が発生していました。これを DynamoDB に移し、パーティションキーで読み書きする速度が速いという長所を、最大限に活かすことにしました。

1つのパーティションキーに、ボックスを配列で

ボックスができたら配列に値を追加し、ボックスを開けたらその項目だけを直せば済みました。見た目は MySQL と変わりませんが、1つのパーティションキーで照会・更新するので、はるかに速くなりました。また、ユーザーがその日初めてアクセスしたときに溜まっていたデータを消すようにして、以前のように値が際限なく増え続けることも防ぎました。

新たにできたポリシーで、1人のユーザーが開けられるボックスは最大20個までという制約もありました。この制約もボックスの length を数えるだけですっきり解決でき、さらに効率的でした。

ランキングと設定はどこに置くか

ランキングシステムも新しく作りました。Redis の ZSET は重複のないキーをスコア順に並べておき、順位の読み取りと更新を O(log n) の時間計算量で行えます。さらに、高額当選者が一定期間ふたたび当選できないようにする必要があるという要件もありましたが、当選したユーザーの ID に TTL をかけて Redis に保存しておけば、データベースで当選記録を照会しなくても簡単に解決できました。

当選確率のような設定値は、MySQL に残しました。管理者コンソールとの連携を考慮する必要があったからです。ボックスを開けるたびに MySQL を一度読まなければならないという短所はありますが、設定をリアルタイムに反映でき、読み取りだけなので大きな負担にはならないと判断しました。

ボックスを開けるときに、3つのストアを回る順序

結果

完成したシステムは、少しずつトラフィックを受けていきました。朝は10%ほどだけ受けながら様子を見て、問題がなかったので午後に100%まで上げました。MySQL の負荷が下がり、何よりランキングに活気が出ました。6時間に一度更新されていた値が、リアルタイムで動くようになったからです。運営チームも、当選確率を設定しやすくなったと満足していました。

問題を見つけてチームリーダーに報告した日から最後のリファクタリングまで、まる1年かかりました。自分の書いたコードが少しずつトラフィックを受け、100%までリリースされていく様子を見守った経験は、今でも忘れられない開発の楽しさです。