はじめに
はじめまして、バックエンドエンジニアのぽこひで (@pokohide) です。
最近の日課はゲーム実況者「兄者弟者」の「DYING LIGHT 2 STAY HUMAN」と「エルデンリング」を見る事です。
本記事ではタイミーで長年使われていた、マイクロサービスとして切り出されたチャットサーバ(以降、旧チャットサーバと呼びます)をタイミーの中核を担うモノリシックなRuby on Railsサービス(以降、タイミー本体と呼びます)に移行した話です。
今回は移行した経緯、気にした点などを紹介します。
対象にしている読者は以下の方々です。
- レガシーなシステムと向き合っている人
- 無人化システム ※1 に疲弊している人
※1 : 無人化システムとは この記事 に出てくる造語で「誰も詳細は知らないが、なぜか動いているシステム」を意味する
チャット機能とは
タイミーは「働きたい時間」と「働いてほしい時間」をマッチングするスキマバイトサービスです。マッチングすると事業者(クライアント)と働き手(ワーカー)はチャットを通じてやりとりができます。
タイミーリリース当初から提供されており日常的に使われている機能ですが、マッチング後に働くまでの流れといったテンプレートメッセージを事業者が手動で送信していたり、画像等のファイルを送受信不可能だったりと色々な課題も抱えてもいました。
この機能の改善はマッチングから働くまでの無駄な時間の削減に繋がり価値が高いため、弊社ではコアドメインと考えています。
旧チャットサーバとは
旧チャットサーバはGo言語で書かれておりアカウントのAuth、プロフィール情報の保存、メッセージの送受信、既読の有無といった機能を有するサービスです。
以下の図は、アプリ上でメッセージタブを開いた時の流れを簡略化したものです。
旧チャットサーバの持つアカウントはタイミー本体のアカウント情報をハッシュ化したものを利用しているため、タイミー本体は旧チャットサーバのアカウントやルームを取得できるが逆は行えないといった欠点があります。
そのため、タイミー本体からマッチング中の募集を取得してそれに対応するルームを取得するといった流れになっています。この際、GETなのにDBにWriteが走る可能性があったり悲しい事にもなっていました。
以下の図は、ルームを選択してメッセージ投稿までを簡略化したものです。
旧チャットサーバでは受信者へのプッシュ通知やメール送信は行なっていないため、メッセージ登録時にタイミー本体にリクエストを行なっています。
他にもプロフィール更新はタイミー本体を介して旧チャットサーバにリクエストを送るため、同一アカウントに対して複数の経路からAuthが実行される事で認証の奪い合いになるといった事象も発生していました。
なぜやるのか
端的に言うとコアドメインにも関わらず継続的改善ができない状況だったからです。
上述したものも含めツラミをまとめたのがこちら
- クライアント、バックエンドどちらからも認証を行うため認証の奪い合いが起こる
- アカウント情報がハッシュ値のため特定が難しく分析が困難
- チャット → マッチングが疎結合で体験的改善が走らない
- 障害やエラーが発生しても対応が困難
- 単一機能を提供することを前提とした設計のため追加の開発が困難
色々なツラミを抱えていましたが、今回は次のような目的と制約を決めました。
目的 - チャットを**継続的改善**と**タイミーとのシームレスな連携**が可能な状態にすること - チャット情報を**分析可能な状態**にすること 制約 - 旧チャットサーバに**一切手を加えない**
やること・やらないこと
移行の流れを決める前に今回の目的と制約の中でやる事・やらない事を決めました。
やること
- 将来を見据えた設計で旧チャットサーバの機能を踏襲したチャットの実装
- 今までと変わらないメッセージ体験の提供
- ルームやメッセージなどは同じ機能を持つ
- 古いバージョンのアプリをサポート
やらないこと
- メッセージの既読情報の同期
- 旧チャットサーバ側のRDBに保存される過去データの移行
- 厳密な同期処理の実現
旧チャットサーバはメッセージ作成時にタイミー本体にリクエストを送っていますが、中身はメッセージ本文とアカウント情報のみで既読などの情報は含んでいません。旧チャットサーバに手を加えない制約のため、無理な同期はせずこれはやらないこととしました。
他にも、タイミーでは報酬確定後24時間でルームが閉じます。このため、見えないデータである過去データの移行は行わない事に決めました。
タイミーにとってのチャットは重要な機能ではありますが、SNSやメッセージアプリに比べて高精度な同期性を求められるほど頻繁に連絡が行われているわけではありません。早く届くことよりも確実に届けられることが重要です。そこで、高精度な同期はベストエフォートとしつつ結果整合性を重要視しました。
移行計画の検討
「マイクロサービスとして新規開発する」「メッセージ関連の外部SDKを導入する」といった方法も考えられましたが、タイミー本体とのシームレスな連携をするためにチャットをタイミー本体に移行することを選択しました。
また、チャットはWebブラウザからもアプリからもアクセスされるサービスです。アプリにはバージョンの概念があるため移行に向けて必ずアップデートの浸透待ちを必要とします。
そこで今回は、タイミー本体で新しくチャットAPI(以降、新チャットサーバと呼ぶ)を実装して、Webブラウザとアプリの特定バージョン以降はタイミー本体を利用させつつ、アプリの推奨・強制アップデートを活用して旧チャットサーバを利用するバージョンを徐々に減らしていく手法を考えました。
その間、新旧チャットサーバを行き来することが考えられるのでデータ同期が必要となります。ここで既存のチャットサーバからのWebhookや解放されているAPIを利用します。
実際には旧チャットサーバのAuthは不安定なところがあるため、新→旧の同期処理は非同期で行われています。正しくデータが同期される事を保証したいのですがリトライ上限に達して非同期ジョブが終了してしまう可能性もあります。それを考慮して上限に達した場合はログを残し、それを検知可能にする事で見つけ次第、泥臭く対応する事にしました。
お気づきの方もいるかもしれませんがこのデータ同期は完璧ではありません。なぜなら、旧チャットサーバはメッセージの登録を契機にWebhookリクエストを行います。そのため、悲しい事に新 → 旧で同期を行うと旧 → 新と全く同じメッセージが返ってきてしまいます。
さらに上述の通り、このWebhookリクエストの中身はメッセージ本文とアカウント情報のみです。同じアカウントが全く同じ内容のメッセージを送信した場合、それらが違う事を識別できません。
そこで新チャットサーバ側で、5分以内に全く同じ内容のメッセージがある場合は何もしないといった処理を加えました。これにより
- 新 → 旧 → 新のメッセージ投稿のループに対応できる
- 同じアカウントから同じ内容のメッセージは識別できないのであえて識別せず、5分以内であれば1つのメッセージとして扱う
ようにしました。これは今回の移行に際して諦めたことです。
最終的にはアップデート浸透待ち期間を経て、アプリから旧チャットサーバへのアクセスがなくなったことを確認し、データ同期処理をやめて完全に新チャットサーバのみで稼働させることで移行を完了させるといった計画を立てました。
移行計画まとめ
- タイミー本体で新しくチャットAPI(新チャットサーバ)を実装する
- アプリで新チャットサーバを利用する
- 推奨・強制アップデートを利用して旧チャットサーバから新チャットサーバに徐々に移行する
- 旧チャットサーバへのリクエストがなくなったことを確認したのち、データ同期処理を止める
以下は余談です。
今思えば、アプリのバイナリ単位で向き先を変えるのではなく、ストラングラーパターン ※2 を用いて段階的に向き先を変えることで制御可能でロールバックも容易になり、より安全な移行ができたなと思いました。
※2 : 既存のアプリケーションと新しいアプリケーションを振り分ける「ストラングラーファサード」というレイヤーを設けて機能の特定部分を新しいアプリケーションに徐々に置き換えながら移行する手法
結果
以下のような時系列で特に大きな障害なく旧チャットサーバと完全にお別れができました。
- 2020年12月
- 2021年3月
- 2021年4月 ~ 2021年11月
- 2022年1月
推奨・強制アップデートのフロー構築も同時に準備していたためアップデート浸透待ちに半年以上を要した結果となりました。
タイミーとのシームレスな連携も可能となり「チャットを通じたお問合せ」「複数人のチャット」「システム的なメッセージ」「メディアのやりとり」といった将来を見据えた拡張性の余地も持たせることができました。
また、副次的な効果として全エラーの約3割を占めていた旧チャットサーバとのAuth時の401エラーも0件になりSentryのノイズも減りました。
最後に
今回はコアドメインなのに継続的改善が行えない状況をタイミー本体に移行する事で解決しました。リスクの伴う決断でしたが、継続的な体験改善が行えるようになったのでより使いやすいアプリ開発を頑張っていきます。
技術的負債の解消や継続的な体験改善に興味がある方は是非、声をかけてください!
※ まだ自身のMeetyを作成していないので同僚のページを載せています