こんにちは。GMOリザーブプラスの馬場です。
突然ですが、呼び出しディスプレイという機能をご存じでしょうか?機能名は知らなくても、以下を見ればどういうものかはなんとなくわかるのではないかと思います。
要するに、クリニックにおいて番号呼び出しをする際に使うリアルタイムな待ち行列表示システムですが、メディカル革命においては、これはWebsocket を使って実装しています。Websocket についてはすでにご存じの方も多いと思いますが、とても雑に表現するのであれば、双方向通信を可能にするプロトコルで、例えば診察室から次の番号を呼び出す操作をしたら、それが待合室のディスプレイに即座に反映され、同時に「〇番の方、診察室にお入りください」などと音声にて患者が呼び出しされるわけです。
このようなリアルタイム通信をPC のブラウザ間で実装するための1つの方法として、弊社ではこれまでNode.js で実装したWebsocket 専用サーバーを用意していました。
ただし、上記画像を見れば一目瞭然ですが、これには可用性と負荷分散という大きな課題がありました。通常、このようなWebsocket 機構は、通常のWeb サーバーとは異なりステートレスではないため、可用性や負荷分散性能を確保するために、単純にサーバー数を増やしてLB を置けばいいというものではなく、複数台の仮想マシンの間でデータをどのように共有するかが問題になってきます。しかも、双方向のリアルタイム通信が必要という特徴から、例えばA とB のWebsocket サーバーを用意した場合、A サーバーに送信されたデータが、B サーバーにも即座に同期されなければ、A とB に接続したユーザー間でデータの不整合が発生します。つまり、ある呼び出しディスプレイと別の呼び出しディスプレイで、表示している番号が異なる可能性が出てきます。
そこで、最近弊社では、AWS のサーバーレスなシステムを活用し、可用性や負荷分散性能、コストにも優れたWebsocket のシステムを構築し、導入を開始し始めております。本記事では、そのアーキテクチャと、アーキテクチャ選定に至るまでの流れをご紹介します。
いわゆる典型的なAWS サーバレス構成であるところの、Lambda, API Gateway, DynamoDB を活用しています。その他にも、証明書管理にはAWS Certificate Manager、DNS はRoute53、監視にはCloudwatch/Cloudwatch Logs を活用しています。
典型的過ぎて同じような構成がAWS 公式のチュートリアルにも登場しているため、特にここについては目新しいものはないと思いますが、これまで単一のNode.js でやっていたWebsocket セッション管理とロジック、データ保存が、それぞれAPI Gateway, Lambda, DynamoDB に分散されて担当することになっているため、ある意味マイクロサービス的な形になっています。それゆえ構成としては複雑になっているのですが、実際のところこの構成は可用性などの非機能要件をプラットフォーム側に押し付けることができているので、このレベルの可用性やパフォーマンスを実現しようと思った時には、かなりシンプルな構成だと思います。
重要なのはここからで、この構成にいきつくまでに、当然いろいろな構成の選択肢が出てきます。もちろん、この構成が必ずしも正解とは言えないのですが、ここまでに至るまでの道筋こそが重要だと思いますので、本記事ではこちらをメインにご紹介します。
まずは現状の課題の洗い出しから始めます(さらっと書きましたが実はここが一番重要です)。
念のため用意しているコールドスタンバイ用の仮想マシンのコストも発生している
ざっくりとこんな感じでした。そこで、冗長化ができれば、少なくともコスト以外は解決ができるはずだと考えます。
が、そもそもとして、ブラウザ上でのリアルタイム通信にはWebsocket 以外にもいくつか方法があります。例えばSSE (Server-Sent Events) やWebRTC などです。SSE は昔からありますが、最近はChatGPT のレスポンスなどに利用されていることから再び注目を浴びていますが(回答がだんだんとブラウザ上に表示されるアレです)、今回はWebsocket のままで進める判断をしています。理由は、既存のアプリへの改修を最小限に抑えたかったからです。理想的には、裏側の接続先を新しいアーキテクチャで実装したものに変更するだけでよい世界を目指しました(が、実際には多少のアプリ改修が入っています)。
ということで、Websocket を前提としたときの冗長化のパターンとして、最も有名なのは以下のPub/Sub 構成かと思います。
要するに、Websocket サーバー間のデータの同期をリアルタイムに行うために、Redis のようなPub/Sub の機能を持つ仮想マシンを用意し、Publisher のWebsocket サーバーから送信したメッセージを、Subscriber の他のWebsocket サーバーにブロードキャスト送信する、メッセージ同期の仲介役を別途用意するイメージです。今回のアーキテクチャ選定でも、実は最初はこの構成にしようか迷いました。LB やRedis を追加で用意することができれば、既存の構成にそこまで手を入れなくてよいのが大きなメリットです。
しかしながら、この構成では、当然Redis の可用性を考えたり、Redis やLB のコストがかかったり、スケーラビリティを考慮したときのRedis の運用がなかなか辛いという大きなデメリットがあります。エンジニアリソースも限られている中、今現在単純な構成である呼び出しディスプレイのために、Redis のクラスタリングなどを考える余裕はありません。
そこで、もう1つの選択肢、つまりサーバレスでの実装を考えました。この構成のメリットは、デフォルトでリージョン内のAZ 間で分散されるため、可用性を考慮しなくてもよく、サーバレスなのでスケーラビリティに優れており、運用性のメリットが非常に大きいという点です。また、コスト面においても、使った分だけ課金というのは呼び出しディスプレイと相性が非常に良かったためです。通常、夜中も稼働するクリニックは非常に少ないため、呼び出しディスプレイの稼働時間も必然的に大部分が日中の限られた時間となります。円安ドル高でAWS のコストが増加しているとはいえ、トラフィックがない間も仮想マシンを実行し続けるよりははるかに安いと想定されました。
なお、AWS を選定した理由は、既にある程度ナレッジがあり、十分なドキュメント類のリソースも提供されていて、弊社としては最も始めやすかったためです。
コスト面に関しては、社内でのGo サインをもらうために、少しばかり丁寧に見積もりました。サーバレスでクラウドなので、月額の正確な見積もり金額を出すこと自体ナンセンスな気がしますが、ある程度コスト効果が示せるとプロジェクトを進めやすくなります。
そこで、AWS が提供する見積もりツールなどを使いながら、いろいろと決め打ちで概算を出しました。1日当たりの患者数を大体100 人、1日のクリニックの稼働時間を12時間、30日稼働と想定し、クリニックで呼び出しディスプレイを使うデバイスやメッセージ数などなど、ある程度多めに決め打ちで推定すると、実際の課金金額は、今のNode.js サーバーを常に稼働させる構成よりも半分以下になることが分かりました。
冗長化やパフォーマンスの課題も解決、おまけにコストも安くなるのであれば選定しない理由はありません。こうして、API Gateway, Lambda, DynamoDB を使ったサーバレス構成で構築しようか、という流れになりました。ちなみに、マルチリージョンの可用性は、段階的に進めるために現時点では不要という判断をしています。
次にやることは実現性の確認です。これまでサーバレス構成で本番環境で稼働するアプリケーションを作った経験が弊社ではなかったため、この構成を提案した以上、本当にできることを示さなければなりません。
実現性確認の段階では、本番環境では影響が出ない範囲でアプリケーション側の改修をしながら、検証用のAWS 環境を用意し、そこでサーバレス構成の呼び出しディスプレイのバックエンドを構築しました。構築やアップデート、リソースの削除、本番環境での展開を楽にするために、デプロイはAWS SAM を使いました。
ここで1つ、厄介な問題が出てきます。それがWebsocket API のドメインです。
API Gateway はカスタムドメインをサポートしています。もちろん、Websocket タイプもカスタムドメインが使えます。厄介と表現したのは、AWS 側の制限というよりは、クリニックのネットワークの特殊性によるものです。サカモトに聞く!その2 独特すぎるクリニック内ネットワークの話でもお伝えしていますが、クリニックの呼び出しディスプレイの「呼び出す側の」端末は、診察室の中、しかも電子カルテネットワークに繋がっていることがあります(電子カルテを見る端末と同居して予約システムを使うパターンがあります)。そのようなネットワークが制限されている環境に対して、迂闊に新しいドメインを追加すると、既存のネットワーク構成を変更する必要が出てきます。
既存のWebsocket 通信で使っているドメインにそのまま差し替えられればいいのですが、既存影響を考えるとそうもいかないため、なるべく影響を最小限にとどめるように工夫します。例えば、専用で新しいドメインを切るのではなく、既存のドメインのサブドメインという形で定義し (そのサブドメインがワイルドカードのホワイトリストに入っていれば問題なし)、移行スケジュールも、一気に移行するのではなく、段階的に移行することによって影響が起きたときのリスクを最小限に抑えるという、ある意味技術以外のところでの工夫でも乗り切ることにしました。
本記事では、メディカル革命の1つの機能である呼び出しディスプレイをサーバレス構成で実装するまでの話をお伝えしました。現時点では、実際にカスタムドメインでのテストが完了し、呼び出しディスプレイの動作も問題ないことが分かったので、今後は小規模ユーザーから導入を段階的に進めていく、という段階にあります。本番環境での導入においては、例えば見積もり上では問題なかったけれども実運用でコストがかかりすぎたり、安定性など新たな問題が出てくるかもしれませんが、その後の展開がどうなるか成功/失敗に関わらず、またブログでお知らせできればと思います。