研究下php调用redis的长连接问题。

生命周期

对于长连接可以这样想:长连接解决的问题是重复建立连接所带来的的性能损失,而如果要保持长连接肯定需要一种机制来存储已建立的连接。

而对于php来说,除了一些常驻内存的扩展之外,只有php-fpm能实现这种功能,因为他是常驻内存的。

结合扩展的加载流程可知,redis的长连接一定是在MINIT阶段做的初始化,在执行php代码的时候将长连接存储在这个空间内,php进程执行完成之后长连接依然存在,从而实现在不同请求之间共享连接。

根据以上推理,翻一下源码,在redis.c的833行,有这样一个调用:

/* Register resource destructors */
    le_redis_pconnect = zend_register_list_destructors_ex(NULL, redis_connections_pool_dtor,
        "phpredis persistent connections pool", module_number);

根据代码的注释和调用,大概能猜到这里是注册了一个phpredis的持久化连接池。这里的redis_connections_pool_dtor,应该是一个资源的析构函数,在开发者没有显示调用close的关闭资源的时候由引擎调用。

上面的如果懂了,可以推测出php-fpm的进程退出时会断开所有的长连接。

保活

一个tcp长连接建立后,需要某种机制来保活,也就是发送keepalive包。

而phpredis文档上并没有说明keepalive的相关配置,猜测是并没有实现主动保活的功能(操作系统应该有机制可以实现,默认时间可以通过sudo sysctl -a | grep keepalive查看)。

查看redis的配置文件,发现有tcp-keepalive,配置文件注释也表明这个选项是用来保活的。 新版本默认配置为300秒,也就是每五分钟发送一个tcp包给客户端连接,保持连接始终为ESTABLISHED

通过抓包可以看出:

sudo tcpdump -i any tcp port 6379
redis keepalive

通过抓包,也能确认在php-fpm进程退出的时候会有关闭连接的数据包产生。

注意事项

在phpredis扩展的github首页,有这样一句话:

So be prepared for too many open FD’s errors (specially on redis server side) when using persistent connections on many servers connecting to one redis server.

大意是使用长连接要注意打开太多文件描述符的错误,造成这个问题的原因是redis保存了大量的客户端连接,最终超过了限制,目前默认限制是10000(maxclients)。

不过phpredis是根据tcp+ip来识别不同的连接,测试过程中发现一次http请求,php-fpm假设有5个worker进程,可能会一次创建小于5个的长连接。

这个问题产生的错误是max number of clients reached,涉及到这个问题的配置项为timeout,默认0,也就是说不超时。所以如果开启了长连接,同时有keepalive,那么这些连接将一直存在。

还有一个事情就是php-fpm的配置项pm.max_requests,这个选项用来避免fpm的worker进程内存泄露。也即比如worker进程处理了100个进程之后,由master重新生成worker进程来使其重生,放在长连接的场景下,会出现长连接的age有所不同的情况。

在redis-cli内可以通过下面两个命令查看客户端列表:

client list
info clients

最后

本文主要讲了phpredis长连接的生命周期和如何保活,以及结合配置文件说明一些可能出现的问题。