从错误说起
版本信息
- php-7.1.x
- phpredis-4.0.x
一个PHP常驻内存进程
,连上Redis
后,定时做brpop
操作,阻塞时间为10s
。问题出现在,几天(不定时)后,该进程就会
僵死
,表现为:
netstat
下,php进程与redis建立的客户端连接仍在(ESTABLISHED)- 在客户机
tcpdump
,没有输出任何数据包信息(没有通信?) strace
该php进程,并没有输出任何系统调用(阻塞在哪了?)- 查看redis-server,发现
client list
中,并不存在该client(被移除了?)
phpredis客户端连接为何不断?
关于phpredis连接,有下面几个地方需要理解清楚
- connect() 函数参数 timeout 为 0
- ini_set(‘default_socket_timeout’, -1)
- setOption(\Redis::OPT_READ_TIMEOUT, -1)
- pconnect
connect 函数参数 timeout
参数:
- host: string. can be a host, or the path to a unix domain socket. Starting from version 5.0.0 it is possible to specify schema
- port: int, optional
- timeout: float, value in seconds (optional, default is 0 meaning unlimited)
- reserved: should be NULL if retry_interval is specified
- retry_interval: int, value in milliseconds (optional)
- read_timeout: float, value in seconds (optional, default is 0 meaning unlimited)
这里的timeout
表示建立连接
时的超时时间,调用此函数时,客户端将与服务端进行三次握手,建立TCP连接。由于网络原因,可以指定一个超时时间,意思是,如果客户端和服务端在该时间限制
内未能建立连接,则返回false
文件:redis.c 行:935
PHP_METHOD(Redis, connect)
{
if (redis_connect(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0) == FAILURE) {
RETURN_FALSE;
} else {
RETURN_TRUE;
}
}
其中,redis_connect的函数原型为
PHP_REDIS_API int redis_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent);
persistent 为 0
表示不建立持久连接
,下面会聊到等于 1
的情况。说明connect
函数建立的是短连接
,当调用close
函数时,连接就会关闭。看下面的源码确实如此,如果在建立连接前已经存在另一个连接,则关闭。
文件:redis.c 行:1011
redis = PHPREDIS_GET_OBJECT(redis_object, object);
/* if there is a redis sock already we have to remove it */
if (redis->sock) {
redis_sock_disconnect(redis->sock, 0);
redis_free_socket(redis->sock);
}
default_socket_timeout
这个配置可以在php.ini找到,文档注释很简单:基于 socket 的流的默认超时时间(秒)
redis是基于tcp协议
的程序,所以这个配置也会对其造成影响。比如read error on connection
错误,这是phpredis在执行get、brpop等操作时,如果在default_socket_timeout
时间内不返回结果就会报这个错误。php.ini中默认为60s
。可以在程序中使用内置函数ini_set
在运行时修改。
OPT_READ_TIMEOUT
phpredis版本的“default_socket_timeout”,通过这个值,一样可以达到同样的效果。那么如果同时设置了default_socket_timeout
和OPT_READ_TIMEOUT
,优先级是怎样的?
实测发现,如果同时存在两个配置,优先使用OPT_READ_TIMEOUT
的配置,这样是合理的。
文件:redis_commands.c 行:3980
case REDIS_OPT_READ_TIMEOUT:
redis_sock->read_timeout = zval_get_double(val);
if (redis_sock->stream) {
read_tv.tv_sec = (time_t)redis_sock->read_timeout;
read_tv.tv_usec = (int)((redis_sock->read_timeout -
read_tv.tv_sec) * 1000000);
php_stream_set_option(redis_sock->stream,
PHP_STREAM_OPTION_READ_TIMEOUT, 0,
&read_tv);
}
RETURN_TRUE;
pconnect的原理是什么?
文件:redis.c 行:947
PHP_METHOD(Redis, pconnect)
{
if (redis_connect(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1) == FAILURE) {
RETURN_FALSE;
} else {
RETURN_TRUE;
}
}
建立连接时,先到连接池
获取连接(最后一个),并移除最后一个连接实例。如果连接是活跃的(PHP_STREAM_OPTION_CHECK_LIVENESS),则直接返回。如果连接已失效,则建立新的连接。
文件:library.c 行:1828
if (redis_sock->persistent) {
if (INI_INT("redis.pconnect.pooling_enabled")) {
p = redis_sock_get_connection_pool(redis_sock);
if (zend_llist_count(&p->list) > 0) {
redis_sock->stream = *(php_stream **)zend_llist_get_last(&p->list);
zend_llist_remove_tail(&p->list);
/* Check socket liveness using 0 second timeout */
if (php_stream_set_option(redis_sock->stream, PHP_STREAM_OPTION_CHECK_LIVENESS, 0, NULL) == PHP_STREAM_OPTION_RETURN_OK) {
redis_sock->status = REDIS_SOCK_STATUS_CONNECTED;
return SUCCESS;
}
php_stream_pclose(redis_sock->stream);
p->nb_active--;
}
int limit = INI_INT("redis.pconnect.connection_limit");
if (limit > 0 && p->nb_active >= limit) {
redis_sock_set_err(redis_sock, "Connection limit reached", sizeof("Connection limit reached") - 1);
return FAILURE;
}
gettimeofday(&tv, NULL);
persistent_id = strpprintf(0, "phpredis_%ld%ld", tv.tv_sec, tv.tv_usec);
} else {
if (redis_sock->persistent_id) {
persistent_id = strpprintf(0, "phpredis:%s:%s", host, ZSTR_VAL(redis_sock->persistent_id));
} else {
persistent_id = strpprintf(0, "phpredis:%s:%f", host, redis_sock->timeout);
}
}
tv.tv_sec = (time_t)redis_sock->timeout;
tv.tv_usec = (int)((redis_sock->timeout - tv.tv_sec) * 1000000);
if (tv.tv_sec != 0 || tv.tv_usec != 0) {
tv_ptr = &tv;
}
redis_sock->stream = php_stream_xport_create(host, host_len,
0, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT,
persistent_id ? ZSTR_VAL(persistent_id) : NULL,
tv_ptr, NULL, &estr, &err);
if (persistent_id) {
zend_string_release(persistent_id);
}
if (!redis_sock->stream) {
if (estr) {
redis_sock_set_err(redis_sock, ZSTR_VAL(estr), ZSTR_LEN(estr));
zend_string_release(estr);
}
return FAILURE;