はじめまして、京都大学大学院情報学研究科修士1年の江本洸海です。普段は離散アルゴリズムの研究を行ったり、趣味として競技プログラミングを嗜んだりしています。今回、開発4センター/Official Account開発室のOA Dev 2チーム所属として技術職就業型コースのインターンシップに参加しました。本レポートではインターンシップで取り組んだ問題について説明します。
取り組んだ問題
配属されたOfficial Account開発室では主にLINE公式アカウントの開発を行っています。LINE公式アカウントにはLINEユーザーのアクションに反応してメッセージを自動送信する機能がありますが、依存システムの障害などによって送信に失敗するという問題がありました。この問題を解決するため、メッセージを再送信する仕組みを実装しました。以下で詳しく説明していきます。
LINE公式アカウントとは
LINE公式アカウントは、企業や店舗などがLINEユーザーとコミュニケーションをとれるサービスです。キャンペーン情報の提供や予約受付などさまざまな機能を有しており、みなさんもLINEユーザーとして飲食店やアパレルショップなどのLINE公式アカウントを利用したことがあると思います。2022年7月時点、LINE公式アカウントのアクティブアカウント数1は約39万アカウント存在しており、非常に規模の大きなサービスとなっています。
自動送信されるメッセージ
LINE公式アカウントの機能の一つとして、LINEユーザーがLINE公式アカウントから自動でメッセージを受信することがあります。たとえばみなさんがLINE公式アカウントを友だち追加したときに、しばしば「友だち追加ありがとうございます」などのメッセージを自動で受信すると思います。これはあいさつメッセージ(画像左)と呼ばれ、LINEユーザーの友だち追加というアクションに反応して送信されています。ほかには以下のようなメッセージが自動で送信されます。
- LINEユーザーがメッセージを送信したとき、事前に設定しておいたメッセージをそのユーザーに対して送信する応答メッセージ(画像中)
- LINEユーザーからのLINEコールに応答できなかったとき、そのユーザーに対して送信する不在時メッセージ(画像右)
- LINE Beaconで検出したLINEユーザーに対して送信するメッセージ
以下の図を用いて、あいさつメッセージを送信する内部的な動きを説明します。LINEユーザーが友だち追加のアクションを起こすと、OAサーバー2はLINEプラットフォームからフォローイベントを受信します。フォローイベントはWebhookイベント3の一種であり、どのLINEユーザーが友だち追加したかなどの情報を含んでいます。OAサーバーは受信したフォローイベントなどを見て、LINEユーザーにあいさつメッセージを送信するか否か、どのようなメッセージを送信するかを決定します。ここではメッセージ "Hello" を送信するように決定しています。メッセージの送信にはMessaging API4を用いています。Messaging APIの一部は一般向けに公開されており、OAサーバーに似たボットサーバーをみなさんも構築できます。
送信に関する問題
Messaging APIを用いたメッセージの送信ではリクエストに失敗する可能性があります。対応としてメッセージの再送信が考えられますが、単純な再送信ではメッセージが重複する恐れがあります。なぜならLINEユーザーに正しくメッセージが届いたときでも、サーバー側が送信した直後にネットワーク障害が発生したり、クライアント側が処理を打ち切ったりすることによって、見かけ上APIの呼び出しに失敗する場合があるからです。
以下のシーケンス図を用いて、実際にメッセージを再送信する様子を説明します。まずは重複なくメッセージを再送信できた例です。フォローイベントを受信したOAサーバーはMessaging APIを用いてメッセージ "Hello" を送信します。エラーが発生すると、OAサーバーは再びメッセージ "Hello" を送信します。これによってLINEユーザーにメッセージ "Hello" が届けば、再送信は効果を発揮していると言えます。
一方、次の図はメッセージの重複が発生した例です。OAサーバーの送信したメッセージ "Hello" はLINEユーザーに届いたにもかかわらず、エラーが発生しています。このときOAサーバーが同じ内容で再送信を行い、そのリクエストも成功してしまうと、メッセージの重複が発生してしまいます。
このようにメッセージの再送信には重複の問題がありますが、メッセージ送信の実装当時は後述するリトライキーの仕組みがなかったなどの理由により、OAサーバーではメッセージを再送信しない設計を選んでいました。しかしこのままではLINEユーザーに正しくメッセージが届かない恐れがあります。今回のインターンシップではこの問題に取り組み、重複のない安全なメッセージの再送信を目指しました。
実装した再送信の仕組み
Messaging APIにはメッセージの重複を防ぐリトライキーという仕組みがあります。これを用いて再送信の仕組みを実装しました。なお一般向けのMessaging APIでも同じ仕組みを実現できます。以下で詳しく説明していきます。
メッセージの重複を防ぐリトライキー
Messaging APIの送信リクエストにはリトライキーを含めることができます。これはメッセージの重複を防ぐもので、同じリトライキーを含む送信リクエストは一度しか処理されないような保証がされています。この機能を用いれば、再送信によってメッセージが重複する問題は防げます。送信したいメッセージに対してリトライキーを一つ定め、リクエストヘッダーのX-Line-Retry-Keyフィールドで指定すればよいです。
リトライキーを用いる様子を以下のシーケンス図に示します。フォローイベントを受信したOAサーバーは、そのイベントに対してリトライキーを一つ定めます(ここでは "123e4567-e89b-12d3-a456-426614174000")。そしてメッセージ "Hello" を送信するときにはリクエストヘッダーにそのリトライキーを指定します。エラーが発生すると、OAサーバーは再びメッセージ "Hello" を送信しますが、先ほどと同じリトライキーを指定します。
次の例でリトライキーの効果は発揮されます。メッセージ "Hello" はLINEユーザーに届いたにもかかわらず、エラーが発生しています。このときOAサーバーは同じ内容で再送信を行いますが、先ほどと同じリトライキーを指定しているため、同じメッセージが再度届くことはありません。リトライキーによって重複を防いでいます。
リトライキーの生成
送信リクエストに対するリトライキーは一意である必要があります。そのためメッセージを安全に送信するためにはリトライキーの生成を工夫する必要があります。ここで、自動送信されるメッセージには対応するWebhookイベントが存在することに注目し、今回はWebhookイベントID5をリトライキーとして用いました。
ただしリトライキーは十六進法で表記されたUUIDでなければならない一方、WebhookイベントIDはULIDの形式をとっているため、ULIDをUUIDに変換する必要があります。今回はSulky ULIDによって以下のように変換し、リトライキーを生成しました。なおULIDは128ビットの数値であり、UUIDと互換性をもちます。
再送信の手法
今回再送信の手法として、再送信すべきエラーが発生した直後、同一スレッドで再送信を試みる単純なものを採用しました。実装のバリエーションはさまざま考えられますが、特に再送信の回数制限や時間間隔を適当に設定する必要があります。時間間隔については指数関数的に長くするエクスポネンシャルバックオフが推奨されています。
ただしこの手法は、再送信を試みる間、実行中のスレッドをブロックしてしまい、後続のイベントを処理できないというデメリットがあります。さらにエクスポネンシャルバックオフを導入すると、再送信にかかる時間はさらに長くなり、メッセージの遅延が増幅してしまいます。
今後の課題
今回、再送信の手法として単純なものを実装しましたが、改善の余地は残されています。たとえば前述のように再送信を待機している間はほかのイベントを処理できません。またネットワーク障害などの時間が少し長くなると、自身で設定した再送信の回数制限に引っかかる可能性が高く、メッセージが失われてしまう恐れがあります。特に今回は処理すべきイベントが頻繁に発生するため、回数制限を小さくする必要がありました。時間の都合上、インターンシップの期間中に実現できませんでしたが、以下を用いる案が考えられます。
DecatonのRetry Queuing
上であいさつメッセージを送信する内部的な動きを説明しましたが、実は以下の図のように、受信したイベントをApache Kafka経由で処理しています。ここでApache Kafkaはイベントストリームのための分散プラットフォームであり、ジョブキューとして用いています。特徴としてはメッセージの永続化や高いスループットなどがあり、OAサーバーではそのような有用性を高めるためにDecatonというライブラリを用いています。DecatonはLINEによって開発されているオープンソースソフトウェアです。
Decatonの機能の一つにRetry Queuingがあり、タスクの処理に失敗したときの再試行をサポートしています。具体的には以下の図のように、メインのキュー (topic) から受け取ったものの失敗したタスクを一時的に別のキューへ移し、しばらく保留した後で再び処理を試みることができます。
この機能をメッセージの再送信に活かすことも考えられます。
メリット
メッセージの永続化に伴い、損失なくメッセージを送信できる可能性が高くなります。たとえばイベントを処理するサーバーに不具合があったとしても、Apache Kafkaでイベントを保持しておくことができ、サーバーの復旧後に処理を再開できます。ほかにはバックオフの制御なども容易です。
デメリット
一つのイベントに対して複数の処理が必要なときは制御が難しくなります。特に今回扱う処理では必ず実行したい、一度だけ実行したいなどの制約が複雑に絡み合っているため、成功した処理と失敗した処理が混在し、実行記録の保持などが問題となります。この問題を解決するために、メッセージの送信に特化した別のtopicを用意することが考えられますが、エラーがほとんど起きず、重要度の高くないイベントに対してはオーバースペックな可能性があります。
感想
今回のインターンはフルリモートでしたが、メンターの方などの手厚いサポートによって非常に多くのことを学ぶことができました。何より実務経験がほぼないところから、実務に沿う形で参加させていただき、見えるものすべてが新鮮に映りました。ここで説明した問題以外にもgRPCやReactive Streamsなどに触れ、LINEでしか味わえないような以下の経験ができました。
- 大規模なプロダクトの一部を担う開発
- チャットツール上でのコミュニケーション
- 膨大なトラフィックを意識した開発
- 実際の障害に対して原因を特定していく様子
- ランチ会でご馳走していただいたうな重
ここで学んだことは今後の開発や研究に活かされると確信しています。みなさんもぜひ応募してみてはいかがでしょうか。
1 認証済アカウントのうち、月に1度以上機能を利用しているアカウント数
2 ここではOfficial Account開発室が運用しているサーバー全体を簡易的にOAサーバーと呼んでいます。
3 Webhookイベントは、LINEユーザーのアクションがLINE公式アカウントに通知されるとき、送信されるデータであり、イベントのタイプや発生時刻などさまざまな情報を含んでいます。ここで紹介した友だち追加のイベント以外にも、送信した動画をLINEユーザーが最後まで視聴したことを示すイベントなどがあり、内部的には自動送信だけではなくプッシュ通知などにも用いています。
4 Messaging APIは、LINE公式アカウントとLINEユーザー間で柔軟なデータのやり取りを可能にするサービスです。一部は一般向けに公開されており、LINE公式アカウントからLINEユーザーにメッセージを送信したり、友だち追加しているLINEユーザー一覧を取得したりすることができます。
5 WebhookイベントIDは、Webhookイベントを一意に識別するものです。2022年4月からWebhookイベントに付与されるようになりました。