実装(ZeroMQ編)
章の冒頭で述べた通りoslo.messagingでは、アプリケーションコードを変更せずに別のMQを利用して同じ処理を行うことができます。ここでは、使用するMQを「RabbitMQ」から「ZeroMQ」に変更し、先ほどのRPCスクリプトを実行します。
oslo.messagingで利用可能なMQは「RabbitMQ」の他に「Kafka」や「ZeroMQ」などがありますが、デフォルトで選択される「RabbitMQ」が一般的に利用されており「ZeroMQ」などのサブドライバが利用しているパッケージはoslo.messagingの依存パッケージに含まれていません。そのため、「ZeroMQ」ドライバの依存パッケージを別途インストールしてやる必要があります。
以下では「ZeroMQ」ドライバを利用する上で必要な「ZeroMQ」のPythonライブラリ、および「Redis」サーバとライブラリをインストールしています。
vagrant@vagrant:~$ sudo apt-get install -y redis-server libzmq-dev
vagrant@vagrant:~$ sudo pip install pyzmq redis
ここで「ZeroMQ」ドライバに「Redis」が必要な理由を説明します。
「ZeroMQ」ドライバでは「Matchmaker」と呼ばれる仕組みによって、動的に追加されたホストに対する「topic」通信が行えるようになっています。
なぜこうした仕組みが必要なのかについて理解するために、「ZeroMQ」と「RabbitMQ」の構造の違いについて簡単に解説します。
以下は「RabbitMQ」を利用したプロセス間のメッセージ転送処理を表した図になります。「RabbitMQ」には"ブローカー(Broker)"と呼ばれるメッセージを仲介する存在があります。メッセージを送信するプロセス(Publisher)はブローカーに対して送信することで、メッセージを受信するプロセス(Subscriber)がどこにどれだけ存在するかについて意識しなくてよくなります。
この仕組みによって、Subscriberを動的に追加(あるいは削除)した場合でもPublisherは何も意識(コード修正や設定変更)せずにメッセージ転送処理を継続させることができます。
Brokered MQのアーキテクチャ(出典:ツチノコブログ)
これに対して「ZeroMQ」では、以下の図で示すようにBrokerが存在せず、直接PublisherからSubscriberに対してメッセージを送る構造になっています。これによって、メッセージを低レイテンシで、かつ少ないトラフィック量でPublisherからSubscriberに送ることができます。しかし、その反面、動的にSubscriberが追加(あるいは削除)された場合、その都度Publisher側において送信先の設定変更を行う必要があります。
Broker-less MQのアーキテクチャ(出典:ツチノコブログ)
そこでoslo.messagingの「ZeroMQ」ドライバでは、「Matchmaker」という仕組みによってこの問題に対処しています。以下は「Matchmaker」を利用したRPC Server/Clientのアーキテクチャと処理の流れを表した図になります。「Matchmaker」はRPCサーバのホスト情報を保持するハッシュテーブルを持っており、RPCサーバが起動した際に「Matchmaker」を通してサーバのホスト名とポート番号が登録されます。RPC Clientは「Matchmaker」を通じてこの情報を参照することで、RPC Serverが動的に追加(あるいは削除)された場合においてもリクエストを問題なく送ることができます。
また「Matchmaker」自体がプラグイン機構(Pluggable)になっており、多様なデータストアに対応が可能です。ただ本稿執筆時点で利用可能なプラグインが「Redis」向けのプラグインしかないため、「Redis」サーバと「Redis」にアクセスするためのPythonパッケージをインストールしました。
では実際に「ZeroMQ」ドライバを利用して、先ほどのRPC server/clientのサンプルを実行します。念のため「RabbitMQ」を使わずにRPCが行えることを確認するために「RabbitMQ」を停止させます。
vagrant@vagrant:~$ sudo service rabbitmq-server stop
まず「ZeroMQ」ドライバを使ってサーバを起動させます。「ZeroMQ」ドライバを使う場合には、次のように--config-fileパラメータに「ZeroMQ」ドライバを使用する設定を記述したファイル「config/driver_zmq.conf」を指定します。
vagrant@vagrant:~/oslo-messaging-examples$ src/rpc_server.py --config-file config/driver_zmq.conf
次に「ZeroMQ」ドライバを使ってクライアントからサーバに対してRPCを実行します。クライアントも同様に--config-fileパラメータに「ZeroMQ」ドライバ用設定ファイル「config/driver_zmq.conf」を指定します。
vagrant@vagrant:~/oslo-messaging-examples$ src/rpc_client.py --config-file config/driver_zmq.conf 20 vagrant@vagrant:~/oslo-messaging-examples$
コード修正を行わず、設定の変更だけで「RabbitMQ」の場合と同様の結果が得られることを確認できました。
OpenStack(Nova)の利用例
ここで紹介したoslo.messagingのRPC機能の使いどころを掴むため、osloを最も利用しているOpenStackがどのように利用しているかを見ていきます。
RPCはその名のとおりプロセス間通信を実現する手法の一つですが、OpenStackではRPC以外にもREST APIによるプロセス間通信の仕組みを提供しています。OpenStackではこれら2つの手法をどのように使い分けているのでしょうか。
それを理解するために、OpenStackを構成する「コンポーネント」と「サービスプロセス」の関係について簡単に整理します。
以下の図は一般的なOpenStackのアーキテクチャを表した図になります。OpenStackは、図の点線の四角形で囲まれた複数の「コンポーネント」と呼ばれるプロジェクトの集合から成り立っています。図中の「OpenStack Dashboard」や「OpenStack Orchestration」がそれぞれコンポーネントを表しています。各コンポーネントはそれぞれ独立したチームによって開発が進められており、これらが有機的に組み合わさって一つのOpenStackが成立しています。
OpenStackのアーキテクチャ(出典:OpenStack Docs/Logical architecture)
そして各コンポーネント内部には、実線の角丸の四角形で囲まれた複数の「サービスプロセス」が動作しています。図中の上段中央のコンポーネント「OpenStack Orchestration」において、「heat-api」と「heat-api-cfn」、そして「heat-engine」がそれぞれサービスプロセスを表しています。
以降ではこれら「コンポーネント」と「サービスプロセス」において、RPCとRESTAPIをどのように使い分けているかについて見ていきます。
まずはRPCのユースケースを見ていきます。以下はNovaコンポーネント内部におけるoslo.messagingによるRPCの参照関係を表した図になります。nova-network、nova-conductor、nova-scheduler、そしてnova-computeのサービスプロセスにおいてRPCサーバが起動しており、実線で結ばれたサービスプロセス間でRPCが行われています。
続いてREST APIのユースケースを見ていきます。冒頭で示したOpenStackのアーキテクチャの図において、各コンポーネントがそれぞれREST APIのリクエストを処理するサービスプロセス(APIサーバ)を持っていることが分かります。コンポーネント間のやりとりはすべてAPIサーバを介して行われています。図中の太線で示されたサービスプロセスがAPIサーバになります。
このようにOpenStackでは、コンポーネント間の通信にはREST APIを利用し、コンポーネント内の通信にはRPCを利用するといった使い分けをしています。
こうした使い分けはとても合理的に思えます。なぜならば、もしOpenStackがコンポーネント間の通信もRPCで行う設計になっていたとしたらどうなるか想像してみてください。あるRPCサーバに登録されているメソッドがすべてのコンポーネントから参照され、あるコンポーネントのたった一つのインターフェースの変更がOpenStack全体に波及することになります。その場合OpenStackほど大規模なシステムの維持が途端に難しくなることが分かります。
