Google Cloud Pub/Subを用いたチャット受信の仕組み
※「株式会社YOJO Technologies」から「PharmaX株式会社」へ社名変更いたしました。この記事は社名変更前にリリースしたものになります。
はじめに
こんにちわ!YOJOでエンジニアリングマネージャをしています尾崎(@FooOzaki)です。
YOJOでは薬剤師オペレーションシステムのコア機能として、
LINEを利用した患者様と薬剤師のチャットシステムを内製しています。
本記事はチャット受信部分のアーキテクチャの紹介記事となります。
メッセージの受信漏れを防ぐために、Google Cloud Pub/SubというGCPサービスを利用しているところがポイントです。
Google Cloud Pub/Subは、Pub/Subメッセージングモデルのメッセージング機構+メッセージキューイング機構を簡単に導入できちゃう素晴らしいサービスなので、大量メッセージや大量イベントデータを捌く際のアーキテクチャとして参考にしていただければと思います。
また、必ず処理しないといけないWebHook受信処理のアーキテクチャとしても参考になる内容になっているかと思いますので是非読んでみてください!
旧システムの構成と問題点
最初にあえて2年ほど前の旧システム構成図を出してみるのですがどこが問題になりそうでしょうか?
ユーザが送信したLINEメッセージ(WebHook)をRailsアプリケーションで受け取り、処理を実施するというシンプルな構成です。
WebHook受信や受信後処理に失敗した場合にリトライできない
WebHook受信サーバダウンタイム中はメッセージを全て取りこぼす
WebHook受信サーバのスケールがメッセージの流入量増加に間に合わなければメッセージを取りこぼす
などが当時上がった問題です。
WebHookの受信・後続処理に失敗するとユーザーのメッセージを取りこぼし、無視してしまうことに繋がるためユーザー体験を著しく損なってしまいます。
解決策ーGoogle Cloud Pub/Subの導入
そこでYOJOは記事冒頭でもご紹介したGoogle Cloud Pub/Subを導入しました。
※2022年に入ってLINEがWebHook再送の仕組みを提供してくれたので本記事のアーキテクチャ構成を取らなくとも一部解決可能になりました
全体像
Google Cloud Pub/SubはPub/Subメッセージングモデルというメッセージを送受信するための仕組みを提供してくれるマネージドサービスで、メッセージキューもフルマネージドで提供してくれます。
概要は公式ドキュメントのCloudPubSubガイドが丁寧に記載されており、非常にわかりやすいです。
Publisher・Cloud Pub/Sub・Subscriberの3サービスが必要なサービスとなりますが、それぞれのざっくりとした流れは以下の通りです。
①PublisherがCloud Pub/Subにメッセージを送信します。
②Cloud Pub/SubはTopicと呼ばれるメッセージ受信機構で届いたメッセージを保存します。(メッセージキューイング)
③Cloud Pub/SubはSubscriptionとよばれるメッセージ送信管理機構で、
④Subscriberにメッセージを送信します。
⑤Subscriberからメッセージが正しく送られたかどうかをSubscriptionは受け取り(Ack [Acknowledgeの略])、メッセージを再送したりデッドレターキューに入れたり良い感じでハンドリングしてくれます
②③はGoogle Cloud Pub/Sub側がいい感じにやってくれる部分です。④⑤もGoogle Cloud Pub/Subがハンドリングしてくれる部分です。
だいたいの用語と流れを押さえてもらえれば、このあとYOJOの構成と合わせて詳細に説明していきます。
読んでいてわからなくなればこの図へ立ち返ってきてもらえると良いかと思います。
コードサンプルもGCPさんが用意してくれてるので具体のコードも見てもらえるとよりイメージつくかと思います
https://cloud.google.com/pubsub/docs/samples
①PublisherとPub/Sub Topic
YOJOではPublisherに相当するサーバはExpress+Node.jsコンテナで動くCloud Runサーバで運用しています。
※言語選定に大きなこだわりや理由はなく、Railsはファットすぎるので他のものを選択した程度です
役割は、
LINEのWebHookを受け取りロギング
Google Cloud Pub/Sub Topicのエンドポイントへそのままメッセージ送信
のみで処理的には1ファイルのjsスクリプトでも完結する程度の本当にシンプルなアプリケーションです。
※Google Cloud Pub/Sub Topicとはメッセージの宛先に相当するもので、Publisher(メッセージ送信元)からの送信先Pub/Subエンドポイントだと思ってもらえれば大丈夫です。
このPublisherが確実にWebHookを受信する仕組みの第一歩目です。
少なくとも「受信する=メッセージキューにメッセージを格納する」ことはPublisherサーバが落ちなければ保証することができるようになりました。
Cloud Runを使用し、リクエスト数に応じてスケール可能にしている点もポイントだったりしますが、今回はCloud Runの説明は割愛します。
②③フルマネージドメッセージキュー
続いて、Cloud Pub/SubのTopicで受け取ったMessageは自動でメッセージキューへエンキューされます。
Cloud Pub/Subはメッセージキュー管理もフルマネージドでCloud Pub/Subの機能として提供してくれています。
デキュー、エンキュー、デッドレターキューなどの管理は実装不要で後続のGoogle Cloud Pub/SubのSubscriptionが良い感じにハンドリングしてくれます。
楽ちんですね!
AWSのSNS、他のOSSなどPub/Subメッセージング機能を有するミドルウェアやマネージドサービスは他にもありますが、当時Google Cloud Pub/Subを採用した理由はメッセージキューイングの処理も1サービスでお任せしちゃえるからです。
旧システムでは一度失敗したWebHook処理を二度と救うことができませんでしたが、Cloud Pub/Subでメッセージキューイングすることにより、
後続のSubscriberサーバが落ちたとしてもメッセージキューに溜まったメッセージを後から確実に処理することができるようになりました!
⑤Pub/Sub SubscriptionとSubscriber
続いてSubscriberと呼ばれるメッセージの受け手・処理側です。
Cloud Pub/SubではでPull型とPush型という2種類の受け取り方が選択できます。
今回は細かい説明は記事が長くなってきてしまったため割愛しますが、こちらも公式ドキュメントがすごくわかりやすいです。
https://cloud.google.com/pubsub/docs/subscriber
YOJOではSubscriberクライアントのワーカープロセス数・サーバ数で受信量を調整できるようにPull型を選択しています。
SubscriberにRubyを採用した理由は
旧システム時代のWebHook受信後のRailsアプリケーション処理をそのまま移植し、呼び出したかったためです。
正常に処理が完了した場合はPub/Subへ受信完了を通知。
Exceptionはキャッチし、ログ出力&Pub/Subへメッセージ受信失敗を通知します(Acks)。
受信失敗したメッセージは再度キューに戻されリトライ処理が可能です。
旧システムではWebHook受信後のアプリケーション処理で失敗した場合にリトライする機構がありませんでしたが、Pub/SubのメッセージキューとAck機構によりリトライが可能となりました。
旧システムではWebHook受信サーバのオートスケールがメッセージの流入量増加に間に合わなければメッセージを取りこぼす状態でしたが、
Pull型のSubscriberによりメッセージキューに滞留したメッセージが増えた段階でスケールすれば、多少の遅延は発生するもののメッセージの取りこぼしを無くすことが可能になりました。
補足: at least once方式での配信
最後に実際使う上での注意点や導入時に考慮した点なども記載しておきます。
Google Cloud Pub/Subはat least once 方式で配信をおこないます。
最低1回の配信は保証にするけれど、重複して配信する可能性があるという配信方式です。
YOJOではmessage_idというメッセージの一意なキーを利用し、重複配信を予防しています。
Subscriberがメッセージを受け取ったタイミングでメッセージキーをRedisに保存し、重複配信を防ぐ仕組み実装しています。
対応は処理の特性に応じてだと思いますが、冪等性が担保されていれば大丈夫でしょう。
2022年6月時点ではプレビュー版(俗にいうベータ版的な扱いです)として一度限りの配信が可能になるオプションが登場しており、これから利用する方はオプションの有効化を検討していただけると良いと思います。
Google Cloud Pub/Sub 配信オプション-exactly-once-delivery
まとめ
Google Cloud Pub/Subを利用したメッセージキューイングをすることにより、チャットメッセージの取りこぼしを防ぐアーキテクチャを実現できました。
文章で書くとすごく複雑なのですがアプリケーションソースコード的にいくと本記事の文字量より圧倒的に少ないです!
細かいエラーハンドリングやロギング処理など本番運用では考慮は必要でしょうが、PublisherとSubscriberの2スクリプトを動かすサーバさえ用意すれば本アーキテクチャは簡単に真似できるので参考にしていただければと思います!
最後に、YOJOではこのように患者さんの体験をより良くするアーキテクチャ設計を共に考え、実現していくエンジニアの仲間を絶賛募集中です!
ご応募お待ちしてます!
この記事が気に入ったらサポートをしてみませんか?