LINE株式会社のmoznionです。 私の所属するチームではLIVEという動画配信サービスを開発しています。芸能人や有名人の生配信やコンサートの様子の中継など様々な映像コンテンツが日々配信されているホットなサービスとなっておりiOS/Androidアプリと共にPCブラウザをサポートしています。 本記事ではこのLIVEにおける連打を支える技術についてご紹介します。 背景 iOS/AndroidのLIVEアプリには、動画プレイヤー上に配置されている「ハート」を押すことで配信者を応援するというシステムがあります(図中1)。視聴者はこの「ハート」を連打することが可能であり、それに呼応してプレイヤー画面に表示されるカウント、すなわち「すべての視聴者が押した『ハート』の数」が増えていくというインタラクションを得ることができます(図中2)。この数字が増えることで配信者はファンの応援を感じることができますし、視聴者は数字がどんどん増えていくさまを目のあたりにすることで配信に対する一体感や熱狂を感じることができるかもしれません。 ところで、開発者の間ではこの「ハート」機能全般のことを指して“love”と呼んでいますから、本記事でも以降は“love”と呼称することとします。 サービスの特性上、人気のあるアイドルの生配信などは莫大なloveが突発的に送られてくるため、サービスを落とさないようにこれらのリクエストを効率的にさばく必要があります。そうした大量のユーザが連打可能かつインクリメンタルなカウンタを、LIVEではどのようにして高可用かつある程度の即時性を持たせて実装しているかを以降で紹介します。 アーキテクチャ 概略図を以下に示します。以降のトピックで、この図に示した内容の詳細を解説します。 はじめの一歩 安定してサービスを提供するためにすべきことの1つとして、「リクエストの数を減らすこと」が挙げられると思います。これは多くの場合に当てはまる本質的な解決法です。 あくまで例ですが、loveが1回タップされる度に1リクエストを発行して、サーバに問い合わせて諸々処理をしてからリクエストを返す、というような実装をしていてはあまりに無駄が多く、またスケールしにくくなってしまいます。事と次第によってはネットワークのレイヤで詰まってしまうかもしれません。それではあまりに大変です。 そうした理由から、クライアントサイドでまとめてリクエストを送ってもらうことで総リクエスト数を減らすという解決策に至るのは自然の成り行きと言うことができるでしょう。 例えば「あるタイムウィンドウ内に何回loveをタップされたか」をクライアント側でバッファリングしてもらい、タイムウィンドウから抜ける際にそのカウントを含めたリクエストをサーバに投げるというような素朴な手法を考えることができます。そしてサーバはそのカウントを受け取り、保持しているカウンタをそのカウント分インクリメントするというような具合です。 LIVEでは純粋なカウントの代わりに、「動画の何秒時点を再生している時にloveがタップされたか」という動画のタイムスタンプをリストとしてバッファリングし、クライアントから送ってもらうというアプローチを採ることで、視聴者1人あたりのリクエスト数を削減することに成功しています。なお、「タップされたタイムスタンプのリスト」を送信しているのは後に分析用途で利用するためです。分析については後述します。 また、こうした仕組みを採用する場合はチート対策もしっかりと講じておく必要があります。例えば悪意あるユーザが1つのリクエストに大量のカウントを乗せてきた時に、それを額面通り受け取ってしまっては目も当てられません。LIVEではチートについてもしっかりと対策を講じています。 高速なストレージを使う ライブ配信中はその配信にアクセスが集中するため、同時接続する視聴者数が多くなります。そして同時接続する視聴者数が多くなれば多くなるほどloveに対するリクエスト数も多くなります。 そこでLIVEでは、RDB(LIVEではMySQLを利用しています)にloveのカウントを持ち、loveのリクエストを受けるごとにインクリメントするという風に都度DBアクセスを生じさせるよりも、インメモリのKVSにカウンタを持っておきそれに対してアクセス・インクリメントを行うことで効率的にIOを処理するという手法を採用しています。 LIVEではRedisのversion 3からサポートされているClustering機能を用いてRedis Clusterを構築しており、loveのカウンタでもそのRedis Clusterを利用しています。Redis Clusterはそのspecにもあるように、高いパフォーマンス、高い可用性、そしてスケールアウトのしやすさを誇るインメモリのKVSです。LIVEではloveの他にも様々な用途でこのRedis Clusterが利用されています。 内部の処理としては、リクエスト経由で受け取ったタイムスタンプのリストのサイズを取り、配信に対応するRedis内のloveカウンタをそのサイズ分だけINCRBYによってインクリメントする、という具合になっています。 またライブ配信の終了時に、その配信に対応するカウンタの内容をRedisからMySQLにflushし、その後にRedisからそのカウンタは削除しています。これはRedisには永続的なデータを残さず、ミニマルに利用したいという理由からです。永続的なデータはRDBに任せ、揮発しても問題が無くなおかつスループットが求められる用途(例えばキャッシュや今回のloveのような)にはRedisを用いるという棲み分けが行われています。 なお本筋とは関係ありませんが、私たちのチームではRedisを使う際にエントリのkeyの前にname spaceを付与する運用をしています。例えば[service-name]|[phase]|[entry-key]という具合です(phaseの部分には、本番環境であれば“release”、ステージング環境であれば“staging”などといった文字列が入っています)。こうすることで、key名を見るだけでどのサービス、どのフェイズなのかをひと目で判断することもできますし、うっかり別のサービスやフェイズのエントリが紛れ込んでもサービス自体に影響を及ぼさずに済むというメリットも得られます。また、prefixをもとにしてサービスやフェイズごとの統計情報も取ることができます。詳しくは以下のブログ記事が詳しいので併せてご参照下さい(なお参照ブログ記事ではmemcachedがトピックとなっています)。 http://blog.nomadscafe.jp/2013/07/cachememcachedfast.html シンプルなデータ構造を扱う 保持するデータ構造、またリクエストに乗せるデータ構造をシンプルに保つというのは、高いスループットを実現するために必要なことの1つです。特にインメモリのKVSの場合、データ構造が複雑になればなるほどストレージに乗せにくくなりがちですし、所望のデータを簡単に参照し、抜き出してくるのも難しくなります。 そこでLIVEでは、Redis内に単純なカウンタのみを作成し、そのエントリに対してINCRBYを発行することで値の更新、GETを発行することで値の取得、DELを発行することでカウンタの破棄、という具合にシンプルな操作で必要とする機能を実現できるようにしました。 また、適切なデータ構造を選択するというのも肝要となるでしょう。 例えばある配信に対するloveについて、「視聴者ごとの送った数ランキング」のような見せ方をしたい、というようなある程度複雑な用途であれば、シンプルなカウンタを用いてアプリケーション側で集計してランキングを組む、という方法よりもRedisの持つSorted Setを1つの配信と紐付けた上で、scoreをloveのカウント、memberを視聴者IDにして、ZRANGEあるいはZREVRANGEを用いて簡単にランキングの構造で引いてくる方が楽ですし効率的でしょう。 ドメインに応じて、用途に沿っていてなおかつ最もシンプルなデータ構造を選ぶことが高いスループットを維持する為の勘所と言うことができると思います。 データ分析 LIVEはloveのカウントをインクリメントするタイミングで、同時にfluentd経由で自社内のHDFSストレージに分析のためのログを投げています。ログの内容としては、「動画中のどのタイミングでloveがタップされたか」といったものをはじめとした様々なデータがあります。そして、収集されたログをHiveやPrestoを用いて分析し、サービス品質の向上に役立てています。 前述した「シンプルなデータ構造を使う」というのはサービスのスループットを上げるために必要な要素でしたが、とは言えそのデータについて分析したくなるというのが人情です。しかし、これらを同時に満足させようとするとどこかで無理が来てしまうか、あるいは相応のコストを支払って解決する必要が出てきてしまいます。従ってLIVEでは、主たる処理と並列して複雑なデータをlogger(今回はfluentd)を用いて収集してHDFSに溜め込み、その溜め込んだ内容を別コンポーネントで別途分析するという構成を採っています。 このようにサービスを提供するコンポーネントとデータを分析するコンポーネントを分離することで、データ分析時に高負荷が生じてもサービスの提供には影響を及ぼさずに済みますし、その逆もまた然りです。そしてサービス側で必要になった段階で、データ分析コンポーネントにリクエストを送って分析結果を取得してくるという方法で分析結果をオンデマンドに取り込んでいます。 分析についてはリアルタイム性はそこまで求められていない、というサービスの特性からこのような仕組みとなっています。 サーバを分離する LIVEはloveのカウントをインクリメントするタイミングで、同時にfluentd経由で自社内のHDFSストレージに分析のためのログを投げています。ログの内容としては、「動画中のどのタイミングでloveがタップされたか」といったものをはじめとした様々なデータがあります。そして、収集されたログをHiveやPrestoを用いて分析し、サービス品質の向上に役立てています。 全てを同じAPIサーバで取り扱った場合、特定のコンポーネントの負荷が極まると他のコンポーネントを巻き込んでしまい、最悪の場合サービス提供が不可能な状態に陥ってしまうかもしれません。高負荷が見込まれるコンポーネントはあらかじめ別のサーバに分離して運用することでこうした事態を防ぐことができます。 また、負荷が高まってきたらそのサーバを増強することでスケールアップ・スケールアウトすることもできます。例えば、突発的な負荷上昇した時にアプリケーションエンジニアが不在の場合でも、インフラの面倒を見ることのできるエンジニアやオペレーションエンジニアのみでもその場の対応が出来る、などといった柔軟性のある運用が可能となるでしょう。 まとめ 以上のトピックをまとめると以下のようになります。 リクエストの数を根本的に減らす。チート対策もしっかり行う。 場合に応じて高速なストレージを使う 高いスループットを実現するためにシンプルなデータ構造を用いる データ分析がリアルタイムに行う必要が無い場合はデータだけをどんどん溜めてゆき、別コンポーネントで分析させる 高負荷が見込まれるコンポーネントは別のサーバに分離して運用する 以上がLIVEにおける連打に対する取り組みでした。 このloveの機能はiOS/Androidのアプリをインストールし、LINEアカウントでログインすると利用可能になりますので興味のある方は是非お試し下さい。 最後になりますが、LINEでは映像や連打に興味のあるエンジニアを募集しています! サーバサイドエンジニア【LINE GAME】【ファミリーアプリ】【LINE Pay】 サーバサイドエンジニア【LINEプラットフォーム】
↧