1

前言

本文主要是一次线上异常排查内容。
排查2020 03-24 23:14分tw挂掉后,英语偶线现取不到数据问题:

  • 2020 03-24 23:14 tw挂了一台,30秒没连接上redis;
  • 有一定机率从redis拿不到数据偶发;没有错误,拿不到数据;

怀疑对象

  1. etcd配置问题; - 确认过相关配置(排除)
  2. twemproxy可能变更节点;-查看过对应日志(排除)
  • 经过排查twemproxy和etcd只是由于线路故障影响30s访问。
  1. 代码存在问题;
  • 由于代码是swoole开发的常驻内存web程序。小伙伴们怀疑可能是redis的问题,因为redis使用的是短连接但是用了单例模式。只是怀疑不过phpredis会有重连机制,但是感觉很矛盾。然后做了一次实验。

代码实验内容

问题复现

通过swoole编写一个单例的redis的web程序。模拟真是场景中的redis访问请求。

  • 搭建redis + tw
  • 编写swoole访问redis的单例应用web

复现步骤:

  • php版本: 7.3.3
  • phpredis版本:4.3.0
  • swoole版本: 4.3.2
  1. 正常请求swoole的redis页面。
  2. 断开twemproxy后请求swoole的redis页面。
  3. 恢复twemproxy连接后请求swoole的redis页面。

最终验证结果百分之百复现,按照如下操作恢复访问后的请求没有报错直接返回了false。
1.jpg

接下来又用strace工具抓了一下进程
2.png
发现失败后并没有重连redis,但是经过看了一下phpredis源码发现和猜想不匹配,然后就怀疑可能是版本存在问题。

问题解惑

经过多个版本源码比较发现phpredis版本 <= 4.3.0 的版本都存在这个问题。常驻内存时只要连接失败,网络恢复后再去连接redis时不会抛出异常,只会返回false。而 > 4.3.0版本其实都会报错的,连接不上直接报Error级别错误Redis server went away 。

  • 该问题比较过多个phpredis版本 5.2.1,5.1.1,5.0.0,4.3.0,3.1.2

4.3.0 源码解析

redis_sock_server_open

PHP_REDIS_API int
redis_sock_server_open(RedisSock *redis_sock TSRMLS_DC)
{
    int res = -1;

    switch (redis_sock->status) {
        case REDIS_SOCK_STATUS_DISCONNECTED:
            return redis_sock_connect(redis_sock TSRMLS_CC);
        case REDIS_SOCK_STATUS_CONNECTED:
            res = 0;
        break;
    }

    return res; //网络恢复后再去连接redis时直接返回-1
}

redis_sock_get

PHP_REDIS_API RedisSock *
redis_sock_get(zval *id TSRMLS_DC, int no_throw)
{
    RedisSock *redis_sock;

    if ((redis_sock = redis_sock_get_instance(id TSRMLS_CC, no_throw)) == NULL) {
        return NULL;
    }

    if (redis_sock_server_open(redis_sock TSRMLS_CC) < 0) {
        return NULL;  //直接返回NULL
    }

    return redis_sock;
}
#define REDIS_PROCESS_KW_CMD(kw, cmdfunc, resp_func) \
    RedisSock *redis_sock; char *cmd; int cmd_len; void *ctx=NULL; \
    if ((redis_sock = redis_sock_get(getThis() TSRMLS_CC, 0)) == NULL || \
       cmdfunc(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, kw, &cmd, \
               &cmd_len, NULL, &ctx)==FAILURE) { \
            RETURN_FALSE; \  //为NULL时直接返回给php false
    } \
    REDIS_PROCESS_REQUEST(redis_sock, cmd, cmd_len); \
    if (IS_ATOMIC(redis_sock)) { \
        resp_func(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, NULL, ctx); \
    } else { \
        REDIS_PROCESS_RESPONSE_CLOSURE(resp_func, ctx) \
    }
  • 4.3.0版本以下,包括4.3.0。其实并没有针对用redis_sock->status 为REDIS_SOCK_STATUS_FAILED做处理,所以返回时均为-1,然后redis_sock_get接收-1时返回NULL,REDIS_PROCESS_KW_CMD宏中接收redis_sock_get的NULL则直接返回false给php

    lldb调试结果:

4.png

版本比对

访问比较
6.png

问题解析

  1. swoole代码中conf.d模版reload_cmd参数写错,写成reload_com导致swoole无法重启。

7.png

  1. 由于swoole代码是常驻内存,调用redis是用的是注册树,这样redis其实就相当于是长连接。

但是由于网络抖动或者是类似tw网络不稳定情况,引发Connection lost操作,这个时候predis其实会发起TCP的close关闭。
但是由于phpredis版本小于4.3.0 ,返回给php为false。不会报错。
9.png

  1. 偶发情况,是因为常驻内存php开启250多个worker,某几个worker存在问题。命中的概率所以为偶发。

c_rain
23 声望8 粉丝