分散型メッセージングミドルウェアの詳細比較

本エントリは翻訳リクエストより投稿いただきました。
ありがとうございます!リクエストまだまだお待ちしております!

メッセージキューについて書いている連載の続きとして、今週末は分散型メッセージングを実行するための様々なライブラリを詳細に分析していきたいと思います。今回の分析では、APIの特性、デプロイメントやメンテナンスの容易さ、そしてパフォーマンスの質を含めて2、3種類の異なる側面に着目します。メッセージキューは2つのグループに分類できます。ブローカレス(brokerless)とブローカード(brokered)です。ブローカードなキューはエンドポイント間に何かしらのサーバを挟んでいますが、ブローカレスなメッセージキューは、メッセージ送信の際でも間に何も挾まないP2Pです。

今回分析するのは以下のシステムです。

ブローカレス
nanomsg
ZeroMQ

ブローカード
ActiveMQ
gnatsd
Kafka
Kestrel
NATS
NSQ
RabbitMQ
Redis

取り掛かりとして、ほぼ間違いなく誰もが最も気にかける、パフォーマンスメトリクスを見てみましょう。2種類のキーメトリクスを測定しました。スループットとレイテンシです。テストは全てMacBook Pro 2.6 GHz i7, 16GB RAM上で行い、一人の生産者と一人の消費者を使ったPublish/Subscribeのトポロジで評価しています。こうすることで、いいベースラインができました。拡大したトポロジでベンチマークを設定するのも面白いのですが、もっと大がかりな設備が必要となります。

ベンチマークを設定する際のコードは、GitHubで入手できるGoで記述しました。以下に示す結果については、良い結果を出すために最適化されていることもあり得るので、絶対に正しいとは思わないでください。プルリクエストは歓迎です。

スループットのベンチマーク

スループットはシステムが処理できる1秒当たりのメッセージ数のことですが、ここで注意してほしいのは、キューに関連するスループットは1方向ではないということです。メッセージは2つの異なるエンドポイント間で送信されます。そのため、”送信”のスループットと”受信”のスループットがあるわけです。つまり、1秒間に送信されるメッセージ数と、1秒間に受信するメッセージ数の両方を見る必要があります。

今回のテストは、1KBのメッセージを100万個送信し、両方のエンドポイントにて送信および受信するのにかかる時間を測定しました。パフォーマンステストでは、100~500バイトほどの小さなサイズのメッセージが使われる傾向があります。私は今回1KBのメッセージを使いました。ケースバイケースで違いがあるにしろ、これが実際のプロダクション環境で目にすることの多いサイズだからです。メッセージ指向ミドルウェアシステムで使われたブローカは、1つだけでした。多くの場合、クラスタ化された環境では、もっと良い結果を出せるでしょう。

当然、送信側のスループットの方が高くなります。しかし、興味深いのが、送信側対受信側のスループット比の差です。ZeroMQは1秒間に500万以上のメッセージを送信することができますが、1秒間に60万メッセージしか受信できません。それに対して、nanomsgは1秒間に300万に満たないぐらいのメッセージしか送信できませんが、1秒間に約200万メッセージを受信することができます。

では、ブローカードのメッセージキューを見てみましょう。

直感的に、ブローカードのメッセージキューは、ほとんどのシステムにおいて、ブローカレスに比べて2桁ほど数字が小さく、劇的にスループットが低いということが分かりますね。ブローカードのメッセージキューのうち半数は、1秒間に2万5,000メッセージ未満というスループットです。ただし、Redisの値は少々誤解を招く恐れがあるかもしれません。Redisは、Publish/Subscribe機能があるにもかかわらず、強固なメッセージキューとして運用するようにデザインされているわけではありません。ZeroMQと同じような方法で、Redisは低速のクライアントを切断します。これは重要なことですが、Redisはメッセージを確実に処理できているわけではないということに注目しましょう。そのようなわけで、Redisの値は外れ値として考えます。NATSとKafkaにはRedisと同じような特徴がありますが、断続的な障害を起こさずに、メッセージを確実に処理することができていました。GoによるNATSとgnastsdの実装においては、ブローカードのメッセージキューとしては、例外的なスループットが出ています。

外れ値は別にして、ブローカードのキューは、送受信側でほぼ同一のスループットが出たことが分かります。ブローカレスのライブラリとは違い、送信側と受信側のスループットの値は非常に近く、スループットの差はほとんどありません。

レイテンシのベンチマーク

2つ目のキーとなるパフォーマンスメトリクスはメッセージレイテンシです。エンドポイント間でメッセージが送られるのにどれくらいの時間がかかるかということを測定します。直感では、これはスループットに反比例するものであると思えるかもしれません。つまり、1秒で処理できるメッセージの数をスループットだとすれば、1メッセージを処理するのにかかる秒数がレイテンシであるといったところです。しかし、ZeroMQのホワイトペーパーから持ってきたイメージをよく見ると、反比例するとは言えないようです。

現実には、回線で送られる1メッセージ当たりのレイテンシは均一ではありません。それぞれで大きく異なるものです。実際、レイテンシとスループット間の関係はもう少し込み入っています。しかしながら、レイテンシはスループットとは違って、送信側や受信側といった区切りではなく、どちらかというと全体として測られるものです。とは言っても、それぞれのメッセージについてレイテンシはあるので、全てのメッセージから算出した平均値を見ることにしましょう。さらに、送信メッセージ数を変えた時に、メッセージレイテンシの平均値がどのように上下するのかといった関係性も見てみたいと思います。繰り返しになりますが、直感的にはメッセージの数が増えるとキューイングされる数も増え、レイテンシが大きくなるであろうと推測できます。

前と同様に、ブローカレスのシステムの方から見てみましょう。

一般的には、システムに送られるメッセージの数が増えると、それぞれのメッセージのレイテンシが大きくなるといった先ほどの推測は合っているようです。興味深いのは、50万メッセージでレイテンシが大きくなる割合が減り、その減ったままの割合で100万メッセージに向かっているということです。もう1つ興味深いことがあります。レイテンシの最初の山が1,000~5,000メッセージの間にあることです。この傾向はnanomsgの方でよりハッキリと見えますね。因果関係を正確に示すのは難しいのですが、このような変化は、メッセージの処理方法によるのかもしれませんし、他のネットワークスタックの走査最適化がそれぞれのライブラリで実装されていることによるのかもしれません。データポイントを増やすことで、可視性は上がりそうです。

ブローカードのキューでも同じようなパターンが見えますし、新たな興味深い点も出てきました。

Redisは、先ほど見たグラフに似ています。レイテンシの最初の山が出現した後に、急に増加の勢いが衰えるといったグラフです。違いは、5,000メッセージを超えた直後に基本的に一定の値になっているという点でしょうか。NSQでは、そのようなレイテンシの山や性質は見られず、おおよそ直線になっています。Kestrelでは推測どおりの結果が出ました。

NATSとgnatsdは、ほとんどグラフ上に現れていないということに注目してください。驚くほど低いレイテンシで動作し、メッセージの数との間に思いもよらない関係性を示しています。

意外なことに、NATSとgnatsdのメッセージレイテンシは一定の値を示しました。これは私たちの直感に反した結果で、推測とは異なります。

お気づきかもしれませんが、Kafka、ActiveMQ、そしてRabbitMQは上記のグラフには出てきていません。これらのシステムのレイテンシが他のブローカードのメッセージキューよりも桁違いに大きい傾向にあるというのが理由です。そのため、ActiveMQとRabbitMQは独自のAMQPカテゴリに分けられています。Kafkaはこの2つと同じような数字が出ていたので、AMQPにKafkaを含めてグラフにしてみました。

RabbitMQのレイテンシは一定ですが、ActiveMQとKafkaは線形になりました。よく分からないのは、スループットと平均レイテンシの間に明らかな食い違いがあるということでしょうか。

定性分析

それでは、各ライブラリのパフォーマンスの違いについて、実験に基づいたデータを見たところで、実際的な観点から見た動きを調べてみましょう。メッセージスループットやスピードは重要ですが、そのライブラリの利用やデプロイやメンテナンスが難しいとなると、実用的ではありません。

ZeroMQとnanomsg

厳密に言えば、nanomsgはメッセージキューではなく、分散型メッセージングを様々な使い勝手の良い方式を通して実行する、ソケットスタイルのライブラリです。そのため、ライブラリ自身をアプリケーションに埋め込むということの他に、デプロイするものは何もありません。そのため、デプロイメントに関する問題はないと言えるでしょう。

nanomsgはZeroMQを作ったメンバのうちの1人が書いたライブラリです。私が以前説明したように、両者の動きは非常に似ています。開発の観点から見ると、nanomsgは全体的にきれいなAPIを提供しています。ZeroMQとは違い、ソケットの接続に関する概念がありません。さらに、nanomsgは接続可能なトランスポートプロトコルとメッセージングプロトコルを提供しており、拡張しやすくなっています。追加のビルトインスケーラビリティプロトコルも非常に魅力的です。

ZeroMQのように、nanomsgもメッセージがアトミックに無傷な状態でデリバリされ、順序付けされることは保証しますが、デリバリそのものは保証しません。分割されたメッセージはデリバリされませんし、中には全くデリバリされないメッセージもあります。nanomsgを書いたMartin Sustrikはこれについて非常に明快に語っています。

デリバリ保証なんて神話のようなもので、100%保証されたものなどないのが私たちの住む世界です。代わりに私たちがするべきことは、失敗やダメージが起こりそうな局面において、回復力のあるインターネットのようなシステムを構築することです。

この哲学は、トポロジの組み合わせを利用して、最大限の努力によってそうした保証を付加する回復力のあるシステムを構築するものです。

一方で、nanomsgはまだベータ版で、正式にリリースできる段階とは見なせないかもしれません。結果として、利用できるリソースはあまりなく、開発者のコミュニティも活発ではありません。

ZeroMQは2007年頃から多くの試練を乗り越えてきたメッセージングライブラリです。nanomsgの前身と見なす人もいますが、ZeroMQ にはあってnanomsgに欠けているものがあります。それは、活発な開発者のコミュニティと豊富なリソースと教材です。多くの人にとって、高速の非同期分散メッセージングシステムを構築する上で重要なのは、そうしたデファクトツールです。

nanomsgと同様、ZeroMQはメッセージ指向ミドルウェアではなく、単にソケット抽象として動作します。ユーザビリティの点においては、APIが少し込み入っていますがnanomsgとほぼ同じです。

ActiveMQとRabbitMQ

ActiveMQRabbitMQはAMQPの実装です。メッセージのデリバリを確実にするブローカとしての役割で、継続的デリバリと非継続的デリバリの両方をサポートします。デフォルトではメッセージはディスクに書き込まれていて、ブローカの再起動を免れるようになっています。また、メッセージの同期送信と非同期送信の両方をサポートします。しかし前者の場合はレイテンシに大きな影響が出ます。デリバリを保証するために、これらのブローカはメッセージ確認機能を使用しますが、ここでも膨大なレイテンシというペナルティが発生します。

アベイラビリティとフォルトトレランスに関する限り、ブローカは共有ストレージもしくはシェアードナッシングによってクラスタリングをサポートします。キューはクラスタ化されたノードを共有することで複製でき、単一点障害やメッセージの損失を防ぐことができます。

AMQPは、AMQPのクリエータ自身らが過度に設計されすぎだと主張するほど、非凡なプロトコルです。付加されたデリバリ保証は、大きく複雑な問題やパフォーマンスを犠牲にして実現されています。根本的に、クライアントの方が実装したり使用したりするのが難しいのです。

ActiveMQとRabbitMQはメッセージブローカなので、分散システムの中で処理されるべき付加的な動的部分であり、そこにデプロイメントとメンテナンスのコストが発生します。残りのメッセージキューについても同じことが言えます。

NATSとGnatsd

gnatsdNATSメッセージングシステムの純粋なGo実装です。NATSはあまり企業向きではなく、より軽めであると見直されている分散型メッセージングシステムです(ActiveMQやRabbitMQなどのシステムとは対照的です)。gnatsdを書き、過去にTIBCOでアーキテクトをしていたApcera のCEOであるDerek Collison は、NATSを企業向けのメッセージキューというよりも、「より神経質なシステム」と語りました。持続性がなくメッセージ処理もしませんが、高速で使いやすいのが特徴です。クラスタリングがサポートされているのでクラスタリング環境を構築可能で、高いアベイラビリティとフェイルオーバが見込め、クライアントを分割できます。残念なことに、gnatsdではTLSやSSLはまだサポートされていませんが、将来的にはサポートされる予定です(Ruby NATSではサポートされています)。

既に見てきたように、gnatsdはNATSの最初のRuby実装よりもはるかにいいパフォーマンスをし、クライアントはNATSとgnatsdとを交互に使用できます。

Kafka

もともとLinkedInによって開発されたKafkaは、分散コミットログを通してPublish/Subscribe型メッセージングを実装します。大量のクライアントによって消費(受信)できるクラスタとして動作するようデザインされているのです。 ZooKeeperを使用することでスケールアウトが楽にできるので、消費者やブローカをシームレスに追加導入できます。また、クラスタのバランス調整も透過的に行ってくれます。

Kafkaはブローカにメッセージを格納するために永続的なコミットログを使用します。通常、消費した持続メッセージを除去するその他の永続的なキューとは異なり、Kafkaは設定したある一定期間はメッセージを保持します。つまり、メッセージは消費者側で失敗があった場合でも”リプレイ”できるのです。

ZooKeeperによってKafkaクラスタの処理が比較的簡単になりますが、維持すべき他の要素はまだ導入されていません。ですが、KafkaはAPIが充実しており、Shopifyは
SaramaというKafkaとのインターフェース接続がとても便利なGoクライアントを導入しています。

Kestrel

KestrelはTwitterによってオープンソース化された分散メッセージキューです。高速で軽量のため、クラスタリングやフェイルオーバといった概念がありません。KafkaがZooKeeperによって一から構築されクラスタ化される一方で、メッセージ分配の負担はKestrelのクライアント側に強いられます。ノード間で相互コミュニケーションもありません。そこでKestrelはシンプルという名のもとでバランスを図っています。Thriftやmemcachedプロトコルで利用できる一方で、永続的なキュー、アイテム期限、トランザクションリード、ファンアウトキューが特徴です。

Kestrelはコンパクトにデザインされていますが、強固なメッセージングシステムを構築するためには開発者がより多くの作業をする必要があることを意味しています。Kafkaの方が”オールインワン”ソリューションのように見えますね。

NSQ

NSQはBitlyが構築したメッセージングプラットフォームです。プラットフォームと言ったのは、リアルタイムの分散型メッセージングに、より役立つよう構築されたツールがNSQ周辺にはたくさんあるからです。メッセージを受信し、格納し、クライアントに送り届けるデーモンをnsqdといいます。デーモンはスタンドアロンでも動作しますが、NSQは分散型トポロジとして動作するようデザインされていて、nsqlookupdという別のデーモンを活用します。nsqlookupdはnsqdインスタンスのためのサービスを検出する仕組みです。NSQはまた、nsqadminというリアルタイムのクラスタ統計を表示し、キューの削除やトピックの処理など様々な管理業務を実行する役割を果たすWeb UIも提供します。

デフォルトではNQS内のメッセージは永続的ではありません。本来、インメモリ・メッセージキューになるようデザインされていますが、ある時点でメッセージをディスクに書き込めるようキューのサイズを構成できます。にもかかわらずビルトインの複製は行われません。NSQはメッセージのデリバリを保証するのに確認機能を使いますが、デリバリの順番は保証しないのです。メッセージが1回以上、送り届けられることもあり得るので、重複の実行を防ぐようにするのは開発者の責任になります。

Kafkaと同じように、追加のノードはNSQクラスタにシームレスに増設できます。またHTTPのAPIやTCP APIも充実しているので、メッセージをシステムに入れるためのクライアントライブラリも不要です。あらゆる動的部分があるにもかかわらず実際にデプロイするのは非常に簡単です。APIも使いやすく、利用できるクライアントライブラリもたくさんあります。

Redis

最後に紹介するのはRedisです。Redisは軽量のメッセージングと一時的なストレージに最適ですが、分散型メッセージングシステムのバックボーンとしての使用は推奨できません。Publish/Subscribeは速いのですが、機能が限られているのです。強固なシステムを構築するのには大変な作業が必要でしょう。既に述べてきたような、よりよいソリューションは他にもありますし、Redisにはスケーリングに関する懸念もあります。

こうした問題はありますが、Redisは使いやすく、デプロイしやすく、管理も簡単です。また比較的、省スペースでもあります。使用するケースによっては、以前ご説明したように、リアルタイムメッセージングには最適な選択といえます。

まとめ

今回の分析は、”優勝者”を決めるのが目的ではなく、分散型メッセージングの異なるオプションを複数紹介するためのものです。必要に応じてオプションも変わってくるので、”万能の”オプションなどありません。高速で1方向メッセージであることが必要なケースもあれば、デリバリの保証が求められるケースもあります。実際は、多くのシステムでその両方の組み合わせが要求されます。今回の分析によって、個々の問題に対してどのソリューションが最適なのかという理解が深まり、皆さんの今後の賢明な判断に役立てれば幸いです。