php扩展常驻内存,PHP常驻内存下phpredis扩展的一个BUG

前言

本文主要是一次线上异常排查内容。

排查2020 03-24 23:14分tw挂掉后,英语偶线现取不到数据问题:

2020 03-24 23:14 tw挂了一台,30秒没连接上redis;

有一定机率从redis拿不到数据偶发;没有错误,拿不到数据;

怀疑对象

etcd配置问题; - 确认过相关配置(排除)

twemproxy可能变更节点;-查看过对应日志(排除)

经过排查twemproxy和etcd只是由于线路故障影响30s访问。

代码存在问题;

由于代码是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

正常请求swoole的redis页面。

断开twemproxy后请求swoole的redis页面。

恢复twemproxy连接后请求swoole的redis页面。

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

1460000022227771

接下来又用strace工具抓了一下进程

1460000022227770

发现失败后并没有重连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调试结果:

1460000022227773

版本比对

访问比较

1460000022227772

问题解析

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

1460000022227774

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

但是由于网络抖动或者是类似tw网络不稳定情况,引发Connection lost操作,这个时候predis其实会发起TCP的close关闭。

但是由于phpredis版本小于4.3.0 ,返回给php为false。不会报错。

1460000022227775

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