RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

RabbitMQのQuorum Queue

f:id:tech-rakus:20210129191630p:plain

皆様お初にお目にかかります。楽楽勤怠開発課のy_konnoと申します。2020年7月に入社したあまりフレッシュではない新入りです。

入社してからMQに関して取り扱う機会が多いのですが、ラクスでMQというとRabbitMQがスタンダードになりつつあります。つい先日(1月22日)、RabbitMQ 3.8.11がリリースされ、なんともタイミングが良いので触れてみようと思います。

ちなみに3.8.9が2020年9月28日のリリースなので、約4ヶ月ぶりのバージョンアップになります。今回は文献のあまり多くないうえにアップデートのあったQuorum Queueについて触れてみます。

Quorum Queue以前のクラスタリング

RabbitMQでクラスタ構成を組む場合、ExchangeやBindingなどは各ノードへよしなに分散化してくれますが、Queueについては明示的に設定をしなければなにもしてくれません。Queueについての設定をしないままクラスタを組んで運用を始めてしまい、マスターノードが死んだ場合、そのままメッセージがロスするという恐ろしい事態が発生します。

バージョン3.8以前ではMirrored QueueがQueueの分散化について用意された唯一の選択肢でした。

f:id:y_konno_rakus:20210128164735p:plain
Mirrored Queue

Mirrored QueueはマスターのQueueでクライアントからのコマンド(Write、ACKなど)を処理し、ミラーリングされたQueueに対してデータをレプリケーションしていきます。もし、マスターが何らかの原因で落ちた場合は、ミラーのいずれかがマスターとしてプロモーションすることでサービスを維持します。

このようにMirrored Queueはクラスタ内の各ノードにQueueをミラーリングすることで高可用性をもたらす機構ですが、データの一貫性については問題がある設計になっており、状況次第ではデータロスが生じる可能性が高まる危険があります。

1.ノード再起動時のミラーメッセージ消失

Mirrored Queueの1つ目の大きな問題は、あるノードが再起動した場合ミラーにあったデータ内容はすべて消去されるという挙動です。何らかの問題が生じたノードが再起動すると、ミラーにはデータが一切ないため、マスターノードからデータをすべて同期し直す必要があります。

これだけ見ると大して深刻な問題が起きそうにも見えませんが、このマスターからの同期がさらなる問題を引き起こします。

2.同期によるブロッキング

第2の問題はミラーへのデータ同期がブロッキングで行われる点です。同期中は当該Queueへのメッセージ送受信がすべてブロックされるため、動機が完了するまでそのQueueは一切使えなくなります。

一般にRabbitMQが健全な運用状態であれば、Queueにメッセージが溜まった瞬間にConsumeされることがほとんどです。このため、Queueの状態はごく少数のメッセージが存在するか、もしくは空の状態になっていることがほとんどのはずです。しかし、何らかの問題でConsume側の処理停滞し、Queueに大量のメッセージが滞留した状態で同期によるブロッキングが発生すると、長時間に渡って該当のQueueが利用不能になってしまいます。ひどい場合には同期に膨大な時間とノードのリソースを消費した挙げ句に再度ノードがダウンするようなケースもありえます。こうなってしまうと、ミラーの同期を諦めるという選択をせざるを得なくなります。もしミラーの同期をしない場合は、再起動後から蓄積された新規のメッセージについてはレプリケーションがされるものの、既存のメッセージについては同期しないため、データロスが発生する可能性が高まります。

MQで扱うメッセージが損失してしまうことが望ましくないシステムでは上記の事象は大きな問題となるでしょう。また、仮にメッセージ損失が許容できるシステムであったとしても、同期によるブロッキングが長時間に及んだ場合は全く影響を受けないわけではありません。

Quorum Queueの特徴

Quorum Queueの概要

Quorum QueueはRabbitMQ 3.8.0から搭載された新機能で、Raft Consensus Algorithmを利用した高可用性・一貫性を実現するQueueです。特にデータの安全性・一貫性を重視されています。

f:id:y_konno_rakus:20210128164808p:plain
Quorum Queue

Quorum Queueのノードは1つのリーダーと複数のフォロワーから構成されます。このうち、クライアントと実際に対話を行うのはリーダーだけです。フォロワーは冗長化のためだけに存在し、万一リーダーが利用不能になった場合にはフォロワーの中から新たなリーダーが選出され、サービス全体のダウンを防ぎます。

Quorum Queueがデータをレプリケーションする仕組み

Quorum Queueではログレプリケーションという仕組みで各フォロワーにデータを連携しています。リーダーはクライアントからのコマンドを受け取ると、リーダーノード内のログに一時的にそのコマンドをコミットし、各フォロワーへログをレプリケーションします。この時点ではクラスタ全体として更新は確定していません。フォロワーはログのレプリケーションがなされると、自己のログにコミットを行い、リーダーへ返答を返します。リーダーはフォロワーからの返答の数をカウントし、フォロワーの過半数のコミットを以てリーダーもコミットし、クラスタ全体での値が一貫して更新されます。

Quorum Queueでのノード障害発生時

Mirrored Queueとは異なり、Quorum Queueではフォロワーノードが再起動しても保持しているデータを破棄しません。データはフォロワーノード上のディスクに書き込まれており、リーダーノードとの差分をレプリケーションでキャッチアップするだけです。

また、レプリケーション自体も非同期で行われます。このためMirrored Queueで発生しうるQueueがブロッキングされてしまう事象ももはや発生しません。

さらに、新規にノードをクラスタに追加した場合にも非同期で粛々とレプリケーションされるので、Mirrored Queueで発生していた問題は生じません。強いて言えばレプリケーションするデータ量が多い場合にはネットワークI/Oがやや増加する問題があることぐらいでしょうか。

Quorum Queueを使う

能書きが長々としてしまいましたが、実際にQuorum Queueを利用するのは非常に簡単です。クライアントにおいてQueue宣言時にパラメータを追加するだけで作成ができます。

以下はJava Clientでの宣言方法です。Channel.queueDeclare()arguments(第5引数)にx-queue-type=quorumを指定して宣言すればQuorum Queueとして作成されます。

Map<String, Object> extraQueueArgs = new HashMap<>();
extraQueueArgs.put("x-queue-type", "quorum");

channel.queueDeclare("test_queue", true, false, false, extraQueueArgs);

簡単ですね。

なお、Quorum Queueを作成すると、Management Plugin上では以下の様に表示されます。

f:id:y_konno_rakus:20210128164642p:plain
Management Console

TypeQuorumになっていることが確認できますね。

また、メッセージの送受信については通常通りでよく、特別な処理は必要ありません。つまり、Channel.basicPublish()Channel.basicConsume()の内容は通常のQueueやMirrored Queueと何ら変わりません。

Quorum Queueの制限・注意点

ここまでQuorum Queueの利点ばかり述べてきましたが、残念なことにいくつかの欠点が存在します。

利用できない機能の存在

Mirrored Queueに比べて、Quorum Queueはサポートされている機能に制限があります。

以下はその一例です。

  • DurableではないQueue
  • Exclusive Queue
  • Queueのサイズ制限
    • drop-headreject-publish(3.8.10から)のみサポート
    • 公式サイト上ではdrop-headのみとの記載になっていますが、記述が古いです
  • メッセージごとの永続化
  • メッセージのTTL
  • メッセージ優先度
  • グローバルQoS

全く使えないものもあれば、一部のみサポートがされているものがあったりとバラバラな状態ですね。ただ、3.8.10でQueueのサイズ制限にreject-publishが追加されたり、Consumerの優先度がサポートされるようになったりと、今後は徐々にサポートが拡充されていくのかもしれません。

あまりケースとしては考えにくいですが、すでに構築済みのQueueをQuorum Queueに移行したい場合には、利用している機能がすべてサポートされているかは要確認です。最悪の場合は機能不足で移行できない可能性もありえるでしょう。

常にDurableなQueueとして扱われる

先述の通り、DurableではないQueueはQuorum Queueにおいてサポートされていません(そしておそらく今後もサポートされない可能性が高い)つまりQueueは常に永続化がされます。またメッセージもすべて永続化が強制的にされてしまいます。

メッセージの性質が損失を許容できないようなものであれば何ら問題ありませんが、メッセージの安全性よりもパフォーマンスに重きを置きたいようなユースケースには適していません。このあたりは新しい機能だからといって盲目的に採用するのではなく、採用するシステムに適した選択を心がけたいですね。

ディスクI/O

Quorum Queueはメッセージを強制的に永続化する、すなわちディスクに書き込むため、ディスクI/Oについて注意をする必要があります。インフラ要件的にはなるべくSSDを採用することが求められます。

インフラ要件以外にも注意すべき点があります。それはExchangeでFANOUTを利用すべきでないということです。

FANOUTはすべてのQueueに対してメッセージを配送するExchangeです。Quorum QueueにおいてはQueueの数が増加するとそれだけディスクに書き込むメッセージ数が増加してしまうため、MQ全体に大量のQueueが存在するような場合には膨大なディスク書き込みを発生させることになります。

このため、Quorum QueueにおいてはFANOUTの利用は避けるべきでしょう。

ノードの数

Quorum QueueはRaft Consensus Algorithmを利用しているため、クラスターを構成するノード数に留意する必要があります。これを守らないとQuorum Queueの恩恵を受けられず、可用性・一貫性が損なわれる可能性があります。

とりあえず抑えておくべきは以下の2つです

  • ノード数全体が奇数であること
  • 最低ノード数は3

特に注意が必要なのはノード数が2以下の場合はノードが1台も落ちることが許されなくなる点です。また、偶数にしてしまうと、ネットワークパーティションへの耐性が失われる可能性があります。

また、7個以上のノードでQuorum Queueを構成すると性能が低下(レイテンシが増加)するとされています。これを考えると実用的なノード数が3か5ということになります。

レイテンシ

Quorum Queueでは常にディスク書き込みが発生することと、Raft Consensus Algorithmのログレプリケーションの仕組みの関係上、従来のQueueに比べるとレイテンシが高まる可能性があります。

ただ、Quorum Queueの主目的がデータの安全性としているので、パフォーマンスが若干犠牲になるのは致し方ないところではあります。

おわりに

以上、Quorum Queueの紹介でした。

Quorum Queueは日本語はおろか、英語でも文献が少ないこともあって、まだまだ広まっていない印象を受けます。Quorum Queueを利用すること自体はコード上では非常に簡単にできてしまいますが、性質についてはしっかりと理解した上で採用するかどうかを見極める必要があると思います。

Copyright © RAKUS Co., Ltd. All rights reserved.