调redis就进程crash
2018-02-23
[07-Feb-2018 18:50:06] WARNING: [pool www] child 597 exited on signal 11 (SIGSEGV) after 6502.027154 seconds from start
[07-Feb-2018 18:50:06] NOTICE: [pool www] child 640 started
多次测试后必现。
SIGSEGV是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号。
SIGSEGV会导致php进程的crash,同时观察php的存货进程,确定每次调用该接口的时候所在进程会被kill掉。
查一下php的代码调用什么会造成这个结果,最终定位到的代码是:
$redis->zRevRangeByScore($key,'+inf','0',['withscores'=>false,'limit'=>1111]);
这个的确是官方的写法,但是我在物理机上执行这个命令的时候,却没有crash。
查看系统环境:php版本是一致的,redis的版本不一致,docker上是3.0.0,物理机上是3.1.2,根据经验很可能是redis版本造成的问题。
通过以下方式可以暂时解决这个问题
- zRevRangeByScore换一种写法
- 换redis版本
从根本上看一下这个问题的原因:
先strace看一下日志:
可以看到从redis已经返回数据了,但是返回数据之后马上收到一个SIGSEGV的报错,还看不出报错的具体原因。
再用gdb看一下:
可以看出报错是发生在zend_hash_index_find()函数上,在执行这个函数的时候发生了内存错误。
看一下最新的phpredis和phpredis3.0.0在这个函数上的对比:
3.0.0:
// Check for an options array
if(z_opt && Z_TYPE_P(z_opt)==IS_ARRAY) {
ht_opt = Z_ARRVAL_P(z_opt);
// Check for WITHSCORES
*withscores = ((z_ele = zend_hash_str_find(ht_opt,"withscores",sizeof("withscores") - 1)) != NULL && Z_TYPE_P(z_ele) == IS_TRUE);
// LIMIT
if((z_ele = zend_hash_str_find(ht_opt, "limit", sizeof("limit") - 1)) != NULL)
{
HashTable *ht_limit = Z_ARRVAL_P(z_ele);
zval *z_off, *z_cnt;
if((z_off = zend_hash_index_find(ht_limit,0)) != NULL &&
(z_cnt = zend_hash_index_find(ht_limit,1)) != NULL &&
Z_TYPE_P(z_off) == IS_LONG && Z_TYPE_P(z_cnt) == IS_LONG)
{
has_limit = 1;
limit_low = Z_LVAL_P(z_off);
limit_high = Z_LVAL_P(z_cnt);
}
}
}
3.1.6:
// Check for an options array
if(z_opt && Z_TYPE_P(z_opt)==IS_ARRAY) {
ht_opt = Z_ARRVAL_P(z_opt);
ZEND_HASH_FOREACH_KEY_VAL(ht_opt, idx, zkey, z_ele) {
/* All options require a string key type */
if (!zkey) continue;
ZVAL_DEREF(z_ele);
/* Check for withscores and limit */
if (IS_WITHSCORES_ARG(ZSTR_VAL(zkey), ZSTR_LEN(zkey))) {
*withscores = zval_is_true(z_ele);
} else if (IS_LIMIT_ARG(ZSTR_VAL(zkey), ZSTR_LEN(zkey)) && Z_TYPE_P(z_ele) == IS_ARRAY) {
HashTable *htlimit = Z_ARRVAL_P(z_ele);
zval *zoff, *zcnt;
/* We need two arguments (offset and count) */
if ((zoff = zend_hash_index_find(htlimit, 0)) != NULL &&
(zcnt = zend_hash_index_find(htlimit, 1)) != NULL
) {
/* Set our limit if we can get valid longs from both args */
offset = zval_get_long(zoff);
count = zval_get_long(zcnt);
has_limit = 1;
}
}
} ZEND_HASH_FOREACH_END();
}
发现在3.1.6上面多了一个判断:
else if (IS_LIMIT_ARG(ZSTR_VAL(zkey), ZSTR_LEN(zkey)) && Z_TYPE_P(z_ele) == IS_ARRAY) {
会判断传的参数是不是array,而3.0.0没有判断……
而调用的方式是:
$redis->zRevRangeByScore($key,'+inf','0',['withscores'=>false,'limit'=>1111]);
所以在zend_hash_index_find(htlimit, 1)的时候会找不到内存地址而crash……
再看看github上zRevRangeByScore的用法发现是
$redis->zRangeByScore('key', 0, 3, array('withscores' => TRUE, 'limit' => array(1, 1)); /* array('val2' => 2) */
limit需要写个数组…… 改一下就好了……
附录:
strace的使用方式
sudo strace -p pid
gdb的使用方式: 参考 使用gdb调试PHP段错误 - ≈正念≈
echo '/tmp/coredump-%e.%p' > /proc/sys/kernel/core_pattern
注意这个物理机才能用,docker不好改系统变量
rlimit_core = unlimited
在/etc/php-fpm.conf 的[www]加上这个
gdb /usr/sbin/php-fpm /tmp/coredump-php-fpm.30345