Sansanを支えるスケーラブルなメッセージング基盤

加畑博也氏(以下、加畑):こんばんは。よろしくお願いします。Sansanの基盤チームでエンジニアをやっています。

本日は、「メッセージング基盤を用いたスケーラブルなアプリケーション開発」というタイトルでお話しさせていただきます。

Sansanという会社については、たぶんテレビCMとか電車内のCMなどで知っていただいている方も多いのではないかなと思います。法人向け・個人向けと大きく2つのサービスを提供しておりまして、どちらも名刺関連のサービスです。今回は、法人向けの『Sansan』というサービスについてのお話になります。「名刺を企業の資産に変える」というコンセプトで、数名規模から大手まで、約7,000社に導入いただいております。最近だと、数万人規模の大きな企業様にもご利用いただいている状況です。

サービスをいろいろな人に使ってもらえるとうれしいです。私が入社したのは3年ほど前ですが、そのころと比べるとユーザの数も増え、サービスの成長に携わることができ、非常に嬉しく思っています。反面、さまざまな課題が浮き彫りになってきています。そこで今回は、スケーラビリティとメッセージングについてお話させていただこうと思います。実際にSansanで使っているメッセージング基盤の構築と、その活用についてお話しします。

スケーラビリティとメッセージング

それでは1つ目、スケーラビリティとメッセージングについてです。最初にスケーラビリティとはなんぞや、という定義からお話しします。CMUの研究者の人がシステムスケーラビリティについてのテクニカルレポートをまとめています、その中で、このように定義しています。雑に訳すと、「システムのキャパシティを拡大するために、コストパフォーマンスの良い戦略を繰り返し適用することで、負荷の増大に対応する能力」みたいなかんじです。

これはまた別の人ですが、ワーナー・ヴォゲルスというAmazon.comのCTOの方が、「こんなサービスはスケーラビリティだ」という定義を示しています。「システムのリソースを投入することによって、投入したリソースに比例してパフォーマンスを向上するようなサービス」です。効率のいい戦略をリソースの導入と言ったり、負荷への対応をパフォーマンスの向上と言ったり、先ほどよりも少し具体的な定義になっています。

これはさらに別の人で、この人は、リソースをどこに投入するかによってスケーラビリティを定義しています。1つ目は「Vertical Scalability」、垂直スケーラビリティと呼ばれますが、1つの論理ユニットの中にリソースを投入することでキャパシティを上げる、というものです。もう1つが水平スケーラビリティと言われますが、これは複数の論理ユニットを組み合わせて1つのユニットとして動かすことでキャパシティを上げる、というものです。

また、性能の観点でもスケーラビリティを分類しています。投入したリソースに比例してLinearに性能が上がるものを『Linear Scalability』と言って、これが理想的な状態です。でも実際はオーバーヘッドがあったりして、sub-Linearになりますよね、みたいなことが載っています。

水平かつ線形なスケーラビリティは「困難」ですが、垂直かつ線形なスケーラビリティは「不可能」です、と言っています。なので、システム開発において目指すべきところは、線形なスケーラビリティなわけですけれども、垂直にそれを実現することは原理的に不可能ということで、水平方向の拡張によって、いかに損失を出さずにスケーラブルなシステムを作るかが課題になってくるという話です。

どういうことかというと、ある処理をする必要があるとなったときに、それを複数の処理を実行するユニットに振り分けて、それぞれで並列に実行するということを考えます。これをするとリソース投入のところでは、先ほど言ったように、処理実行ユニットを追加していくことによって、原理的には線形なスケーラビリティが実現できます。

メッセージングとは何か?

 

それを実現していくための方法として、「メッセージング」という仕組みがあります。『Enterprise Integration Patterns』という本の中で、メッセージングというアーキテクチャパターンが紹介されています。

Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions (Addison-Wesley Signature Series (Fowler))

最もベーシックな仕組みがこの図です。アプリケーションAがイベントあるいは情報をMessage Busに流して、それを他のアプリケーションにとどけるというものです。

ここにも書いてあるように、メッセージングというアーキテクチャパターンは「情報の共有をアプリケーション間でいかに行うか」という課題を解決するためのものです。なので、もともとスケーラビリティを本来の目的としたパターンではないのですが、これを活用することによって先ほど言ったような水平なスケーラビリティを実現できます。

これが全体像です。細かく説明はしませんが、アプリケーションAがアプリケーションBに対して情報を共有するときにいくつかの要素があり、まずアプリケーションAからメッセージングシステムへのエンドポイントがあります。メッセージというのは情報をラップしたものと考えてただければと思うのですが、メッセージをチャンネルという論理的な経路に流してアプリケーションBに届けます。

その過程で、もし必要であればアプリケーションが複数ある場合やチャンネルが複数ある場合にルーターを使うので、ルーターという要素があったり、アプリケーションAアプリケーションBの間でメッセージのフォーマットが異なる場合は途中トランスレーターという要素をかませたりしなければいけません。

この本では65パターンくらい紹介されています。ここまでが大体スケーラビリティとメッセージングの関係と概要になります。

メッセージングシステムの作り方

ここから、これを実際にどう作っていくかというお話です。これは先ほどの図の再掲ですが、処理が発生したときにばらしたりということです。そして、これを実現するのか、軽くはしりだけお話しすると、アプリケーションAとアプリケーションBは処理を共有したいということで、何をやってほしいのかをメッセージに含めてチャンネルに流します。

そして、処理内容を伝えたいというメッセージを送ります。これを「Command Message」と言ったりします。チャンネルもいくつかのパターンがあります。例えば「1対1」か「1対多」かによって設計が変わってきます。

いまSansanの中で使っているメッセージシステムの構成は、ざっくりこんなかんじです。AWSがでてきているので薄々わかるかと思いますが、Azureとかは一切出てこないので、そのあたりは同様のサービスで置き換えながら聞いていただければと思います。

メッセージングアーキテクチャパターンでいうところの「Channnel」と呼ばれる要素には、AmazonのSQSというサービスを使っています。そこにメッセージを投入するアプリケーションは、Webサービスやスマホアプリ、バッチ、あるいは外部のサービスも含みます。それをメッセージングサーバと呼ばれるバッチが定期的にポーリングし、メッセージを受信したらワーカースレッドを立ち上げて処理しています。

Sansanでの活用事例

ここからは、実際にこれをどういう風に使っているのかをお話したいと思います。1つ目の例ですが、長期間ログインしていないユーザに対して、「最近ログインしていませんね」というメールを送ってログインを促進したい、というケースがよくあると思います。

そんなとき、従来のやり方だと、一番簡単なのはバッチでブンブン回します。ここでやっているようにユーザの一覧をとってきて、順次メールを送信するやり方です。ですが、これをやった場合、処理時間がユーザー数に依存するし、配信メール数に依存します。そのため、たとえば朝の7時からメールを送りはじめたのに、届くのは昼過ぎ、というように、ユーザーが増えていくとそれに応じてバッチの実行時間が伸びていきます。

最初に説明したスケーラビリティの観点に立ち戻ると、このメール配信システムに対してリソースを投入する場合にできることは、バッチサーバーのCPUを上げてあげるみたいな、いわゆる垂直なスケーラビリティしか確保されておらず、スケーラブルなデザインにはなっていません。

そこで、メッセージングサーバを使います。こんなかんじになっています。AWSのCloudWatch Eventsというサービスで、これはcron的なことができるんですが、ある特定時刻にメッセージをSQSに投げ込み、それをメッセージングサーバが拾ってユーザ単位に分割する、という処理をします。

そして新たにメッセージをユーザごとに作って、チャンネルに投入します。それらを拾って、ユーザ単位にメールを送信するっていうところを並列処理します。

より具体的に説明すると、CloudWatch Eventsは、任意のテキストをSQSにメッセージとして投げることができます。jsonでクラス名を明記しておき、メッセージングサーバはjsonをデシリアライズしてクラスとマッピングすることで、任意のメッセージを実行します。

この図で説明してるのはやったのは先ほどのバッチでやっていたことと同じで、ユーザの一覧を取得してきてブンブン回すのですが、先ほどと違うのは、メールの送信という重い処理をループの中でやるかわりに、「特定のユーザにメールを送るというメッセージを送信する」という処理をしています。

ここで送信するメッセージがこんなかんじです。ユーザのIDという情報を持っています。それを受けて、今度はメール送信するメッセージを実行して、メールをバンバン送っていく感じになっています。こうすることによって、ユーザーが増えた場合やメールの配信数が増えた場合に、メッセージサーバーの中で並列度を上げるとか、ノード数をあげるみたいなかんじで、制御できます。ということで、先ほど言ったような水平なスケーラビリティを実現することができるようになりました。

また、別な嬉しいこともあります。たとえば送信に失敗した場合、自動的にリトライされるんですが、それでも送れなかった場合はDead Letterという扱いになります。これはなにかというと、Dead Letter Queueという別の特殊なキューにメッセージを投げます。それでキューをポーリングするしたDead Letter monitorという要素を設けます。これはDead Letterがきたら、我々エンジニアに対してアラートを通知しつつ、実行に失敗したメッセージをテキストファイルとして保管します。

なので、もしメール送信に失敗したユーザに対して再送したいと思ったら、そのテキストをキューにもう一度投げてもらえば、勝手に拾ってメールを送ろうとしてくれるので、リカバリもやりやすくなります。

「あらゆるシステムに適用できる万能なサイズは存在しない」

名刺のデータ化でも使っています。うちのサービスでユーザが名刺をスキャナで取り込んでいただいて、その画像を、社内の別組織が作っているデータ化のサービスに投げこみます。そのデータ化のサービスの中で、その画像を機械の照合や人力を使ってテキストにします。

そして、氏名や会社名、メールアドレスなどを抽出して、うちのサービスに返してくれます。それをデータベースに書き込んでユーザーはそれを見れるようになるという流れになります。ただ、書き込みに対してデータベースの負荷がかかります。なので名刺がドバっと入ってきてドバっとデータ化されたらデータベースにすごい負荷がかかります。

ということでメッセージング化することを考えます。データ化サービスはこれまでどおりAPIを叩くんですが、中では直接DBに書き込むのではなくて、キューにメッセージをどんどん投げ込みます。そして、メッセージングサーバのほうは自分のペースで取り出してそれを順次データベースに書き込んでいきます。非同期にすることで、データベースの負荷を制御することができます。メッセージはたとえばこんな感じです。先ほどはIDだけでしたが、会社名や氏名、住所などデータ化された情報をメッセージにします。

また、データベースをメンテしたいとなったときは、これまでだと「サービスから送ってくるのをやめてください」というようなかたちで、データ化を担当する社内の別組織との調整が必要でした。メッセージング化されていれば、メンテしたいときにメッセージングサーバーさえとめておけばデータ化サービスは勝手にキューにメッセージを突っ込み続けるのですが、たまっていっても別によくて、メンテナンスが終了したときにもう一度蓄積されたメッセージを順次取り出して書き込むことができます。

そのほかにも、いろいろなところで使っています。先ほどはデータ化を受けるところだけでしたが、それを依頼するところや画像を投げるところも使います、Mixpanelというほかのサービスへの連携部分でもメッセージングで処理できたりします。最後に雑にまとめると、メッセージング基盤により、スケーラブルになりました。かつ、耐障害性や運用のしやすさも向上しました。

ということで最後にこれは引用ですが、「適切なスケールを考えているけれど、あらゆるシステムに適用できる万能なサイズは存在しない。そんなこと言っている人は信じるな」と。みなさんサービスを運営されている方も多いかと思うので、そのあたりを考えながら設計していただければと思います。以上で終わります。ありがとうございました。