unoh.github.com

Q4Mを触ってみる

Thu Dec 17 02:19:35 -0800 2009

yukiです。そろそろクリスマスですね。みんな浮かれていればいいと思います!最近急に目が悪くなって、ツリーの赤色電球と居酒屋の赤提灯の色が判別出来なくなってきました。嘘です。

今回は、みんな大好きメッセージキュー、Q4Mを触ってみた感想を今更ながらレポートします。

Q4M

公式ページはこちらhttp://q4m.31tools.com/

Q4Mはサイボウズラボの奥 一穂氏が開発されており、MySQLの5.1以上でストレージエンジンとして利用できるメッセージキューで、MySQLプラグインとしてGPLライセンスで配布されております。

特長

MySQLのストレージエンジンとして利用できるので、テーブル作成時にストレージエンジンを指定するだけで利用できます。

CREATE TABLE hoge (
     ...
 ) ENGINE = QUEUE

キューの作成(enqueue)は通常のレコード操作と同様にINSERTで行い、キューの取り出し(dequeue)も同様にSELECTで行えるので、シンプルに扱う事が出来ます。

導入事例

公式ページには導入事例として以下の2つが載っています。

インストール

最新版は0.8.9となっています。こちらからバイナリ・ソース版と用意されているので、適切なものを選んでインストールします。

GCCのバージョンによってはコンパイルに失敗するエラーがありましたが、現バージョンでは解消されて手軽にインストールできるようになりました。

インストールが終わると、MySQLにプラグインとして認識されているのがわかります。

mysql> show plugins;
+------------+----------+----------------+--------------------+---------+
| Name       | Status   | Type           | Library            | License |
+------------+----------+----------------+--------------------+---------+
| binlog     | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| partition  | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| ARCHIVE    | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| BLACKHOLE  | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| CSV        | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| FEDERATED  | DISABLED | STORAGE ENGINE | NULL               | GPL     |
| MEMORY     | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| InnoDB     | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| MyISAM     | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| MRG_MYISAM | ACTIVE   | STORAGE ENGINE | NULL               | GPL     |
| QUEUE      | ACTIVE   | STORAGE ENGINE | libqueue_engine.so | GPL     |
+------------+----------+----------------+--------------------+---------+

実際に動かしてみる

まずはテーブルを作成します。

mysql> CREATE TABLE queue_table (
  message TEXT NOT NULL,
  priority TINYINT UNSIGNED NOT NULL DEFAULT 10
)
ENGINE = QUEUE

次に、実際に処理するキューを作るため、キューをINSERTします。

mysql> INSERT INTO queue_table (message) VALUES ('ms1'), ('ms2') ,('ms3');

dequeueしてみます。dequeueにはqueue_waitというfunctionを利用します。

mysql> SELECT queue_wait('queue_table');
+---------------------------+
| queue_wait('queue_table') |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0.00 sec)

成功すると、結果として1が返って来ます。
キューを取り出しているこの状態をオーナーモードと呼び、この状態だと、該当のテーブルからは1レコードだけ引けるようになります。

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms1     |       10 |
+---------+----------+
1 row in set (0.00 sec)

取り出して処理が完了した場合、queue_endを呼ぶと、取り出したキューが削除されます。同時にオーナーモードが解除され、非オーナーモードに戻ります。

mysql> SELECT queue_end();
+-------------+
| queue_end() |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)

非オーナーモードに戻ったので、通常のSELECTを行うと、残りの全キューを引くことができます。
ただし、別のコネクションでオーナーモードになっている場合、そのコネクションで得たキューは表示されません。(後述。)

mysql> SELECT * FROM queue_table;
 +---------+----------+
 | message | priority |
 +---------+----------+
 | ms2     |       10 |
 | ms3     |       10 |
 +---------+----------+
 2 rows in set (0.00 sec)

またこのとき、処理中に例外エラーなどでキューを戻したい場合は、queue_abort()を利用すると直前に取り出したキューを戻します。queue_wait()→queue_abort()した際の流れです。

mysql> SELECT queue_wait('queue_table');
+---------------------------+
| queue_wait('queue_table') |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
+---------+----------+
1 row in set (0.00 sec)

mysql> SELECT queue_abort();
+---------------+
| queue_abort() |
+---------------+
|             1 |
+---------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
| ms3     |       10 |
+---------+----------+
2 rows in set (0.00 sec)

オーナーモードと非オーナーモード

queue_wait()を呼ぶとオーナーモードに入り、先述したように、そのコネクション内では1行のみ取得できるようになります。
また、別のコネクションでは、他コネクションでオーナーモードで取得した行以外を取得できるようになります。

コネクション1

% mysql -u test test
mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
| ms3     |       10 |
+---------+----------+
2 rows in set (0.01 sec)

コネクション2

% mysql -u test test
mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
| ms3     |       10 |
+---------+----------+
2 rows in set (0.01 sec)

コネクション1でオーナーモードに入る

mysql> SELECT queue_wait('queue_table');
+---------------------------+
| queue_wait('queue_table') |
+---------------------------+
|                         1 |
+---------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
+---------+----------+
1 row in set (0.00 sec)
コネクション2でキューを全件表示すると、コネクション1で引いたキューは表示されない。
mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms3     |       10 |
+---------+----------+
1 row in set (0.00 sec)

優先度別キュー取出

優先度低めのキューテーブルを作り、優先して取得したい順にqueue_waitの引数に加えます。

mysql> CREATE TABLE low_priority_queue (
   message TEXT NOT NULL,
   priority TINYINT UNSIGNED NOT NULL DEFAULT 10
 )
 ENGINE = QUEUE;

mysql> INSERT INTO low_priority_queue (message) VALUES ('lm1'), ('lm2'), ('lm3');

mysql> SELECT queue_wait('queue_table', 'low_priority_queue');
+-------------------------------------------------+
| queue_wait('queue_table', 'low_priority_queue') |
+-------------------------------------------------+
|                                               1 |
+-------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

成功すると、取得できるテーブルの順の番号が返って来ます。(上記は1なのでqueue_table、2であればlow_priority_queueになります)あとは通常通り取得します。

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms2     |       10 |
+---------+----------+
1 row in set (0.00 sec)

ここの挙動がよくわかりにくく、どなたか分かる方がいたら教えて頂ければ大変嬉しいのですが、優先度が高いテーブル(queue_table)の中身が空の時、タイムアウトを指定しないでqueue_waitすると「ゼロ」(データ無し)と返って来ます。

2009/12/21追記: 複数指定する場合は、必ずタイムアウト引数を付けなくてはいけないので、この用法は間違いでした。お詫びいたします。

mysql> TRUNCATE queue_table;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT queue_wait('queue_table', 'low_priority_queue');
+-------------------------------------------------+
| queue_wait('queue_table', 'low_priority_queue') |
+-------------------------------------------------+
|                                               0 |
+-------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

タイムアウトを与えてやると、正しく取得できます。下記の場合はqueue_tableにレコードがないので、low_priority_queueから取得するため2番が返り、期待通りの動作となります。

mysql> SELECT queue_wait('queue_table', 'low_priority_queue', 10);
+-----------------------------------------------------+
| queue_wait('queue_table', 'low_priority_queue', 10) |
+-----------------------------------------------------+
|                                                   2 |
+-----------------------------------------------------+
1 row in set (0.00 sec)

mysql> SELECT * FROM low_priority_queue;
+---------+----------+
| message | priority |
+---------+----------+
| lm1     |       10 |
+---------+----------+
1 row in set (0.00 sec)

条件付き取り出し

簡易なWHERE句のようにして取得することが可能です。queue_waitのテーブルを指定する引数に、条件を付加して取得します。
下記の例は優先度2で新たなキューを発行し、優先度3以下で取得するようにしてみます。

mysql> INSERT INTO queue_table(message, priority) values ('blocker', 2);
Query OK, 1 row affected (0.00 sec)
mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| ms1     |       10 |
| ms2     |       10 |
| ms3     |       10 |
| blocker |        2 |
+---------+----------+
4 rows in set (0.00 sec)

mysql> SELECT queue_wait('queue_table:priority<3');

mysql> SELECT * FROM queue_table;
+---------+----------+
| message | priority |
+---------+----------+
| blocker |        2 |
+---------+----------+
1 row in set (0.00 sec)

リレー

別のサーバにキューをリレーする事が出来ます。同梱されているq4m-forwardというperlスクリプトを起動すると、無限ループでキューを待ち、別のサーバへ転送するような処理になっています。
同様の処理であれば、自作のスクリプトで同様の処理が可能です。
daemontoolsなどを使えば、割とお手軽に転送させることが出来るかと思います。

まとめ

いかがでしたでしょうか。メッセージキューイングというとActiveMQやTheSchwartzなど、いろいろなソリューションがあると思いますが、キューイング方法などかなり手軽に扱えるので、1度試してみてはいかがでしょうか。

試している最中、queue_wait()してオーナーモード中にtruncateしてしまい、drop databaseすら効かなくなってrm -r /var/lib/mysql/testしたりしましたが、ご愛敬ということで。