zoco

调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版本造成的问题。

通过以下方式可以暂时解决这个问题

  1. zRevRangeByScore换一种写法
  2. 换redis版本

从根本上看一下这个问题的原因:

先strace看一下日志:

NuaaAe.png

可以看到从redis已经返回数据了,但是返回数据之后马上收到一个SIGSEGV的报错,还看不出报错的具体原因。

再用gdb看一下:

NudSjx.png

可以看出报错是发生在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