问题现象:
线上的php在流量高峰等情况下出现redis连接proxy失败的情况,随着流量增长连接失败数量会增长。
虽然对线上的服务造成的影响很小,担心问题随着流量增长有增加趋势,可能会进一步引发线上问题。
问题排查与分析:
统计REDIS连接失败的数量,发现连接失败随流量增长有增加趋势。
查看redisproxy的session情况。加群里面咨询其他语言连接池的实现方式是否有连接失败情况。
收集到信息如下:
golang集群连接失败一周内偶现几条。php集群一天内千条左右。
结论:跟php本身的语言有关系。
检查php连接redis的类 发现如下代码:
代码释义:在php7版本中使用长连接。在php5版本中使用短连接。
分析:如果按照长连接的方式。以php的实现方式。长连接会将连接的套接字信息存储在工作的worker进程的内存空间内。
PHP 生命周期见博客:https://www.kancloud.cn/digest/php-src/136264
此内存在 MSHUTDOWN 阶段时才会清理 。具体可见博客:https://anyof.me/articles/533 分析。
php-redis和myssql的长连接分析见:http://blog.aimager.com/post/2017/12/25-Php-persist-connect.html
推理可得:以当前流量情况和qps情况。那么有多少个cgi访问过redis 。在超时时间内就会有多少proxy的session。但是观察proxy的session情况。
session数量与cgi数量并不符合。 并且session的数量波动和流量正相关。
怀疑: php没有使用长连接的方式来连接redis的proxy。也就是说redis的pconnect是以connect的方式工作的。
**排查:**模拟线上环境。 只打开一个php-cgi进程 。strace cgi进程。研究调用堆栈。连续调用多次,set ,get
代码如下:
得到调用堆栈如下:
可见redis连接了多次,并且close了多次。执行 netstat -at|grep 9200 查看 tcp连接。得到如下图:
TIME_WAIT状态出现在主动断开连接的一方在执行完成四次挥手后要保证网络上的数据包消逝掉而维持的一个状态。
也就是说redis连接在client端使用后被主动断开了。采用的工作模式是短连接。
按照正常理解,如果使用长连接,那么出现的连接应该是 每个worker建立一个连接。后续不再建立连接 、并且连接状态应该是:ESTABLISHED
可查看tpc 协议状态转换图:
见博客:http://xiehongfeng100.github.io/2018/09/09/net-tcp-state-transition-diagram/
对照试验:
在我自己的测试机上面安装php.7.0.33 redis扩展,版本3.1.5 同样的测试脚本。 只启动一个cgi进程。
得到的调用堆栈,只有一次connect调用。 netstat 查看,有且仅有一个ESTABLISHED 的连接
怀疑和线上环境有关系。
得到结论: 线上使用的phpredis与redis的proxy并没有按照预期使用长连接进行数据交互。而是短连接。
那么连接失败的请求就是因为短连接太多导致proxy无法accept请求或者并发请求量太多导致网络负载过高而出现的问题。
如果按照这样的情况,在redis使用量增加或者proxy的负载情况不乐观的情况下,redis连接失败可能会大面积爆发。
行为不一致:
继续排查为线上php和我搭建的PHP为何行为不一致。
查看php版本:
线上:
我的环境:
线上编译的版本为 zts 线程安全版本。我编译的版本为 nts 非线程安全版。
怀疑问题出现在这个地方。
研究zts : 见博客:
http://blog.codinglabs.org/articles/zend-thread-safety.html
https://blog.csdn.net/risingsun001/article/details/50497930
http://blog.codinglabs.org/articles/zend-thread-safety.html
查找PHPREDIS的扩展手册:
注意这句话:This feature is not available in threaded versions. pconnect and popen then working like their non persistent equivalents.
在线程版本中,工作模式和短连接是一致的。
下载redis扩展3.1.4源码 。查看源码:
在redis.c 中发现如下定义:如果发现zts ,那么关闭长连接。
得到结论: 由于线上php版本编译的是ZTS版本你的,所以redis扩展的pconnect按照connect的工作模式工作。
即线上使用的redis扩展在连接redisproxy的过程中使用的是短连接,直接原因是因为线上的PHP是编译的zts版本的。
短连接的影响:
受到网络波动的影响较大。 每次读写需要频繁建连。
由于tcp连接在建立连接到打到读写峰值有一段时间的“连接预热“过程 , 导致连接后数据传输的时间会比长连接长,传输的数据量越大,影响越严重。
受proxy性能影响严重,建立连接过程需要proxy能够accept连接的请求,完成三次握手。而redis的使用场景并发量高。proxy的负载会很高。如果proxy无法accept请求,会出现连接失败。
解决办法:
直接合理的解决办法,如果可能,将线上的php编译成nts版本的,使用redis扩展提供的长连接支持。在应对更多的并发请求过程中可以复用连接。
如果存在某些原因无法更改。还有折中和取巧的办法: 见博客:
https://blog.huoding.com/2017/09/10/635
或者使用swool等扩展来实现连接池。 但是整体改动成本很高。
或者更改语言,使用其他语言来实现连接池。
目前该问题已经定位,但是没有更好且合理的解决办法。