LINE公式アカウントに関連するシステムを支える変更データ同期

松田一樹氏(以下、松田):『Large scale CMS を支える変更データ同期』ということで、LINE開発4センターの松田が発表いたします。今日はよろしくお願いします。さっそくですが、内容に入らせてください。今日はLINE公式アカウントに関連するシステムを支える変更データ同期について紹介します。

LINE公式アカウントと書くと長いので、以降あいまいさがない場合には、OA(Official Account)と書かせてください。LINE社内には、OAとひもづいて提供されるサービスがいろいろあるのですが、そのサービスがOAのデータのコピーをもっていることがあります。

OAのCM情報が、我々のCMS、LINE Official Account Managerを通じて更新された場合、各サービスにその情報を同期する必要があるということが問題になっていまして、その変更同期がどのように行われているのか、その変遷と最終的なデザインを紹介できればと思います。

本日のアジェンダですが、まずはLINE Official Account Managerと社内サービスの関係について説明して、その中での課題を説明します。次に、今までどんな手法でやっていたのかという紹介。それらを踏まえて、最後にたどり着いたやり方を説明させていただく、という内容です。短い時間ですが、よろしくお願いいたします。

LINE Official Account Managerの開発はおもしろい

最初に、LINE Official Account Managerに関して、少し詳しく説明させてください。manager.line.bizというURLでアクセスできるOAの管理者が利用するWebツールと、その背景にある社内向けのAPI群をこう呼んでいます。OAの管理者が利用する管理ツールで、いわゆるコンテンツマネジメントシステム(CMS)と呼ばれるものですね。OAのコンテンツは、一般的にはここから投稿することになります。

LINE Official Account Managerでできることに具体的にどのようなことがあるかですが、「公式アカウントを通してメッセージを送信する」「タイムラインに投稿する」「公式アカウントの名前を変える」「公式アカウントのプロフィール画像を変える」など、みなさんが個人としてLINEアプリ上でやっていることをいろいろとWeb画面から行えるツールです。

こういったCMSというと、例えば入稿システムや裏方のシステムなので、あまりおもしろいというイメージをもたれていない方もいるかと思いますが、LINE Official Account Managerとその関連システムを開発するのはおもしろいんですね。

おもしろさの理由は、単純にトラフィックが多いからというだけではありません。LINE社内ではOAを主体として提供されているサービスが多くあって、それと連動するためにさまざまな技術的な課題を解決しないといけないところにあります。

例えばLINE占いでは占い師の実装がOAであって、占い師のみなさんはLINE公式アカウントを通じて、ユーザーとコミュニケーションを取ったりしています。このようにOAの情報が社内のさまざまなサービスで利用されていて、ユーザーに見えるのですが、各サービスはデータベースも別のことが多いですので、更新の内容、例えばOAのプロフィール画像が変わって、それをユーザーにすぐ届けようと思ったら、いろいろな問題を解く必要があるということになります。これがおもしろいポイントです。

関連システムの規模と課題

社内のいろいろなサービスから利用されるのがおもしろい、という話をしたので、改めて簡単に規模感を紹介します。まず書き込みパスの数なのですが約22のシステムから、データが書き込まれていました。先ほど社内のドキュメントを確認したら、LINE Official Account Managerとその裏側のInternal-api、直接ユーザーがLINE Official Account Managerを使うケースに加えて、社内のさまざまなツールを通じて公式アカウントの情報が更新されています。

バックエンドに共通のデータベースがあって、社外ユーザーにはLINE Official Account Managerのような Web 画面が提供されますし、社内ユーザー向けには各種 API で提供している、というイメージです。

さらにOAの情報を利用する読み込み専用のサービスが社内に50以上存在します。LINE公式アカウントリストといったような、公式アカウントの一覧が載っているメディア面とか、LINE広告もそのOAの情報を利用しています。

さらに内部的なコンポーネントで言うと、OAの情報をElasticsearchのような全文検索をサポートしたエンジンに書き込んでおいて、名前をもとに検索することがあれば、こちらにクエリを流すみたいなこともしています。

50サービス以上をさまざまなプログラミング言語で開発していますし、開発チームも日本以外にも分散しています。エンドユーザーも一般のLINEユーザーのこともあれば、例えばLINE占いで占い師をされている方々みたいなケースもあるわけです。以上が、LINE Official Account Managerとそれに関連するシステムの説明でした。

ここで解決しないといけない問題について改めて整理しますと、社内外のユーザーがOAの情報をさまざまなかたちで更新するので、それを別のサービスに提供、同期していくハブにならないといけない、ということになります。

そして、そのとき以下を満たす必要があります。特定のサービスがダウンしているときでもほかのサービスに影響を与えずに同期が行える。50サービスあるとダウンしているサービスもあったりなかったりするわけですね。また新規サービスインされるサービスでも、できれば同じ方法で同期できること。また同期ミスがあったサービスも、サービス単体の力で同期を回復できることです。

以上の課題を解決するために、いろいろな共有と同期方法があるわけですが、従来どんな方法を取ってきたかを紹介したいと思います。

データ共有と同期の6つの方法

ここでは、方法を6つ紹介したいと思います。名前からイメージを掴んでいただけたらと思います。順番にShared DB、API Integration、API Crawling、Data Feed Integration、API Push、Message Queueの6つです。

最初の方法ですが、Shared DB。文字通りデータベースでの読み取りアクセス権を共有して、データを提供していくパターンです。シンプルなのが特徴ですが、非常に強いスキーマの依存関係があるので、変更があったときとサービス側のデータとジョインできないことで、問題になることもあります。

次がAPI Integration。JSON RESTや、先ほどのgRPCみたいなAPIで情報を提供するパターンです。マイクロサービスといえばまずこれ、といったようなイメージも強いし、LINEでも実際非常によく使われています。一方で、サービス側の都合でジョインしたり自由なクエリができないということで、まだまだサービス側の独立性が足りないといった考え方もできます

次がAPIのCrawlingで、なんらかのかたちでID一覧などの元データがあるので、検索エンジンのようにAPIを順次呼び出して、データを同期しようというのがCrawlingの方法です。データを一旦サービス側に取り込むので、好きにジョインができます。

また特徴としては、先ほどのAPI Integrationで始めた場合に移行しやすいということです。API側のパフォーマンスに問題がなければ、今まではリアルタイムで呼び出していたものを深夜に1回の同期で全部呼ばせるようなこともできるわけです

ただデメリットも多くて、やっぱりパフォーマンス的なところが問題になって「いや、全部順番に呼ぶのはやめてくれ」みたいなことを言うこともあります。我々でも問題になったことはあります。

また社内でもよくあるパターンで、リトライはできるものの、通常こういったバッチは深夜に1日1回同期みたいなことをするんですが、問題が発生すると、営業時間内にまたバッチをリランすることになります。

いつもは実行しているこのバッチですが、ユーザーのリクエストもある昼間の時間帯にリランして、大丈夫なんだっけ? っていうようなことを呼び出し側、呼び出し元、両方で不安になりながら実行することもよくありました。

これを少し改良したのがData Feed Integrationで、プラットフォーム側がバッチでデータを書き出しておいて、サービス側もバッチでそれを取り込む連携方法です。

5つ目6つ目はプラットフォーム側が状況に積極的に関与する方法

5つ目なんですけれども、API Pushは、データが更新された場合にプラットフォーム側が能動的にサービス側のAPIを呼び出して通知するようなパターンです。もしLINE BOTなんかを作られた方がいれば、まさにBOTというのは、この方法でトークルーム内のメッセージを送信しているのをご存知だと思います。

更新があったアイテムだけ処理が走るので、リアルタイムで動作するのが特徴です。デメリットとしてはプラットフォーム側の負担が少し大きくて、サービス側がダウンして通知を受信できないときにリトライをしてあげる。ないし、そもそも諦めてもらうという判断が必要になります。

実際にこれを業務で使われている方は、受け側に問題があったので、もう1回API Pushを送ってくれませんか? みたいなのをお願いされた経験もあるんじゃないかなと思います。

最後の6つ目がMessage Queueを使って通知を送るパターンで、Webhookの代わりにMessage Queueを使うことによって、配送専用のミドルウェアに任せられるので、障害の問題から解放されるという話があります。Webhookとの比較ではMessage Queueを使ってもらう必要があるっていうので少し取っつきにくいかなというのが多いです。

今までの6つのデータ同期方法をまとめさせてください。主に2つずつ見ていくのがいいと思います。

1つ目のShared DBと2つ目のAPI Integrationは、どちらもマイクロサービス側に情報をもたずにエンドユーザーからリクエストが来たタイミングで、プラットフォーム側にデータを問い合わせるという方法でした。違いはデータの問合せに、読み取り専用のアカウント経由のSQLを使うのか、RESTとかを使うかだけです。

状態をもたないためマイクロサービス、サービス側へのリクエストに比例した負荷がかかることになり、これが原因で適応できないケースも多いです。ただ、この状況っていうのは同期にとっては理想的で、なぜならサービス側に状態をもっていないので、そもそも同期の問題について悩む必要がないんですね。したがって、遅延もゼロという感じです。

3つ目4つ目は単純に言うと、バッチで同期しておこうというアイデアです。バッチが同期する分、範囲が遅れることになります。1時間とか1日ですね。

5つ目6つ目は、もうちょっとプラットフォーム側が状況に積極的に関与して、更新情報を送ってあげるというバターンでした。OAはさまざまメリットデメリットはあるんですけれども、ここで紹介した方法をいろいろ組み合わせて使っています。そして今日このあと紹介するのは、6つ目のMessage Queueを利用した一番新しい方法です。

最終的にはMessage Queueを利用した手法を導入

それでは新しいデザインについて紹介したいと思います。我々は最終的にはMessage Queueを利用した手法を導入しました。理由はここに書いてあるとおり大きく4つ。1つ目はStatelessな方法がそもそも使えないサービスがあるということでした。

OAのデータとサービス固有のデータをジョインしながら検索、ソートしたいというケースとか。プラットフォーム側が落ちていてもサービスを提供したいとか。本当にとんでもないリクエストが突発的に発生するので、ローカルにデータをもっておきたいということです。

次がリアルタイムの同期が必要で、バッチによる同期が適用できないということ。さらにサービス側がダウンしたときも自然に回復するようにしたい。最後はプラットフォーム側の都合なんですけれども、アップデートがあった数だけの処理で済ませたいと。ほかのやり方だと連携するサービス数がオーダーに関わってきたりするんですけれども、それを避けたいということです。

1つ注意点なんですけれども、我々はすべてMessage Queue単位でやっているということではなくて、場合によって使い分けています。

Message Queueを使ったインテグレーションについて

ここからはMessage Queueを使ったインテグレーションについて紹介したいと思います。具体的な設計について、Message Queueを使うというのは、先ほど触れたとおりです。Message Queueの具体的なプロダクトにはApacheのKafkaを利用しました。

RDBのMySQLとかに比べると知名度は劣りますが、社内では安定して非常に広く使われているミドルウェアで、情報を配信するFan-outに非常に適したデザインになっているのが理由です

またここが特徴的なところで、メッセージ通信の起点はMySQLのbinlogをパースして、データベースに変更があればもれなく通知するかたちに実装しています。ここのデザインの大きな位置を占めているのが、データベースに対する変更データキャプチャというデザインパターンと、それを実現するDebeziumというオープンソースのソフトウェアです。

DebeziumはRedHatが中心となって開発されているOSSで、機能としてデータベースの変更をキャプチャしてアプリケーションから利用可能にすることがあります。MySQLほかいろいろなリレーショナルデータベースに対応しており、AWSでの利用実績もかなりネット上でヒットします。Javaで書かれているということも、我々にとっては心理的障壁が低くなるポイントでした。

binlogをパースするというと、けっこう遠まわりな方法のような気がしますが、我々は2つのメリットがあって、このbinlogをパースする方法を採用しました。まず第1にメッセージを送信する実装漏れを避けられることがあります。

背景として、我々は270を超えるInternal-apiを社内に対して提供しているんですね。素直にアプリケーション上でDBに書き込んだあとコード上で更新通知を送るようにすると、どうしても実装漏れというのが出てきてしまいます。

ただのバグなんですけど、こういうことがあると、結局各サービス側から「いや、1週間に1回は全部Crawlingさせて同期させてくれ」みたいな話になるので、少しよろしくないと。

MySQLのログをもとにキャプチャすれば、実装漏れがなくて、MySQLに書き込まれたものがすべて更新通知の対象になります。オプションでマニュアルオペレーション、直接開発者が発行したアップデートも通知の対象になるのがありがたい点です。

最終的な実装

もう1つが、これもRDBを直接参照するのが必要な点として、Replication Delayを考慮した実装が可能だったことです。背景として、我々のサービスというのは1つのMySQL source、マスターのことですね、に対して9つのレプリカを接続してサービスリクエストを捌いています

負荷が高くなると、Replication Delayが大きくなって、プライマリーを更新完了後すぐにKafka経由で更新通知を送るとサービス側から、洗い替え用のGETリクエストが来て、これがレプリカからのデータ呼び出しになるのですが、この時点ではレプリケーションは完了していないので、更新前のデータを呼び出してしまうことがありました。

この解決方法として、binlogを読み込んだときのレプリケーションポジションを明示的にもっておいて、すべてのスレーブがそのデータを返せることが保証されたタイミングで、更新通知をサービス側に送るといったことで解消できました。

最終的な実装がこのようになりました。今回の実装の中心になるのが、中央に書かれているdb-event-producerと呼ばれるコンポーネントで、起動するとbinlogの読み込みを開始して、MySQLに新しい更新があるかどうかというのを監視し始めます。また各サービスもMessage QueueであるところのKafkaに接続して変更がないかを監視します。

実際にユーザーが変更を行いますと、その結果が最終的にデータベースに書き込まれるのですが。この102番、103番ですね。データベースに書かれたあと、MySQLはレプリケーション用のbinlogを生成して、この内容をアプリケーションのevent-producerがConsumeします。

この中には、興味があるテーブルと興味がないテーブルすべての変更が入っていますので、必要に応じて興味があるテーブルの更新情報だけをフィルターして。あとAPI単位じゃなくてレコード単位の更新になりますので、必要に応じてDe-dupをして。さらにすべてのスレーブが同期済みになったことを確認してから、Kafkaに更新通知を送るといったことになります。

その通知を受け取れば、各サービスはデータを更新したり、またはキャッシュを能動的に消しに行ったりみたいな行動をするわけです。

アプリケーションで変更をハンドリングすると思わぬバグの温床になりがち

最後にまとめです。学びとしては変更データ同期です。リレーショナルデータベースが中心のプラットフォーム、今回の我々のCMSですが、更新をもれなく伝えるためには、まずはRDBを中心に考えるとよいなというのが学びでした。

これ同じことを言っているイメージなんですが、アプリケーションで変更をハンドリングすると、思わぬバグの温床になりがちだったんですね。もちろんうまくできればいいんですけれども、変更の同期に関してRDBはすでにレプリケーションという仕組みをもっていて、これは十分実績があります。

レプリケーションログをアプリケーションが取得することは可能で、オープンソースの安定したプロダクトもあるので、まずはこれを中心に考えるとよかったなというのがあります。

2つ目がシステムの整合性について、さまざまな単位で知っておく必要があったのも学びでした。整合性というと、例えばリレーショナルデータベースが単体でもっている整合性、トランザクションをどうするかみたいな話ですね。

MySQL単一ノード上の整合性が基本にあるんですけれども、ソースとレプリカを含めたMySQLクラスタ全体がもっている整合性はそれとは異なります。また変更通知をKafkaで送った場合に、通知を受けたサービスから変更内容が本当に見えるのか、といった点意識するのもまた楽しかった点です。

最後にLarge scale CMSでまとめます。非常に大きな単位のコンテンツマネジメントシステムはデータベースを更新して終わりではなくて、状況に積極的に関与して更新通知を送っていく必要がありました。また関連するマイクロサービスに対する責任は大きいが、おもしろいというのがあります。知っていたつもりだったけれども、実はあんまり考えていなかったことがありました。

以上で発表を終わります。本日はありがとうございました。

LINEではKafkaの専門チームがいるから安心して運用できる

司会者:どうもありがとうございました! 質問が来ていて、そちらにちょっとお答えいただきたいなと思っています。「貴社ではMessage QueueとしてKafkaをよく使うということですけども、RabbitMQなどほかのMessage Queueを使わない理由を教えていただけませんか?」ということですが。こちら、いかがでしょうか?

松田:我々が、とくにこの案件でKafkaを使った理由は、1つ目はやはり社内で専用チームがいたので、Availabilityもパフォーマンスも最高のステータスだったということと、もう1つが、今回のように情報を発信するファンアウト、つまり1個のメッセージをいろんなサービスに届けるところに、非常にマッチしたというのがあります。

これは、RabbitMQでもできるかもしれないですが、同じサービスの複数のサーバがあったときに、1回更新通知を送れば、それぞれのサービスに届くというのが1つと、あとはKafka上にデータが永続的に保存されるので、サービスがダウンしたような状態であっても、あとから再度つなぎにいけば、そのサービスはダウン中の更新通知を一気に受け取れるというようなことがあります。Message Queue 側の接続設定を変更することなく連携サービス(Consumer Group)や、各サービスのサーバー台数を増減することができるのは非常に便利です

司会者:ありがとうございます。次の質問です。インフラの質問になるのですが、マネージドサービスは使ったり検討したりすることはあるのでしょうか。例えばキューイングだとAmazonSQSなどです。

松田:まずLINEのサーバーはオンプレミスなので、(そこだけ)AWSを使うという選択肢がありません。AWS であれば、AmazonSQS や AWS Kafka も検討にあがったかもしれません。

司会者:ありがとうございました。