My Avatar

JacketWoo

King Birds Fly Fast and Early!

phpredis的RedisCluster客户端使用总结

2016年08月17日 星期二, 发表于 北京

如果您对本文有任何的建议或者疑问, 可以给我发邮件(seuxfw1990@gmail.com)或者在 这里给我提 Issues, 谢谢! :)

前段时间基础架构组、DBA还有云盘团队一起推广了phpredis的RedisCluster的线上使用,目前线上业务已经稳定,单业务的规模水平是:Qps平均15W,数据量在700G左右。现对这段时间的工作和所遇到的一些常见问题进行简单总结:
####################################################################

  1. 问题:fd泄漏

    现象:最开始试水phpredis的Cluster功能时,总是遇到’Couldn’t map cluster keyspace using any provided seed’的错误

    原因:这是由于早期phpredis驱动的fd泄漏问题造成的。每新建一个RedisCluster客户端,客户端会向给定的Seeds节点获取Redis集群的Slots分布信息,但是为了获取slots信息而向Seeds节点建立的连接是不用于后期发送命令使用的,所以在获取slots信息结束后应该释放掉改连接,而phpredis没有释放,这样在新建若干次RedisCluster客户端后,就fd泄漏了,导致连接建立失败,slots信息也就无法获取了,从而报错。

    解决办法:更新phpredis的驱动,后期的phpredis驱动已经修复这个问题 (是我们提交的, wakaka……!)

  2. 问题:TimeWait过多导致“Timed out…“错误

    现象:客户机上timewait端口数量比较多时,‘Timed out…’报错量比较

    分析:我们观察到,当timewait端口数量较多时,会存在两个线程进程绑定绑定到同一个端口,但是却只有一个进程的连接建立成功了,另一个进程的连接建立要在已建立连接断开3s(SYN重试时间)后才能成功,而3s的时间延迟,而3s的延迟在业务方看来是不能容忍的,一般肯定会报“Time out…”的错误。而且随着timewait的数量的增加,这种情况发生的频率也递增;

    解决办法:使用长连接,或者限制timewait端口数

  3. 问题:获取Slots信息比较耗性能

    原因:因为每新建一个RedisCluster客户端都会向给定的Seeds节点拉去Redis集群的Slots信息,因为要建立连接和命令的特殊性,这个过程是相当耗时的(局域网内测试:2ms左右),当在新建RedisCluster的比较频繁的场景下,是比较的耗性能的

    解决办法:修改phpredis驱动,在本地缓存住Redis集群的的Slots信息,把它放在持久性资源内。这样每次新建一个RedisCluster客户端时就不用再走网络获取Slots信息,直接在本地缓存中就可以拿到这些信息了。

    备注:a. 在我们的推动下,作者也出了一个这样的功能,在cache-slots分支上;

    ​ b. 驱动在拉去Slots信息时,总是按顺序选择依次选择第一个,第二个……这样第一个Seed节点的压力就会比较大,所以如果在新建RedisCluster客户端比较频繁的场景下,应该使用Qconf这样的工具或者修改驱动,以随机选择Seed节点,达到均衡负载的目的。

  4. 问题:连接建立失败的问题

    现象:某一个客户端进程持续大量报“Timed out attempting…”的问题

    原因: 这个也是phpredis驱动的bug,产生这个问题有两个原因,一是驱动懒加载问题,当一个连接还未建立时,phpredis的做法是先把连接标志给置位,然后再建立链接,这样当链接建立失败了,下一次的判断却认为是成功的,也就没有机会重新建立连接了;二是连接存活检测的一个bug,当一个连接被认为是死”了, phpredis的做法是连续重连十次,如果都不能建立成功就退出,但是退出前所改变的状态,并不能让该连接有资格尝试下一次重连,这样的话当服务端因为某些原因再这十次重连的时间段内没有恢复过来(卡机了或着挂了等),等他恢复过来后,这个链接按理说是应该且可以恢复的,但是却因为没有机会重连而永远失效。

    解决办法:在每次的存活检测后,如果失效都把连接的状态置为有资格重连的状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     73 /*
     74 #define CLUSTER_SEND_PAYLOAD(sock, buf, len) \  
     75     (sock && sock->stream && !redis_check_eof(sock, 1 TSRMLS_CC) && \  
     76      php_stream_write(sock->stream, buf, len)==len)  
     77	*/  
     78 #define CLUSTER_SEND_PAYLOAD(sock, buf, len) \  
     79     ((sock && sock->stream && !redis_check_eof(sock, 1 TSRMLS_CC) && \  
     80      php_stream_write(sock->stream, buf, len)==len) || \  
     81      ((sock->stream == NULL ? : redis_stream_close(sock)), \  
     82        sock->lazy_connect = 1, \  
     83        sock->status = REDIS_SOCK_STATUS_DISCONNECTED, \  
     84        0))  
    

  5. 问题:连接数过多

    现象:客户机增加时,redis服务端的连接数激增,影响服务的稳定性

    原因:线上大部分客户机一般使用fpm模式来运行客户端的,进程数是128。官方的phpredis版本(cache-slots分支)是必须同时缓存集群的Slots信息和客户端到服务端的连接,这样为了使用缓存集群Slots信息的功能,服务端的连接数也被缓存了,造成连接数激增。

    解决办法:修改驱动,使得缓存集群slots信息和缓存连接相互独立

    备注:理论上来说,使用长连接(缓存连接)的好处要大于短连接,它避免了连接建立上的开销和TW过多等问题,在连接数不大的情况下,应该尽量用长连接来访问

  6. 问题:CPU过载

    现象:访问量增加后,客户端报“invaild reply for Del…”,并伴随着“Timed out…”错误

    原因:经过一段时间的排查,这是由于CPU压力过载导致的。CPU的压力太大,客户端发送过来的命令在给定的时间内(100ms)得不到返回值,也就报“Timed out…“的错误,之所以单单Del会报“invalid reply…”的错误,这是因为Del命令的返回值检查比较的严格,它的返回值必须是整数型的,而其他的命令返回值检查相对宽松。
    解决办法:针对这一问题,只能扩容了。

  7. 问题:命令问题

    现象:phpredis的RedisCluster客户端是不支持密码验证的,这就给数据库的安全带来了一定的风险

    解决办法:修改phpredis驱动,加上密码验证的功能

    备注:phpredis的Pull Requests里,有人已经提交了这个功能,如果有这方面强需求的可以使用或者参照着自己实现(链接

鉴于我们遇到的大部分问题都和驱动内发送命令的逻辑相关,所以最后贴一张phpredis驱动向主节点发送命令逻辑的流程图:

a. 根据key所对应的slot找出负责该slot的Redis主节点

b. 若是与给定节点还没有建立链接,则先建立链接

c. 发送命令:不成功则转向下一个redis主节点,然后从b重新开始

d. 读取返回值:若正常返回,则结束;

e. 判断是否超时:超时了则抛出超时异常;

f. 判断是否发生moved错误:是则转向所moved错误所提示的redis节点,并从b重新开始;

g. 继续从b开始重试

说明: