Sunday, February 21, 2010

使用 php_memcached 连 memcacheq 遇到的诡异问题

使用 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;
}

Wednesday, February 3, 2010

编译 libmemcached 的 undefined reference to `__sync_fetch_and_add_4'

编译 libmemcached 0.37 结果出现这个错误。
  CC     clients/ms_thread.o
  CCLD   clients/memslap
clients/ms_conn.o: In function `ms_get_udp_request_id':
/data2/build/libmemcached-0.37/clients/ms_conn.c:184: undefined reference to `__sync_fetch_and_add_4'
clients/ms_conn.o: In function `ms_reconn_socks':
/data2/build/libmemcached-0.37/clients/ms_conn.c:1055: undefined reference to `__sync_fetch_and_add_4'
clients/ms_conn.o: In function `ms_reconn':
/data2/build/libmemcached-0.37/clients/ms_conn.c:924: undefined reference to `__sync_fetch_and_add_4'
/data2/build/libmemcached-0.37/clients/ms_conn.c:960: undefined reference to `__sync_fetch_and_add_4'
clients/ms_thread.o: In function `ms_setup_thread':
/data2/build/libmemcached-0.37/clients/ms_thread.c:215: undefined reference to `__sync_fetch_and_add_4'
clients/ms_thread.o:/data2/build/libmemcached-0.37/clients/ms_thread.c:198: more undefined references to `__sync_fetch_and_add_4' follow
collect2: ld returned 1 exit status
make[2]: *** [clients/memslap] Error 1
make[2]: Leaving directory `/data2/build/libmemcached-0.37'
make[1]: *** [all-recursive] Error 1
make[1]: Leaving directory `/data2/build/libmemcached-0.37'
make: *** [all] Error 2

当时使用的参数是
./configure --prefix=/data2/server/ --with-libevent-prefix=/data2/server/
后来又是
./configure --prefix=/data2/server/ --with-libevent-prefix=/data2/server/  --disable-64bit
还是不行

后来绕墙看到了这个
libmemcached undefined reference to `__sync_fetch_and_add_4′
改configure参数为
./configure --prefix=/data2/server/ --with-libevent-prefix=/data2/server/   --disable-64bit CFLAGS="-O3 -march=i686"
make clean install 就好了