前言
本文主要是一次线上异常排查内容。
排查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。
接下来又用strace工具抓了一下进程
发现失败后并没有重连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调试结果:
版本比对
访问比较
问题解析
- swoole代码中conf.d模版reload_cmd参数写错,写成reload_com导致swoole无法重启。
- 由于swoole代码是常驻内存,调用redis是用的是注册树,这样redis其实就相当于是长连接。
但是由于网络抖动或者是类似tw网络不稳定情况,引发Connection lost操作,这个时候predis其实会发起TCP的close关闭。
但是由于phpredis版本小于4.3.0 ,返回给php为false。不会报错。
- 偶发情况,是因为常驻内存php开启250多个worker,某几个worker存在问题。命中的概率所以为偶发。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。