RSS

LINE LIVE チャット機能を支えるアーキテクチャ

by LINE Engineer on 2016.9.21


LINE株式会社のOklahomerです。
本記事では、LINE LIVEという動画配信サービスのチャット機能が、どのような構成で成り立っているのか紹介します。

チャットの紹介

LINE LIVEのiOS/Android アプリでは、配信中の動画を視聴しながらリアルタイムにコメント投稿できるチャット機能を提供しています。この機能の役割は、視聴者同士が対話を楽しむだけにとどまりません。配信者が視聴者のコメントに返答するという形で配信者と視聴者の接点として機能したり、また配信者がコメント内容に従って企画を進めるなど、配信者と視聴者が一体となって配信を作り上げていく上でも重要な機能となっています。

これが有名人による配信となれば当然視聴者数も多くなりますし、その配信中に視聴者からのコメントを募れば瞬発的に相当量のコメント流入があることは容易に想像できるでしょう。もちろんコメント流入が増えるということは、全視聴者へと中継すべきコメントの量も増えますから、それらをいかに高速に捌くかが常に課題となります。実際、一配信のみで分速1万件を超えるコメントが投稿されるようなこともあります。

そのためチャットでは、滝のように流れるコメントに耐えることを前提に開発が進められ、今では100台以上のサーバインスタンス上で稼働しています。

以下、その構成を説明いたします。

サーバ構成の全体像

まずは全体像として、以下の画像をご覧ください。

詳細な説明は以降の項目に譲るとして、ここではチャット機能を実装するにあたって重要な「チャットルーム」の概念を理解頂くため、Chat Server1に接続されたClient 1がコメントを送信し、それがChat Server2に接続しているClient 2に送り届けられている点に注目ください。

先述の通り、人気配信者による配信となると流入するコメント量は相当なもので、読み切れないほどのコメントが流れるのは盛り上がりを体感する上でとても重要な要素となります。が、あまりにも多すぎると、コメントを捌くサーバ側にとっても、それを表示するクライアントにとっても負担となってしまいます。そのため、私たちは「チャットルーム」という概念を採用し、視聴者が増えるに連れてチャットルームを分割し、同一チャットルームに属するユーザ同士でのみ対話を行えるようにしました。このチャットルームは複数のサーバをまたいで分散されるため、同一チャットルームに属するユーザ同士でも、その接続は異なるサーバへとバランシングされます。

これを実現するための構成として、チャットサーバの構成の特徴には以下の3つが挙げられます。

  • WebSocket: クライアントとサーバ間の疎通
  • Akka toolkitによる高速な並行処理
  • Redisを利用したサーバ間のコメント同期

以降の項目では、これら3つの項目についてそれぞれ焦点を当てて説明します。

WebSocket

WebSocketを採用することで、張り続けられた単一のコネクション上で、低いレイテンシで双方向のコミュニケーションを行うことができます。これにより、高速に流れるコメントをリアルタイムにユーザへ送り届けることは勿論、ユーザからのコメント投稿に関しても都度HTTPリクエストを送信する必要がなくなるなど、サーバリソースを有効に使える利点があります。

ただし、単一のコネクション上でメッセージングを行うということは、慣れ親しんだWeb APIのようにエンドポイントによってレスポンス形式を分けることができず、サーバ・クライアント共に受け取ったペイロードを適切に識別してハンドリングする工夫が必要になります。チャットの実装ではJSON形式のペイロードを送り合うのですが、全てのペイロードに共通するフィールドを一つ持たせ、その値を識別することによってペイロードが何を表すか識別し、対応するクラスにマッピングできるようにしています。この方法のおかげで、有料ギフトの実装などで新たなペイロード定義が必要になるような場合でも柔軟な対応が可能となります。

ここで注意したいのは、モバイル端末との接続ということもあり、長時間張り続けたコネクションが不安定になるケースが目立つということです。これを回避するため、ペイロードの送信状態を監視し、コネクションが不安定だと判断できる場合は一度コネクションを切断して再接続を促すなどの対応が取られています。

Akka toolkit

Akkaアクターシステムを構成する重要な要素として、何よりまずactorと、そのsupervisorの仕組みが挙げられるでしょう。チャットサーバのアクターシステム構成の前に、前提となる特徴を挙げてみます。アクターモデル全般についての概要は、ここでは割愛します。

Actor

まず、それぞれのactorは内部に状態と振る舞いを持ち、mailboxと呼ばれるキューが割り当てられます。actorが持つ状態は隠蔽されていて外からはうかがい知れないため、actor同士は互いにメッセージを送り合いつつ、受け取ったメッセージに対して各々定義された振る舞いをし、また次のactorにメッセージを送ります。

このメッセージパッシングは非同期に行われるため、メッセージを送ったactorはすぐさま自分のmailboxから次のメッセージを受け取り、次の処理へと移ることができます。そのため、それぞれのactorには大きなタスクを持たせず、細分化されたタスクを少しずつ処理しながら互いにメッセージパッシングすることが、アクターシステム全体として効率よく並行処理を行う上で肝要となります。
この設計を誤ると、ブロッキングな処理がactorの振る舞いに組み込まれてしまい、メッセージが積もってmailboxが溢れてしまうことになりかねません。特に注意したいのは、サードパーティのライブラリを利用する際など、うっかりブロッキングなAPIを呼んでしまうようなケースです。最悪なのは処理が完全にブロックされるケースで、この場合、該当actorの実行を担うスレッドが占有され続けてしまい、ひいてはスレッドの枯渇に繋がります。

それでもakkaアクターシステムを採用する利点としては、「各actorが軽量スレッドを割り当てられていて、そのスレッド上でのみ実行される」というコンセプトで実装できるため、任意のactorが同時に複数スレッドから呼び出されることが無い点が挙げられます。そのため、actor内の状態を管理する際などは、スレッドセーフであることを意識しなくても良くなります。また、対障害性を高める上で重要なsupervisorの仕組みも利点といえるでしょう。

Supervisor

actorのライフサイクルを把握する上で重要となるのは、actorは他のactorによってのみ生成されるということです。これにより、actor間には必ず親子関係が生まれます。この生成したactor(親)が、生成されたactor(子)のsuprevisor(監視者)としての役割を担いますので、どのactorにも必ずsupervisorが存在することが保証されます。Akkaアクターモデルにはlet-it-crashの考えがあり、子actor内で処理が例外を投げた場合は即座にそれがsupervisorである親actorへと伝播され、エラーハンドリングは親actorの責務となります。親actorは受け取った例外を識別し、必要に応じて以下の4つのdirectiveから最適な対応を選択します。

  • Restart: actorの再起動。actorインスタンスを新たに作り、mailboxに溜まった次のメッセージから処理を継続します。
  • Resume: mailboxに溜まった次のメッセージから処理を継続します。Restartがactorインスタンスを再度生成するのに対し、Resumeでは既存のactorがそのまま利用されます。actor内の状態が正しく保てなくなった場合などにRestartを利用し、処理が継続できる場合ならばResumeを利用するなどの使い分けが考えられます。
  • Stop: actorの停止。この時点でmailboxに溜まった残りのメッセージは処理されません。
  • Escalate: 子actorの例外について親自身もハンドリングができないようなケースでは、Escalateでさらに上位のsupervisorに例外を伝播して対応させます。

actorが再起動したり停止したりするとなると、そのactorを参照しているアプリケーションの実装でもactorのライフサイクルを意識しなくてはならないように思えます。が、実際の所、actorを生成した際に返されるのはactorの実体ではなく、ActorRefと呼ばれるactorへの参照のみが返されます。アプリケーション内では、このActorRefに対してメッセージを送ることになるため、その下にいるactorの実体が再起動している最中であったり停止する過程であるなどの状態について意識する必要がなく、実装がシンプルに保てます。また、この抽象化によってアプリケーションコードはactorの所在を知る必要がなくなるため、複数サーバをまたいでアクターシステムを構築する際にも柔軟に対応できます。

チャットサーバのアクターシステム構成

先ほどの全体像より、もう少しactorに焦点を当てた簡略な図を見てみましょう。

上の図のように、主にChatSupervisor、ChatRoomActor、UserActorの3種類のactorが連携しあってユーザのコメントを届けています。それぞれの役割は以下の通りです。

  • ChatSupervisor: JVM上に一つのみ存在するactorで、actorの生成と監視を行ったり、外部から流入するメッセージを対応するactorへとルーティングする役割を持ちます。私達の定義するactor群の最上位に位置するもので、ロジックは持たず、メッセージごとの実際の処理は行いません。
  • ChatRoomActor: 各チャットルーム毎に生成されるもので、チャットルーム内でのコメント送信や配信終了などのイベントを表す各種メッセージは一度ここへと伝えられます。詳しくは後述しますが、サーバ間のコメント同期のためにRedisへpublishしたり、Redisへのコメント保存などもここで行い、クライアントへ届けるべきメッセージはUserActorへとパッシングされます。
  • UserActor: ユーザごとに生成されるactorで、ChatRoomActorからメッセージを受け取り、自分が担当するクライアントのWebSocketコネクションに対してペイロードの送信を命じます。

ここまでの説明で、ChatRoom内でのRedis連携やUserActorでのWebSocketコネクション越しのペイロード送信について言及しました。先述の通り、actor内ではブロッキングな処理を行わないようにすることが重要です。そのため、これらの処理でも可能な限り非同期メソッドを利用し、Akkaアクターシステムに割り当てられた実行スレッドの占有を防いでいます。

Reids Clusterとpub/subの利用

チャットでは、サーバ間のコメント同期と、コメントや各種数値の一時的な保存目的のためにRedis Clusterを利用しています。

コメントの同期

ユーザ数に応じてチャットルームが分割されること、同一チャットルームであっても複数サーバに分散されることは既に述べました。ですが、チャットルームが複数サーバにまたがる場合、サーバ間でいかにコメントを同期するかが重要になります。akka toolkitは豊富な機能を提供していて、Akka clusterやevent busなどの選択肢もあるのですが、akka clusterを採用した場合のnodeの分散や、event busを利用した場合のデプロイ時の煩雑さを考慮して、運用・実装共に容易なredis pub/sub機能を利用しています。以下の図を見ていただくとイメージが湧きやすいでしょう。単一のルームが複数サーバにまたがって存在すること、同一ルーム内でのコメントがredisのpub/subによって同期されていることが伝わるかと思います。

高速なKVSとしての利用

また、配信中のコメントを一時的にredis上に保存する用途やカウンタとしての利用目的で、高い可用性とスケーラビリティを持つRedis Clusterを採用しています。コメントの流量が多くなることなどを考慮し、配信中のコメントやギフト送信情報などのイベントはまず高速なread/writeが可能なインメモリKVSとしてのRedis Clusterに保存し、配信終了後に、恒久的なストレージとしてのMySQLへマイグレートしています。Redisに保存される時点では、これらの各種イベントは配信経過時間を基準とした一つのソート済みセットに納められるため、チャット入室時に直近のイベントを数十件時系列に表示するなどの用途でも重宝しています。これらのイベントはMySQLへマイグレートする段階で正規化され、該当するテーブルへと保存されます。

Redisクライアント

JavaのRedisクライアントライブラリは様々なものがあり、本家ドキュメントで推奨されているものだとJedis、lettuce、Redissonなどがあります。チャットでは以下の理由からlettuceを採用しています。

  • Redis Clusterをサポートしている
  • master/slaveフェイルオーバーやMOVED, ASKリダイレクトに対応し、nodeやhash slot情報のキャッシュを最新に保ってくれる
  • 非同期APIが提供されている
  • pub/subでのsubscribe用コネクションでもフェイルオーバーに対応している
  • 活発に開発が行われている

先述の通り、Akka actor内ではブロッキングな処理を極力避ける必要があるため、非同期APIが提供されているのはとても重要です。またChatRoomActorでのpub/sub利用では、最長で配信開始から終了までの期間にわたってsubscribeし続ける必要があるため、このsubscribe用コネクションの死活監視ができることも重要になります。lettuceではClusterClientOptionsを適切に設定することにより、node downが検知されればsubscribe用コネクションも適宜張り直してくれるという機能があります。また、subscribeの際、接続クライアントが最も少ないノードに対してsubscribe用のコネクションを生成してくれるのも大きな利点です。

まとめ

以上、LINE LIVEのチャット機能を支える構成について紹介いたしました。

  • WebSocketを用いてサーバ・クライアント間のリアルタイムな双方向メッセージングに対応していること
  • サーバ内ではakka toolkitを利用することで高速な並行処理を行っていること
  • Redis Clusterを、一時的なデータの保存用途と、pub/subによるサーバ間のコメント情報同期で用いていること

これらの3点を理解いただければ幸いです。

最後になりますが、冒頭で述べた通り、この機能は100台を越えるサーバインスタンス上で稼働しています。そこで大量のコメントを捌いていると、当然ながらサードパーティライブラリのエッジケースとも言えるissueに行き当たることがあります。そうした際には、その開発コミュニティであったりgithub issueなどを通じて開発者とコミュニケーションを取りながら修正したり、ワークアラウンドを採用することも必要になります。たとえば最近では、Redis Clusterにnodeを追加した際に複数サーバでlettuceの挙動が不安定になり、githubでissue報告をして対応してもらった例などがあります。

詳しくは9/29のLINE Developers Day 2016にてお話させていただきます。

なおLINEでは、次の各分野のエンジニアを募集しています。多くのご応募をお待ちしております。
サーバサイドエンジニア【LINE GAME】【ファミリーアプリ】【LINE Pay】