使用 php_memcached 连 memcacheq 遇到的诡异问题
服务器环境:
Centos 5.4 http://centos.org/
PHP 5.3.1 http://www.php.net/
php_memcached 1.0.0 http://pecl.php.net/package/memcached
libmemcached 0.37 http://tangent.org/552/libmemcached.html
libevent 1.4.13 http://www.monkey.org/~provos/libevent/
Memcacheq 0.2.0 http://memcachedb.org/memcacheq/
给 php_memcached 做了一次封装。其中有个方法是这样写的
<?php
/**
* Memcached Class
*
*/
class spAMLMemcached
{
private $mc;
......
/**
* get方法,增加批量模式
*
* @param string|array $key
* @param callback $cache_cb 批量模式不可用
* @param float|array $cas_token
* @param int $flags
* @return mixed
*/
public function get($key, $cache_cb=null, &$cas_token=null, $flags=null)
{
if (is_array($key)) {
return $this->mc->getMulti($key, $cas_token, $flags);
} else {
return $this->mc->get($key, $cache_cb, $cas_token);
}
}
......
}
方法看起来貌似没问题。
关于 Memcached::get 的说明:
link to php manual;
Memcached::get
(PECL memcached >= 0.1.0)
Memcached::get — Retrieve an item
Description
public mixed Memcached::get ( string $key [, callback $cache_cb [, float &$cas_token ]] )
Memcached::get() returns the item that was previously stored under the key . If the item is found and cas_token variable is provided, it will contain the CAS token value for the item. See Memcached::cas for how to use CAS tokens. Read-through caching callback may be specified via cache_cb parameter.
Parameters
key
The key of the item to retrieve.
cache_cb
Read-through caching callback or NULL.
cas_token
The variable to store the CAS token in.
Return Values
Returns the value stored in the cache or FALSE otherwise. The Memcached::getResultCode will return Memcached::RES_NOTFOUND if the key does not exist.
从这里看也没啥问题。
但是,实际调用的时候,用了这样的代码。
<?php
$a = new spAMLMemcached;
$a->addServer('10.**.*.**', 22201);
var_dump($a->get('example'), $a->getResultCode());
var_dump($a->get('example', null), $a->getResultCode());
结果很遗憾,两个返回的都是false。
getResultCode 出来的结果是 8 即 Memcached::RES_PROTOCOL_ERROR 协议错误。
无法理解。
以前也没遇到过这个问题啊。
tcpdump 抓包看了看
tcpdump -XXAfN -ttt -i eth3 "ip dst 10.**.*.**"
包的数据居然是 。。。
..gets.example..
gets ...
如果仅仅是对 memcached 而言,这个没问题。高版本的memcached 都支持这个。
但是。。。用的是 Memcacheq
根本就不支持这个。
于是协议错误。。。
好吧,查原因。
最后写了个简单的测试代码。
<?php
$s = new Memcached;
$s->addServer('10.**.*.**', 22201);
var_dump($s->get('example'), $s->getResultCode());
var_dump($s->get('example', null), $s->getResultCode());
输出一切正常。
于是不由得怀疑起封装时对
&$cas_token=null 的处理。
做了如下的测试代码。
<?php
$q = new Memcached;
$q->addServer('10.**.*.**', 22201);
var_dump($q->get('example'), $q->getResultCode());
var_dump($q->get('example', null), $q->getResultCode());
echo "\nmcdgettest: \n";
var_dump(mcdgettest('example'), $q->getResultCode());
var_dump(mcdgettest('example', null), $q->getResultCode());
echo "\nmcdgettest1: \n";
var_dump(mcdgettest1('example'), $q->getResultCode());
echo "\nmcdgettest2: \n";
var_dump(mcdgettest2('example'), $q->getResultCode());
echo "\nmcdgettest3: \n";
var_dump(mcdgettest3('example'), $q->getResultCode());
echo "END\n\n";
unset($q);
$q = new Memcached;
$q->addServer('10.**.*.**', 22201);
var_dump($q->get('example'), $q->getResultCode());
var_dump($q->get('example', null), $q->getResultCode());
echo "\nmcdgettest3: \n";
var_dump(mcdgettest3('example'), $q->getResultCode());
echo "\nmcdgettest2: \n";
var_dump(mcdgettest2('example'), $q->getResultCode());
echo "\nmcdgettest1: \n";
var_dump(mcdgettest1('example'), $q->getResultCode());
echo "\nmcdgettest: \n";
var_dump(mcdgettest('example'), $q->getResultCode());
var_dump(mcdgettest('example', null), $q->getResultCode());
echo "END\n\n";
unset($q);
$q = new Memcached;
$q->addServer('10.**.*.**', 22201);
var_dump($q->get('example'), $q->getResultCode());
var_dump($q->get('example', null), $q->getResultCode());
echo "\nmcdgettest3: \n";
var_dump(mcdgettest3('example'), $q->getResultCode());
echo "\nmcdgettest1: \n";
var_dump(mcdgettest1('example'), $q->getResultCode());
echo "\nmcdgettest2: \n";
var_dump(mcdgettest2('example'), $q->getResultCode());
echo "\nmcdgettest: \n";
var_dump(mcdgettest('example'), $q->getResultCode());
var_dump(mcdgettest('example', null), $q->getResultCode());
echo "END\n\n";
unset($q);
function mcdgettest($key, $cache_cb=null, &$cas_token=null) {
global $q;
return $q->get($key, $cache_cb, $cas_token);
}
function mcdgettest1($key, $cache_cb=null) {
global $q;
return $q->get($key, $cache_cb);
}
function mcdgettest2($key, &$cas_token=null) {
global $q;
return $q->get($key, null, $cas_token);
}
function mcdgettest3($key) {
global $q;
return $q->get($key);
}
结果如下:
[root@localhost 3.53]: /data1/www/htdocs/i.sina.com.cn/source/apps/daemon/cron
0> php testMemcachedGet.php
int(140)
int(0)
int(141)
int(0)
mcdgettest:
bool(false)
int(8)
bool(false)
int(8)
mcdgettest1:
bool(false)
int(8)
mcdgettest2:
bool(false)
int(8)
mcdgettest3:
bool(false)
int(8)
END
int(142)
int(0)
int(143)
int(0)
mcdgettest3:
int(144)
int(0)
mcdgettest2:
bool(false)
int(8)
mcdgettest1:
bool(false)
int(8)
mcdgettest:
bool(false)
int(8)
bool(false)
int(8)
END
int(145)
int(0)
int(146)
int(0)
mcdgettest3:
int(147)
int(0)
mcdgettest1:
int(148)
int(0)
mcdgettest2:
bool(false)
int(8)
mcdgettest:
bool(false)
int(8)
bool(false)
int(8)
END
另外一边抓包
[root@localhost 3.53]: /data1/www/htdocs/i.sina.com.cn/source/datadrv/memcacheq
130> tcpdump -XXAfN -ttt -i eth3 "ip dst 10.**.*.**" | grep get
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth3, link-type EN10MB (Ethernet), capture size 96 bytes
0x0040: b19b 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b19c 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b19c 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b19d 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b19d 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b19e 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b19e 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b19f 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b19f 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b19f 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b1a0 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b1a0 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b1a1 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b1a1 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b1a2 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b1a2 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b1a2 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b1a2 6765 7420 6578 616d 706c 6520 0d0a ..get.example...
0x0040: b1a3 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
0x0040: b1a3 6765 7473 2065 7861 6d70 6c65 200d ..gets.example..
81 packets captured
82 packets received by filter
0 packets dropped by kernel
对着一行行看吧。
第一个结论很明显:对于每组测试,只要有一次进入 gets 那后边全都是 gets。
第二个结论就得一点点比较分析了,结论是:只要对 $cas_token 这个引用传值的参数进行了封装,后续的结果就会出错。
为了验证这个错误会不会对最基本的调用产生影响,我在 第二组测试的 321 的 2后边 直接 调用了 $q->get('example') 很遗憾,又是一个 protocol error.
而且,不知道大家注意了没有,从测试脚本输出的最后的false有三个,而抓包只有两个gets。我试着不在结束的时候ctrl+c,然后对跑了两次测试,终于最后位置有 三个 gets。
从源码分析。
首先看 libmemcached。因为 gets 的操作命令来自这里。
get.c 中:Line200 memcached_mget_by_key_real() 中
if (ptr->flags.support_cas)
{
get_command= "gets ";
get_command_length= 5;
}
这是唯一的有 gets 的地方。
于是下一步查哪儿修改了 ptr->flags.support_cas 的这个标志位。
定位到 behavior.c line 81 memcached_behavior_set() 函数内
case MEMCACHED_BEHAVIOR_SUPPORT_CAS:
ptr->flags.support_cas= set_flag(data);
break;
从 php_memcached 的 php_memcached.c 中 找到几次 MEMCACHED_BEHAVIOR_SUPPORT_CAS 的设置。
分别在
static void php_memc_get_impl(INTERNAL_FUNCTION_PARAMETERS, zend_bool by_key)
static void php_memc_getMulti_impl(INTERNAL_FUNCTION_PARAMETERS, zend_bool by_key)
static void php_memc_getDelayed_impl(INTERNAL_FUNCTION_PARAMETERS, zend_bool by_key)
第一个是 Memcached::get 方法所在的位置。
if (cas_token) {
uint64_t orig_cas_flag;
/*
* Enable CAS support, but only if it is currently disabled.
*/
orig_cas_flag = memcached_behavior_get(i_obj->memc, MEMCACHED_BEHAVIOR_SUPPORT_CAS);
if (orig_cas_flag == 0) {
memcached_behavior_set(i_obj->memc, MEMCACHED_BEHAVIOR_SUPPORT_CAS, 1);
}
......
/*
* Restore the CAS support flag, but only if we had to turn it on.
*/
if (orig_cas_flag == 0) {
memcached_behavior_set(i_obj->memc, MEMCACHED_BEHAVIOR_SUPPORT_CAS, orig_cas_flag);
}
return;
} else {
......
如果 $cas_token 这个变量传入了 就会进到这个if判断里。首先会强制开启 MEMCACHED_BEHAVIOR_SUPPORT_CAS 这个选项,当操作完整结束的时候,再将这个选项恢复。
这个的唯一的问题在于,Line 395
if (php_memc_handle_error(status TSRMLS_CC) < 0) {
memcached_result_free(&result);
RETURN_FALSE;
}
出错退出的时候,直接 free result 然后 return false,并未将 这个选项恢复。
等到下一次取这个对象的时候,因为这次已经设为真,即便下一次没走cas_token 的这个值,但是调用 memcached_mget_by_key 的时候,原始这个选项还是会污染新的操作,因为这一次不涉及到恢复这个选项的问题。
这就能解释 为什么 按 3 2 1 的顺序执行的代码 1 会被污染成protocol error.
解决方案是在
php_memcached.c line 395 :
if (php_memc_handle_error(status TSRMLS_CC) < 0) {
memcached_result_free(&result);
RETURN_FALSE;
}
RETURN_FALSE 前 做一次恢复。
即
if (php_memc_handle_error(status TSRMLS_CC) < 0) {
memcached_result_free(&result);
/*
* Restore the CAS support flag, but only if we had to turn it on.
*/
if (orig_cas_flag == 0) {
memcached_behavior_set(i_obj->memc, MEMCACHED_BEHAVIOR_SUPPORT_CAS, orig_cas_flag);
}
RETURN_FALSE;
}