[TIPS] PHPでAMQP+MessagePackを使う時は気をつけたほうがいい

[] PHPでAMQP+MessagePackを使う時は気をつけたほうがいい先週末からずっぷしハマっていたんです。悪気は無かったんです。

現在、リリースに向けて最終調整中のRHETOLO V2システムですが、基本的にPHPで記述してあります。で、負荷分散とか可用性とかムズカシイことを色々考え(ようとしてみ)た結果、Web・API・DBMSのつながりを「ゆるく」しておくべく、リアルタイムの処理・レスポンスがそこまで要求されない部分については「メッセージ・キュー」を使い、タスクを別プロセスに委託する、という手法を取りました。

今回のV2では、ユーザマイページなどRDBMSが必須なApache+&MySQLチームと、APIなど高速な参照が要求されるApache+&Redisチームという2つのシステムに分け、RDBMSに変更がかかった場合はメッセージキューを介して専用のワーカープログラムが処理を行う、という形にしてあります。

メッセージキューシステムにはAMQP実装であるRabbitMQを採用しました。V1ではActiveMQをSTOMPで叩いていましたが、ちょっとした心境の変化というやつですね。

PHPからAMQPにアクセスするために、PECL::amqp エクステンションをインストールしてあります。



さて、メッセージキューには基本的に文字列(String)のデータを投げる必要があります。が、実際にはPHPのObjectだったりArrayだったりのほうが扱いやすかったりするので、Object/Array を文字列化してやる必要があります。通常であればPHPの標準関数であるserialize() / unserialize()を利用しますが、「もっと早く・コンパクトに」シリアライズできるMessagePackを採用しました。

でもって、ここからが本題。

例えば
[php]$var = array(
‘title’  => ‘タイトル’,
‘code’  => ‘0106*2’,
‘status’  => 0,
);
[/php]
なんていうArrayをserializeし、タスク情報としてメッセージキューに投げるんですが、受信側でunserializeに失敗するケースが出てきました。

よくよく調べてみると、status = 0 の時に失敗しているようです。

で、ここで実験。



[php]<?php
$data = array(
‘code’ => ‘0106’,
‘title’ => ‘RHETOLO’,
‘status’ => 0,
);

$ser = serialize($data);
$pck = msgpack_pack($data);

echo "serialize=[{$ser}]n";
echo "msgpack_pack=[{$pck}]n";
$oded = unpack(‘H*’, $pck);
echo "hex :[". var_export($oded, true) ."]n";
echo "msgpack_unpack=[". var_dump(msgpack_unpack($pck)) ."]n";
[/php]




なんてプログラムを走らせてみます。

[php gutter=”0″]
% php foo.php
serialize=[a:3:{s:4:"code";s:4:"0106";s:5:"title";s:7:"RHETOLO";s:6:"status";i:0;}]
msgpack_pack=[Ζcode、0106・titleァRHETOLOヲstatus]
[/php]



・・・・・あれ。

msgpack_pack()の出力が “status” で終わっています。本来はこの後に “=0” に相当するデータがあるべきです。これをヘキサで出力してみたものがこちら。

[php gutter=”0″]
hex :[array (
1 => ’83a4636f6465a430313036a57469746c65a7524845544f4c4fa673746174757300′,
)]
[/php]


0x00になってるぅぅぅ!?
うーむ。まぁ確かに数値としてのゼロなので間違いではないっぽいです。他のPHPバインディングでも同様の結果でした。

さて、ここで今度はPECL:: のソースを見てみます。問題なのは AMQPEnvelope::getBody() の最後。

[php gutter=”0″]
197 /* {{{ proto AMQPEnvelope::getBody()
198 check amqp envelope */
199 PHP_METHOD(amqp_envelope_class, getBody)
200 {
201     zval *id;
202     amqp_envelope_object *envelope;
203
~~~~省略~~~~
215
216     RETURN_STRING(envelope-&gt;body, 1);
217 }
218 /* }}} */
[/php]


このRETURN_STRING()はZend_APIのようです。PHPのソース内の Zend/Zend_API.h にありました。

[php gutter=”0″]
596 #define RETURN_STRING(s, duplicate)     { RETVAL_STRING(s, duplicate); return; }
597 #define RETURN_STRINGL(s, l, duplicate) { RETVAL_STRINGL(s, l, duplicate); return; }
[/php]

えーと、RETVAL_STRING() は・・

[php gutter=”0″]
584 #define RETVAL_STRING(s, duplicate)         ZVAL_STRING(return_value, s, duplicate)
585 #define RETVAL_STRINGL(s, l, duplicate)     ZVAL_STRINGL(return_value, s, l, duplicate)
[/php]

 
てーいっ!
 

[php gutter=”0″]
539 #define ZVAL_STRING(z, s, duplicate) { 
540         const char *__s=(s);           
541         Z_STRLEN_P(z) = strlen(__s);   
542         Z_STRVAL_P(z) = (duplicate?estrndup(__s, Z_STRLEN_P(z)):(char*)_        _s);
543         Z_TYPE_P(z) = IS_STRING;       
544     }
[/php]


やっと辿り着きました。。。中身を見ると

 

strlen() しておる(´Д`;)

 

strlen() って思い切りC言語の関数よね。

 

Cって、0x00は文字列の終端よね・・・・

 

そりゃ切れるわけだorz

 

いやまぁRETURN_STRINGなのでString前提なんですよね。わかる、わかるよ、うん。

 

ということで、 / PECL::amqp どちらかのバグとか言うつもりはありません。むしろそれぞれ理にかなっていると思います。

 



 

PECL::amqp に投げるときは MessagePack で pack するのは避けたほうがいいかも。
ちなみに、「status => 0,」 ではなく 「status => ‘0’,」 と、明示的に文字列として代入しておくと 0x00 にはならないようです。

 

以上、豆知識でした。

  1. コメント 0

  1. トラックバック 0

return top