大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ

(この内容は、2016/04/09にQiitaに投稿した内容と同じです)

3月24日に発表になったLINEのBOT API Trial Accountが、いよいよ4月7日から実際に試せるようになりました。既に多くのBOTが開発者の手によって作られ始めたようですね。QiitaにもいくつかBOTの作り方が投稿されていますので、”LINE BOT“というキーワードで探してみてください。

実際の作り方の基本は他の投稿に任せるとして、BOT API自体は非常にシンプルな作りなので、試すこと自体はすぐにできると思います。しかし、シンプルな反面、仮に近い将来「Trial」が取れて、友だち50人制限が撤廃された時、それでも正しく安定的に動作するBOTとするには、アーキテクチャ上の工夫が必要になります。個人的に、既にLINE BusinessConnectを導入している企業において、大量にやってくるユーザからのメッセージに対応するために、かなり苦労があったという話を多く聞いています。

BOTにおいて、「ちゃんと何らかの返事が来る」ということが、ユーザにブロックされないための最も大事なことです。そのためにTrial時点の今からでも気をつけておくことを簡単にここで紹介してみたいと思います。

多くの人がやっちゃう間違い

今まで登場したBOTのコードを見てみると、以下のような処理の流れがほとんどでした。

3e4f2e0b-7848-6a8b-440b-9614995092bf.png

ユーザがLINEアプリ上で対象のLINE@アカウントに何かメッセージを発言した後、LINE ServerはBOT Server(=予め登録しておいたCallback URL)にそのメッセージが書かれたJSON文字列を送ります。それを受け取ったBOT Serverは、受け取ったメッセージの内容を解析し、その内容に応じて返信を作り、その返信メッセージの文字列をJSONにして、返信のためのAPI Endpointに送ります。返信処理が終わった後に、200を返しています。あ、ちなみに署名検証処理は省いていますが、本当は必要な処理です。

単純に手っ取り早くBOT側のコードを書こうとすれば、最初はこの処理の流れを思いつくでしょう。しかし、このやり方は、あっという間にいろいろな問題が発生します。個人的には、BOT APIがどんなものかを知るだけならこれで良いのですが、実運用するつもりのBOTを作成するのであれば、絶対に上記のような作り方はやめて欲しいと考えています。

1つのJSONに複数のメッセージが含まれる可能性

LINE ServerからBOT Serverに送信されるJSONの中には、最大100メッセージが含まれます。Trialレベルでは1つのJSONの中に1つのメッセージのみとなりますが、友だち50人制限が撤廃され、数十万、数百万ユーザが友だち登録しているBOTになった場合、ユーザからのメッセージが短時間に集中した状況になると、JSONの中に複数のメッセージが入って送信されてくることになります。

「実際試してみて1通しか来なかったので、1リクエスト1通なんだな」と思って、配列から最初の1件のみ取得するコードを書いてしまうと、ユーザ数の増加&メッセージの増加時に、ユーザからの大切なメッセージを取りこぼす(=無視する)ことになってしまいます。

必ずJSON内に複数のメッセージが含まれることを前提にしたアーキテクチャを設計しておくべきです。

返信処理が全体のパフォーマンスを大幅に悪化させる可能性

先ほど1リクエストに100メッセージが含まれるという話をしました。もしその100通がそれぞれ異なるユーザから送信されたメッセージだとしたら、100ユーザにそれぞれ返信メッセージを送信する必要があります。返信メッセージがそれぞれ異なる内容だとすると、返信メッセージを送信するためのAPIを100回呼び出すことになります。

つまり、上記のシーケンス図の2および3を100回行わなければなりません。「メッセージ解析→返信メッセージ構築→APIコール」を100回です。この中には、DBアクセスやHTTP通信といったI/Oが必ず含まれるでしょうから、1メッセージ返信するために、数百msの時間が必要になるでしょう。仮に200msかかったとすると、100メッセージ分ですので、20秒かかる計算になります。

つまり、LINE Serverからすると、BOT Serverにメッセージを送った後、20秒+αも待たされることになるのです。実際には、20秒も待たずに、LINE Serverは10秒以内にBOT Serverから返事が来なければTimeoutとして切断します。

結果ユーザにブロックされる可能性

LINE ServerはBOT Serverの応答が来た後に、次のメッセージをBOT Serverに送信します。LINE Server側のBOT Serverへ送信する処理の並列度は非公開となっていますが、仮に並列度が1だったとすると、BOT Serverが例えば平均9秒応答するのにかかった場合、LINE Serverから次のメッセージが飛んでくるのは9秒後です。その9秒間の間にも、ユーザからのメッセージは次々とやってきます。しかし、LINE ServerとしてはBOT Serverが応答してくれないと次のメッセージ配送ができないため、LINE Server側にユーザからのメッセージがどんどん蓄積されていきます。

多くのユーザからメッセージがものすごい勢いで送信されている状況では、1リクエストあたり100メッセージが含まれます。つまり、100メッセージ/9秒、がBOT Server側の処理能力で、仮に並列度が2だったとすると、200メッセージ/9秒、秒間約22メッセージとなります。1万人が友だち登録していて、その5割の5000人がほぼ同時にメッセージを送信したとすると、5000メッセージ全てを処理するのに、約227秒=3分以上かかる計算になります。

メッセージを送信してから返事が来るまで3分以上もかかるBOT・・・きっとユーザからすぐにブロックされてしまうことでしょう。

「そんなに集中してメッセージが来ることはないのでは?」と考えるかもしれませんが、もちろんBOTの性質によってメッセージの集中度は違います。しかし、一斉にメッセージをBOTからユーザに送って「先着○○名さまに!」とか、「お得なクーポンどうぞ」とか、集中するキャンペーンは誰でも容易に思いつきますし、いつ何時どこかのネットニュースで取り上げられて○○砲を食らうかわかりません。

上記の仮説を聞いて、「BOT Server側からの返事を遅くすれば、大量にユーザからメッセージが送信されたとしても、一旦LINE Server側で貯めてくれるんでしょ?そうなればBOT Server側は高負荷にならずに落ちないから、その方が都合が良い」と考える人もいるかもしれません。しかし、そういう事態になった時に、分単位で返事を待たされるBOTは、ユーザにとっては「使えないBOT」「機能停止しているBOT」です。BOT Serverは止まっているも同然です。

キーワードは「非同期」

では、どうするか?ですが、戦略は以下になります。

  • BOT Serverは、ユーザからのメッセージをLINE Serverに「できるだけ貯めさせない」ようにする。つまり、とにかくLINE ServerからBOT Serverにメッセージを迅速に移動させる。
  • ユーザは数秒であれば、返事を待ってくれる。返信処理を焦って行わない。

この2点です。

LINE ServerからBOT Serverへのメッセージ移動の高速化

とにかくLINE ServerからBOT Serverにメッセージを早く移動させ、LINE Server側にメッセージが貯まらないようにします。そのためには、LINE Serverから来たリクエストに対して、できるだけ迅速に200を返すようにします。来たリクエストを解釈して返信処理をすることは、絶対に避けるべきことです。

処理フローは以下のようになるでしょう。

1727cdf6-d811-f5cf-5444-bb1b2b71d807.png

LINE Serverからメッセージが来たら、それをさっさとQueue(RabbitMQとか)やメモリDB(memcachedやRedisなど)といった「軽いストレージ」に入れて、後は何もせずにさっさと200を返して、LINE Serverから次のメッセージが来るのを待ちます。こうすることで、目に見えないLINE Server内から、手元で何とでもなるBOT Serverにユーザからのメッセージを迅速に移動させます。

とにかくできるだけ速く、です。極端に言うと、JSON文字列をパースすることすらせずにQueueに突っ込む、そして、署名検証も後回しにする、としても良いくらいです。

例えば、1リクエストあたり100msで処理できれば、100/100msになり、秒間1000メッセージの処理能力になります。仮に並列度が2であれば、秒間2000メッセージとなり、先ほどの例(ユーザからほぼ同時に5000メッセージが送信されたケース)であれば、2.5秒となります。LINE ServerからBOT Serverへのメッセージ移動を、3分以上から3秒以内に短縮した計算になります。

メッセージ返信の並列度をあげる

BOT Server側のQueueにJSON文字列が着々と貯められていくと同時に、それらを着々と消費していきます。ここからはLINE Serverは関係なく、BOT Server側で好きなアーキテクチャを採用することができます。つまり、先ほどLINE ServerのBOT Serverへの送信処理の並列度を1や2といった低い数値で見積もりましたが、BOT Server側はもっと並列度をあげることが可能なはずです。

ユーザからのメッセージに返信する処理のパターンはいくつか考えられますが、例えばJSON文字列をそのままQueueに入れた場合は、以下のフローが考えられます。

  1. JSON文字列をQueueから取り出す。
  2. 署名検証する。
  3. JSON文字列をパースし、個々のメッセージを取り出す。
  4. 受け取ったメッセージごとに、返信メッセージの内容を組み立てる。
  5. ユーザごとに返信メッセージをAPIを叩いて送信する。

まず、1.のQueueから取り出す処理と2.の署名検証処理は、Consumer(上記の図で言うJobWorker)の個数を増やせば増やすほど、並行処理数をあげられます。次の3.では、1つのJSONから複数のメッセージを取り出すわけですが、最大100個のメッセージが含まれる可能性があります。ここでも並列処理の可能性があります。取り出した個々のメッセージを再度Queueに入れることで、次の4.と5.の処理も並列化することができるでしょう。

上記のアーキテクチャであれば、Consumerの個数を増やすという単純なスケールアウト戦略で、大量のメッセージに対応することができるようになるでしょう。

APIの呼び出し回数を減らす

処理能力の向上について最も効果がある改善方法は、I/Oをできるだけ減らすことです。ユーザへのメッセージ返信処理で最も時間がかかる処理、それはおそらくLINE Server側のAPI呼び出しです。そのAPI呼び出しの回数を減らすことができれば、BOT Server全体のパフォーマンスは更に向上するはずです。

まず、以下のことに気がつきましょう。

例えば、比較的単純な仕様のBOTとして「返信メッセージのパターンは5つのみ」だと仮定します。上記の処理フローであれば、JSON文字列をパースして個々のメッセージを取り出し、個々のメッセージから5パターンの中で返信すべきパターンを決定した後に、パターンごとに更にQueueを作っておいて返信対象のMIDをそれぞれ振り分けていきます。そして、そのQueueのConsumerは一定時間ごとにMIDを複数取り出して、それらをtoパラメータに配列で指定し、1リクエストで複数のユーザに同時に返信メッセージを送信します。

d679c123-6bab-0277-8602-7c0d96a63291.png

そして、1ユーザから送信された1つのメッセージに対して、複数のメッセージを同時に返信したい場合、例えば3つのメッセージを3回のAPI呼び出しでそれぞれ行うのではなく、Sending multiple messagesを適用して1回で済ませます。

さらに、上記2つはもちろん組み合わせることが可能です。Sending multiple messagesのtoパラメータも配列指定が可能ですので、これを積極的に使うことで、I/Oをかなり削減できるはずです。

まとめ

LINEのBOT APIの仕様は非常にシンプルです。しかし、多くのユーザからのメッセージ送信が集中した際にそれらをどう捌くかは、事前に上記のような工夫を施しておくことが必要になります。いつ突然BOTが人気になるかわかりません。そして、どんなBOTを作るかにもよりますが、どうせなら多くのユーザが一斉にメッセージを送ってきても耐えられる仕組みにしておくことに超したことはありません。

ここで述べたことを参考にして、ぜひ素敵なBOTを作ってみてください。

このエントリーをはてなブックマークに追加

関連記事

2023年のRemap

Remapにファームウェアビルド機能を追加しました

Google I/O 2023でのウェブ関連のトピック

2022年を振り返って

現在のRemapと今後のRemapについて