分析Redis Server went away产生的原因

 2023-09-05 阅读 62 评论 0

摘要:PHP Fatal error: Uncaught RedisException: Redis server went away in /data/php/redis.php:670常见的网络连接错误原因:一、准备知识二、案例分析:二、phpredis源码分析: 项目对Redis依赖较重,使用phpredis扩展操作Redis, 但频繁出现Redis se

PHP Fatal error: Uncaught RedisException: Redis server went away in /data/php/redis.php:670

项目对Redis依赖较重,使用phpredis扩展操作Redis, 但频繁出现Redis server went away错误。

常见的网络连接错误原因:

Network is unreachable
到目标ip无可用路由 非常罕见(断网,或没有网关时)

connection timedout
tcp建立连接超时(目标主机不可达到,或产生丢包)

iptables在高并发连接时丢包,可能导致连接超时 ip_conntrack: table full, dropping packet

connection refused 连接被拒绝,目标主机存活,端口未开放

Couldn’t resolve host 域名解析失败

但是phpredis的server went away非常让人困惑,为了弄清楚phpredis产生server went away的原因,有必要深入分析,从而才有助于解决这个问题。

这个错误信息与PDO的MySQL Server has gone away非常类型,但是词法上却有细微差别

一、准备知识

MySQL Server has gone away产生的原因和解决办法?

pdo使用gone, 而phpredis使用went, 为此查阅资料,两者区别如下:

gone 用于现在完成时, 表示“去了” 强调已经完成

went 用于一般过去式, 表示“去”. 不强调是否已经完成

1.He has gone to Shanghai.他已经去了上海(强调已经到达).

2.He went to Shanghai yesterday.他昨天去了上海(有可能还未到达 还在路上).

所以:

MySQL Server has gone away MySQL服务器已经走了

Redis Server went away Redis Server走了

虽然两者语义上理解上都没有问题,但是学究一些,gone away比went away更为准确一些。

二、案例分析:

目前生产环境使用的phpredis版本为2.2.7, 以此版本为准进行调试。

  1. 无法连接时, 假设本机并没有Redis server监听4512端口,以下代码的运行结果?
<?php$redis = new Redis();$redis->connect('localhost', 4512);

常规方式,连接失败就应该抛出异常,但是phpredis在这里不会抛出异常,也不会产生任何报错,只会返回false, 所以拦截connect异常是徒劳的。
结论:phpredis连接失败时,不抛出异常也不产生错误信息,只返回false。

  1. 连接失败 又同时进行了操作
<?php$redis = new Redis();$redis->connect('localhost', 4512);$redis->set('name', 'value');

这种情况下,如果连接失败,就会抛出went away异常, 但是从语义上来说went away其实是错误的。

  1. 未连接就进行操作(虽然这样没有意义)
<?php$redis = new Redis();$redis->set('name', 'value');

这种情况下,也会抛出went away异常。同样,这其实也存在语义上的问题,不曾拥有,何谈失去?

总结:phpredis在连接失败时,不会抛出异常,只返回false。操作时,如果没有有效的连接,才抛出异常。

  1. Redis Server的错误消息响应
    根据Redis的协议,Redis一共有5种消息类型,使用特定前缀字符区分:

https://redis.io/topics/protocol

Simple Strings: +

Errors: -

Integers: :

Bulk: $

Arrays: *

错误响应时的消息样式:

-Error message\r\n

本机启动redis server于端口6379, 继续测试:

<?php$redis = new Redis();$redis->connect('localhost', 6379);$redis->select(18); //默认数据库编号从0~15, 这里故意模拟错误

结果:只是返回false, 没有任何错误或异常产生!

通过strace调试,实际上Redis Server会输出错误: -ERR DB index is out of range\r\n

但是phpredis并没有抛出异常或错误,导致很难定位错误原因。

再比如,对一个没有启用密码保护的Redis Server, 尝试密码登录:


<?php$redis = new Redis();$redis->connect('localhost', 6379);

这种情况下,仍然不会产生异常或是错误信息,只会返回false, 实际上Redis Server会返回: -ERR Client sent AUTH, but no password is set, 这个可以用strace调试得出。

phpredis对错误响应消息的抑制,导致问题排查困难。

二、phpredis源码分析:

  1. 在phpredis源码中搜索went away,发现以redis.c源程序的int redis_sock_get函数中有went away异常抛出
PHP_REDIS_API int redis_sock_get(zval *id, RedisSock **redis_sock TSRMLS_DC, int no_throw){zval **socket;int resource_type;if (Z_TYPE_P(id) != IS_OBJECT || zend_hash_find(Z_OBJPROP_P(id), "socket",sizeof("socket"), (void **) &socket) == FAILURE) {/* Throw an exception unless we've been requested not to */if(!no_throw) {zend_throw_exception(redis_exception_ce, "Redis server went away", 0 TSRMLS_CC);}return -1;}*redis_sock = (RedisSock *) zend_list_find(Z_LVAL_PP(socket), &resource_type);if (!*redis_sock || resource_type != le_redis_sock) {/* Throw an exception unless we've been requested not to */if(!no_throw) {zend_throw_exception(redis_exception_ce, "Redis server went away", 0 TSRMLS_CC);}return -1;}if ((*redis_sock)->lazy_connect){(*redis_sock)->lazy_connect = 0;if (redis_sock_server_open(*redis_sock, 1 TSRMLS_CC) < 0) {return -1;}}return Z_LVAL_PP(socket);}

大概的意思就是:
查询是否有socket资源,如果没有抛出went away异常。

查询出了资源,但是资源无效(如连接失败),抛出went away异常。

继续跟踪connect方法的流程:

PHP_METHOD(Redis, connect){if (redis_connect(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0) == FAILURE) {RETURN_FALSE;} else {RETURN_TRUE;}}

PHP_METHOD(Redis, connect)定义Redis类的connect方法,调用流程:


redis_sock->stream = php_stream_xport_create(host, host_len, ENFORCE_SAFE_MODE,STREAM_XPORT_CLIENT| STREAM_XPORT_CONNECT,persistent_id, tv_ptr, NULL, &errstr, &err);

也就是说,连接失败时,这里并不会抛出任何异常, 只是返回-1, errstr, err已经包含了详细的错误信息和错误码。

修改源码,连接失败时输出详细的错误信息:

原代码:

efree(host);if (!redis_sock->stream) {efree(errstr);return -1;}

修改后的代码

if (!redis_sock->stream) {char* message = emalloc(256); //分配256字节用于保存错误信息sprintf(message, "%s %s", host, errstr);  //格式化字符串 主机地址 错误信息 错误码zend_throw_exception(redis_exception_ce, message, err TSRMLS_CC); //抛出异常efree(errstr);efree(host);efree(message);return -1;}efree(host);

然后重新编译phpredis,连接失败就会抛出异常:

PHP Fatal error: Uncaught exception ‘RedisException’ with message ‘localhost:6379 Connection refused’ in /root/redis.php:4

替代的方案:

使用其它成熟的redis扩展或类, yii2官方提供了一个类,yii2-redis, 经过研究这个类代码成熟,可以替代phpredis, 特点:

  1. 使用php代码编写,调用底层socket实现连接、请求发送、响应解析,无须安装redis客户端扩展,也不依赖其它第三方扩展,也不受机器环境配置的影响。

  2. 利于编程者更容易掌握Redis的协议,容易扩展。

  3. 提供更直观实用的socket编程范例。

性能上可能比phpredis稍低,但这个差别是微乎其微的,可以忽略。

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/868.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息