ルカデテナ ハビエル アキラ氏:ハビエル アキラ ルカデテナと申します。2017年からLINEのソフトウェアエンジニアとして働いています。今日は、LINEのメッセージングアプリケーションのトラフィック・インテンシブ・ストレージについてお話しします。

まずはメッセージングアプリケーションを紹介して、そのストレージの要件とApache HBaseでどうやってその要件を実現するのかについてお話しします。クラスタ改善のプロセスと、正確なデータのモデル化の重要性についても触れます。

LINEのメッセージングアプリケーションは、グローバルで約2億人の月間アクティブユーザーがおり、平均で約40億のメッセージが毎日送信されています。単なるメッセージングアプリではなく、さまざまなサービスが含まれたプラットフォームと言えます。しかし今日は、メッセージングに特化してお話しします。

LINEのメッセージングサービスの基本ロジック

こちらは、LINEのメッセージングサービスの基本的なビジネスロジックです。各ユーザーのアクションは、一連のイベントを生成します。イベントは、クライアントアプリケーションがステートをアップデートするために使います。

例えば、ユーザーAがユーザーBにメッセージを送ります。このアクションは、ユーザーBが新しいメッセージを受信するというイベントを生成します。そしてユーザーBは、トークを開きメッセージを読みます。ここでも、ユーザーAに対してメッセージが読まれたというイベントがさらに生成されます。

これは、メッセージだけに起こることではありません。例えばプロフィール写真を更新するときも連絡先にイベントが送られるので、ユーザーB側のプロフィール写真も新しく更新されます。基本的には、大きな同期サービスです。

こちらは、バックグランドの様子を簡素化したものです。クライアントアプリケーションからのリクエストは、LEGYという弊社内のリバースプロシキとAPIゲートウェイを通って、リクエストを通信先のバックエンドにリダイレクトします。

そのバックエンドが、弊社のストレージを利用してリクエストに応えます。時としてバックグラウンドでより重いものを処理する場合やタスクをリトライする場合があり、これは非同期タスクプロセッサーで行うことで、バックエンドのアプリケーションの負荷を緩和します。総じてバックエンドは、ストレージをかなりヘビーに使います。

RedisとApache HBase

今日は、RedisとApache HBaseについてお話しします。Redisは多くのエンジニアに馴染みがあると思いますが、Redisはインメモリデータストア(IMDB)で、弊社はこれをデータベースとキャッシュに使っています。

Apache HBaseは分散型ストアで、ランダムでリアルタイムにHDFS(Hadoop分散ファイルシステム)上でRead/Writeアクセスを行い、また永続的レイヤーでもあります。この2つのストレージは、メッセージ、イベント、ユーザーデータ、ソーシャルグラフなどのデータをもちます。

まずはRedisについてですが、LINEには2種類のRedisクラスタがあります。1つ目はRedisのオフィシャルクラスターがまだリリースされていなかったときに、社内で開発したクラスターです。ここで、データがシャードに分けられ、マスタスレーブモードで複製されます。

各シャードのロケーションは、Zookeeperの中に格納されています。したがって、LINE RedisクライアントはZookeeperを使ってシャード情報と同期を取れば、シャードの場所が確認でき、シャードと直接通信してデータを読み書きできます。クラスタマネージャーサーバーは、Zookeeperのシャード情報を更新して、クラスタの健全性をチェックします。

弊社では、徐々にRedisのオフィシャルクラスタの適応を始めています。ビルトインのシャードがあり、これらはマスタスレーブで複製されています。ゴシッププロトコルというシステムの参加者間で、クラスタ内のノードについての情報を伝搬します。

Apache HBaseは、見た目は手強そうですが、それほど複雑ではありません。データはテーブルで整理され、テーブルはデータをリージョンに分割します。データスプリット、またシャードと考えてもいいでしょう。 各リージョンには、リージョンサーバーがあり、青の箱になりますが、これが自分のリージョンのRead/Writeを行います。

そのときに、Hadoop分散ファイルシステムにアクセスして、そこから読み書きします。マスタプロセスを使ってクラスタを管理し、各ノードが生きているかを制御していきます。Zookeeperは、クラスタ内のタスクの設定と調整を行います。

HBaseクライアントは、まずZookeeperと通信して、クラスタの情報とメタテーブルの場所を理解します。メタテーブルは、クラスタ内のリージョンの位置情報をすべてもっています。HBaseクライアントがメタテーブルを読むと、リージョンの場所がわかるので、相手先のリージョンサーバーと直接通信して、データの読み書きを行います。

リージョンは、Hadoop分散ファイルシステムを利用します。データがデータブロックに分割されており、データノードが割り当てられています。各ブロックは、異なるデータノードの中で設定された数の分だけ複製されています。ネームノードは、各ブロックの場所の情報をもち、ブロックの複製の数を維持して、データの損失を防ぎます。

要求条件にはユーザーのニーズを満たすこと、できる限り最高の体験を提供すること

それでは、次に要求条件についてお話しします。LINEのバックグラウンドでは、毎日およそ2.7兆のリクエストに対応しなければなりません。お正月のピークは、通常の3倍のトラフィックにもなります。毎秒1億以上のリクエストが発生します。これは、アナリティクスやメトリックデータは含んでおらず、サービスだけで約1.6ペタバイトのデータを処理する必要があります。

弊社のストレージは、ユーザーの体験に直接関わってくるので、要求条件にはユーザーのニーズを満たすことと、できる限り最高の体験を提供するということが含まれています。サービスが常に期待どおりに機能するためには、ストレージが高性能でかつ高信頼性であり、さらに高可用性であることも必要です。

一般的なハードの不具合があっても、エンドユーザーには影響を与えず、一貫性のあるデータを提供しなければなりません。ユーザーが古い情報や誤った情報を見ることは避けなければなりません。一方で、システムを維持管理するために、スケーラブルなものにして不必要なコストは回避する必要もあります。

ストレージの要求条件

それでは、要求条件を詳細に見てみましょう。まずストレージは、維持管理できてスケーラブルでなければなりません。ここが社内Redisクラスタの苦しいところで、動的にリサイズできないと、Redisのバージョンアップは難しくなり、運用コストもかかります。クラスタを拡張するたびに新しいものを作り、裏でデータを移行させなければならず、時間も運用コストもかかります。

Redisオフィシャルクラスタは、こうした問題に対処し、動的にリサイズできるようになりました。しかし数年前、Redis3でテストをしたのですが、新しいノードを追加してクラスターが大きくなったらパフォーマンスが落ち、メモリとネットワークのコンサンプションが上がりました。概して、メモリコストがディスクよりも高いというのは異論はないと思います。

そこで、Redisオフィシャルクラスタの新しいバージョンを試してみてどうなるかを検証したところ、最終的にApache HBaseは、水平的にスケーラブルであり、パフォーマンスの劣化もないため、よい選択肢でした。

バージョンのアップグレードも問題なく展開できました。さらに、クラスタの運用のための自動ツールの構築に力を注ぎ、運用もしやすくしました。

ストレージの高可用性と耐障害性

もう1つの重要な要件は、ストレージの高可用性と耐障害性です。弊社のバックエンドは、従来のJavaマルチスレッドアプリケーションであり、ストレージに問題が起こると、スレッドが待ち状態になります。これがサービスに悪影響を与え、バックエンドの信頼性にも影響が出ました。たくさんのリクエストがある重要な場所においては、とくに顕著になります。

これを解決するために、まずRedisとHBaseのクライアントのショートリクエストタイムアウトとクラスタリカバリータイムを最小限にしする必要があります。しかし、これだけでは足りない場合があります。

そこで、Redisはショートサーキットブレイカーの仕組みを取り入れました。反応時間が長くなると、一時的に不具合としてシャードをマークして、リクエストの送信を止めます。Redisはシングルスレッドなので、遅くなると同じシャードに向けたリクエストにも影響が出るため、これはとても重要です。

HBaseは、クラスタの二重化を行いました。2つの同じクラスタを構築し、両方のクラスタにリクエストを送ります。少なくとも1つは成功するようにすれば、1つが失敗しても、リクエストはブロックされず、可用性が高まります。

しかし、同じデータをもった2つの同じクラスタがあるということは、時々一時的にデータの一貫性が失われるリスクがあります。ゆえに弊社は、データが変更できない、または一時的なデータの不一致にも耐えられる場合に限定して、この技術を使います。どこでも使うというわけではありません。

RedisとHBaseの非同期を解決する

もう1つの要件は、データの一貫性を提供するということです。ユーザーが古い、または誤った情報を見ることは避けたいからです。まずはRedisをキャッシュとして利用したいのですが、LINEのメッセンジャーの初期には、データは完全にRedisに格納されており、それを使い続けて拡大し、増加するトラフィックに対応していました。

時間の経過とともに必要なメモリが増え、これが大きなコストとなりました。さらに、マスタスレーブモードでデータを複製しても、すべてをメモリの中に入れていたので、データ損失も恐れていました。

そこで、データをHBaseにも書くことにしました。パフォーマンスへの影響をなくすためにオフラインとリトライの仕組みを使って、非同期にHBaseに書き込むことにしたのですが、お気づきのとおり、RedisとHBaseの間には、本当のトランザクションがありません。

非同期で書くことでRedisの条件の影響をより受けやすくなります。両方のストレージを時間の経過とともに一致させ続けるのはとても難しく、HBaseに格納されていたデータは、100パーセント信頼できない状態になっていました。

メモリの問題は、まだ解決していませんでした。まだすべてがメモリの中にありました。お金がとてもかかっていました。そこで、ほとんどのコア機能に対して、HBaseをプライマリーストレージにすることにしました。そしてRedisの役割を変えて、より従来のアプリケーションアーキテクチャのように、キャッシュとして使うようにしていきました。

こうして信頼できる1つの真実の源をもつことができ、メモリの使用も減らし、HBaseの永続性とHDFSのデータ複製のおかげで、データ損失の可能性も回避できました。

こうして、コア機能はHBaseをプライマリーストレージとし、新しい機能もHBase上に構築され、新しいサービスやモジュールにも使われるようになりました。しかし、Redisを忘れたわけではありません。まだ大半の読み込みを行なっています。

しかし、HBaseの責任と利用が高まっているのは明らかで、それはHBaseがユーザー体験に大きな影響をもつことを意味します。ゆえに、HBaseの要求条件に対して真剣に対応しなければなりません。

HBaseで水平的スケーラビリティが実現でき、メモリの使用を減らしてコストが削減できました。データの一貫性も改善し、データ損失のリスクも低減しました。高可用性もクラスタの二重化で実現できました。

信頼性とパフォーマンス

信頼性とパフォーマンスはどうでしょうか? 特にRedisのようなインメモリストレージと比較するとどうでしょう? クラスタの信頼性とパフォーマンスのさらなる向上が必要です。

そのためには、使用するすべてのバージョン、設定、機能を注意深く評価し、テストする必要があります。他の技術同様、パフォーマンスは使い方の影響を受けるので、データのモデル化、アクセスの仕方に注意しなければなりません。

このような要求条件を、Apache HBaseでどのように実現したのか見てみましょう。クラスタを強くするために、すべての新しいバージョン、機能、設定を注意深く評価しテストをしています。実際に見てみましょう。

HadoopとHBaseのオープンソースコミュニティは、すばらしい仕事をしていると思います。しかしどんなソフトもそうですが、バグがあります。問題はそうしたバグが、弊社のサービスに影響を与えるか否かです。そういったバグやパフォーマンスの劣化がサービスに対して起らないように、評価とテストを行うために、テスト環境を作りました。商用クラスタ内の条件やトラフィックを忠実に複製しました。

アプリケーションのバックエンドが、本番環境のHBaseクラスタに出すリクエストをインターセプトして、シリアル化してKafkaに送り、小さいコンシューマーを使ってリクエストをリプレイして、テスト用のHBaseクラスタに流します。そして、新しいバージョンや新機能を試します。

このような、商用環境とほぼ同じトラフィックの安全な環境で、バグやパフォーマンスの劣化を見ていきます。

これが通常のフローです。まずリクエストをリプレイし、テストをする新しいバージョンまたは機能をデプロイし、バグを検知し修正する。またはオープンソースコミュニティがフィックスしていたら、それをバックポートします。機会があれば、オープンソースプロジェクトにも貢献します。最後に、安全に本番環境に適応していきます。