当前位置: 首页 > 后端技术 > PHP

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

时间:2023-03-29 15:04:00 PHP

前言本文主要是网上异常排查。查看202003-2423:14tw挂掉后英文双线取不到数据问题:202003-2423:14tw挂掉一个,30秒没连接redis;有一定概率从redis中获取到没有零星的数据;没有错误,没有数据;怀疑etcd配置问题;-确认相关配置(排除)twemproxy可能会改变节点;-排错后查看对应日志(已排除),twemproxy和etcd只受线路故障影响30s访问。代码有问题;因为代码是swoole开发的常驻内存web程序。小伙伴们怀疑可能是redis的问题,因为redis使用的是短连接但是使用的是单例模式。只是怀疑phpredis会有重连机制,但是感觉很矛盾。然后做了一个实验。代码实验内容问题复现。通过swoole写一个单例的redisweb程序。模拟真实场景中的redis访问请求。搭建redis+tw写swoole访问redis单例应用web复现步骤:php版本:7.3.3phpredis版本:4.3.0swoole版本:4.3.2正常请求swoole的redis页面。断开twemproxy后,请求swoole的redis页面。恢复twemproxy连接后,请求swoole的redis页面。最终验证结果100%复现,按照以下操作恢复访问后请求返回false,不报错。接下来用strace工具抓进程,发现redis失败后并没有重连,但是查看phpredis源码后发现和猜测的不符,然后怀疑可能有版本问题。问题解决经过比较多个版本的源码,发现phpredis<=4.3.0的版本有这个问题。只要常驻内存时连接失败,网络恢复后连接redis就不会抛异常,只会返回false。但是版本>4.3.0其实会报错,如果连接失败,会直接报一个Error级别的错误Redisserverwoneaway。对比过phpredis多个版本5.2.1,5.1.1,5.0.0,4.3.0,3.1.24.3.0源码分析redis_sock_server_openPHP_REDIS_APIintredis_sock_server_open(RedisSock*redis_sockTSRMLS_DC){intres=-1;switch(redis_sock->status){caseREDIS_SOCK_STATUS_DISCONNECTED:returnredis_sock_connect(redis_sockTSRMLS_CC);案例REDIS_SOCK_STATUS_CONNECTED:res=0;中断;}返回资源;TSRMLS_DC,intno_throw){RedisSock*redis_sock;如果((redis_sock=redis_sock_get_instance(idTSRMLS_CC,no_throw))==NULL){返回NULL;}if(redis_sock_server_open(redis_sockTSRMLS_CC)<0){返回NULL;//直接返回NULL}returnredis_sock;}#defineREDIS_PROCESS_KW_CMD(kw,cmdfunc,resp_func)\RedisSock*redis_sock;字符*命令;intcmd_len;void*ctx=NULL;\if((redis_sock=redis_sock_get(getThis()TSRMLS_CC,0))==N乌尔||\cmdfunc(INTERNAL_FUNCTION_PARAM_PASSTHRU,redis_sock,kw,&cmd,\&cmd_len,NULL,&ctx)==FAILURE){\RETURN_FALSE;\//NULL时直接返回phpfalse}\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->statushandlesREDIS_SOCK_STATUS_FAILED,所以返回-1,然后redis_sock_get收到-1时返回NULL,而REDIS_PROCESS_KW_CMD宏接收到redis_sock_get的NULL,则直接返回false给phplldb调试结果:版本比较访问比较问题分析swoole代码中,conf.d模板的reload_cmd参数写错了,写成reload_com,导致swoole重启失败。由于swoole代码常驻内存,通过注册树来调用redis,所以redis其实就相当于一个长连接。但是由于网络抖动或者类似的tw网络不稳定,会触发Connectionlost操作。这个时候predis实际上会发起一次TCP关闭。但是由于phpredis版本低于4.3.0,所以返回false给php。不会报错。偶尔是因为常驻内存PHP启用了250多个worker,部分worker出现问题。因此,击中的概率是偶然的。