Redis:排查 read error on connection 小记

本文详细探讨了Redis连接中遇到的'read error on connection'问题,从PHP Redis客户端连接不断的原因、pconnect的工作原理、Redis服务器移除client的场景,到使用docker重现问题和解决方案。重点分析了OPT_READ_TIMEOUT、TCP KEEPALIVE的设置,并提供了代码示例和系统参数调整建议,以增强连接的稳定性。
摘要由CSDN通过智能技术生成

从错误说起

版本信息

一个PHP常驻内存进程,连上Redis后,定时做brpop操作,阻塞时间为10s。问题出现在,几天(不定时)后,该进程就会
僵死,表现为:

  1. netstat下,php进程与redis建立的客户端连接仍在(ESTABLISHED)
  2. 在客户机tcpdump,没有输出任何数据包信息(没有通信?)
  3. strace该php进程,并没有输出任何系统调用(阻塞在哪了?)
  4. 查看redis-server,发现client list中,并不存在该client(被移除了?)

phpredis客户端连接为何不断?

关于phpredis连接,有下面几个地方需要理解清楚

  1. connect() 函数参数 timeout 为 0
  2. ini_set(‘default_socket_timeout’, -1)
  3. setOption(\Redis::OPT_READ_TIMEOUT, -1)
  4. 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_timeoutOPT_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;
    
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值