零 概述
在业务中不可避免的需要使用缓存,当缓存数据很少时,只需要几台缓存服务器即可满足需求。 但是当业务增多时,就需要N个业务M个缓存服务器。缓存服务器不管使用memcache还是redis,都是需要业务方来管理缓存服务器的host和port。
我们业务中使用的memcache服务器接近千台,还需要业务方告知相关人员来处理memcache的问题(memcache机器宕机或者连接数打满)。而实际上,对于我们业务方而言,只需要使用缓存服务器即可,业务方不需要关注缓存服务器的重启和健康检查。当然了,业务方还是需要关注访问缓存服务器的访问时间。
一 我们对缓存服务的要求
- 单个请求耗时在2ms以内
- 能够抗住30w qps的压力
二 我们的尝试
I. 首先尝试twitter的twemproxy
Twemproxy介绍
Twemproxy是一个使用C语言编写、以代理的方式实现的、轻量级的代理服务器,通过引入一个代理层,将应用程序后端的多台Redis实例进行统一管理,使应用程序只需要在Twemproxy上进行操作,而不用关心后面具体有多少个真实的Redis或Memcached实例,从而实现了基于Redis和Memcached的集群服务。当某个节点宕掉时,Twemproxy可以自动将它从集群中剔除,而当它恢复服务时,Twemproxy也会自动连接。
Twemproxy的缺点:1. 无法平滑地扩容/缩容, 2. 运维不友好,没有控制面板(对于业务方而言,这个不是问题)3. 经过一层代理,对性能有损耗。
我们测试的结果
a. 压测方式一:
使用twitter自家的mcperf压测工具来压测twemproxy。
压测命令:
/bin/mcperf --timeout=0.2 --conn-rate=1000 --call-rate=5000000 --num-calls=1000 --num-conns=100 --sizes=u32,1024 -s 10.10.10.10 -p 11211
压测结果:
|case| 直连memcache|连接twemproxy|
|—|—|—|
|数据在32字节和1k字节之间|57750 qps, 耗时100ms以内|25589 qps,耗时100ms以上非常多|
|数据在4k和8k字节之间|18156 qps | 12376 qps|b. 压测方式二:使用n个php进程压测,这种压测方法并不能够得到一个
mcperf
的准确的qps,但是更接近我们的应用场景,也能够看出一些问题。这种方式压测中发现的问题:1) twemproxy所在机器TIME_WAIT和CLOSE_WAIT非常高
2) 单次请求耗时增加,连接twemproxy的qps小于直连memcacheTIME_WAIT
及CLOSE_WAIT
非常高的原因:php端设置超时时间是100ms,即server端100ms内没有响应时,php端就会主动断开连接。上面提到mcperf压测时超过100ms的请求非常多,那么100ms没有收到响应时php段就会主动断开连接。由于twemproxy服务器过于繁忙,没有调用close从而导致twemproxy处于
CLOSE_WAIT
状态。对于
TIME_WAIT
而言,则是twemproxy向memcache服务器发送请求,等待某个时间(twemproxy中设置),如果没有响应,twemproxy就会主动断开连接;当大量从twemproxy到达memcache的请求超时时,就会出现TIME_WAIT
过多。
II 可以考虑方案
我们将可以考虑的方案列一下,360的bada,360的pika,豌豆荚的codis。
先来说一下豌豆荚的codis,它跟twemproxy一样都是经过一层代理,首先经过一层代理必然会造成一定的性能损耗,其次在内存拷贝上codis并没有进行优化,官方开发者spinlock在答疑中提到:a. twemproxy只有redis性能一半
, b. codis比twemproxy慢
。
也就是说,不管是twemproxy还是codis,这种经过一层代理的方式都会造成一定的性能损耗;并且在高并发的情况下,代理会带来更多的问题;综合考虑,我们不使用这种代理的方式
再来说一下公司的pika,数据存在硬盘并且底层是rocksdb,单个请求和并发都不满足需求。
再来说一下公司的bada,qps在10w以上,但是平均耗时比redis多。
综合考虑,我们决定测试一下RedisCluster,看是否满足我们的需求。
III RedisCluster
- 压测环境
Redis3.0.5版的Redis-benchmark并不支持对RedisCluster的压力测试。看了一下常见的压测工具都没有直接对RedisCluster进行压测的工具。没有办法,只好使用25台测试机,单机进程总数,每个进程请求数量10w个,通过日志记录每个请求的耗时时间,对日志进行分析。 qps粗略计算
采用php直接压测,qps的粗略计算:
机器数*每台机器的php进程数*1000(1s=1000ms)/ 单个请求耗时(单位ms)
如果单次请求是5ms,则预计是
25*5*1000/5=2.5w
当然还可以通过redis-trib.rb得到,将数字部分累加即可。
$./redis-trib.rb call 10.138.113.202:16001 info | grep -iE "instantaneous_ops_per_sec" instantaneous_ops_per_sec:3963 instantaneous_ops_per_sec:3842 instantaneous_ops_per_sec:4219 instantaneous_ops_per_sec:3590
对RedisCluster,Memcache,Redis进行压力测试,主要是测试在高并发情况下三者的耗时。耗时位于0-5ms的比例Redis好于Memcache好于Cluster。解释:
因为RedisCluster在创建对象的时候需要发送
cluster slots
命令获取一次集群的节点信息,导致耗时比redis和memcache要久。当然这也是其比memcache明智的地方, 因为memcache需要业务方维护一份节点信息,一旦需要更换节点,则需要业务方进行更改;而cluster的节点信息是由cluster的管理员维护。对于单个http请求而言,与memcache和Redis相比,只是多了获取节点信息的操作
压测中发现phpredis驱动的一个问题:
压测过程中发现单个redis-cluster节点接受cluster-slots命令是4000qps.(该命令获取集群节点映射信息),当大于4000qps时,耗时就会急剧增加。
对于这个问题,可以通过增加cluster节点来解决。
当然最好的方法是修改phpredis的cluster驱动,能够缓存cluster节点信息,不用每次创建对象的时候都发送clusterslots命令
我们最后的选择
基于上面的压测,我们决定使用RedisCluster,原因有二:
a. 对于单个http请求(里面可能有多个缓存操作)而言,相比直连memcache或者redis,只是多了一步获取节点信息的耗时,耗时在2ms左右,这点性能损耗我们是可以忍受的。
b. 对于DBA而言,维护简单,扩容方便;对于业务方而言,我只需要获取RedisCluster的一个节点进行连接即可,跟使用普通Redis差别不大。
使用RedisCluster中存在的问题
前端机连接RedisCluster使用短连接,因此造成前端机TIMEWAIT过高,从而导致大量的连接超时。这儿要说明两个问题,第一个是为何使用短连接,因为使用长连接的话就不会存在TIMEWAIT过高的问题。第二个是TIMEWAIT过高的解决方式。
问题1:使用短连接而不使用长连接
原因是:使用长连接可能造成前端机或cluster机器端口不够用
先确定采用长连接时Cluster服务器连接数的计算方式:
前端机数量*每台前端机fpm进程数量
。如果前端机数量非常多,比如300台,每台前端机fpm128个,那么与某台cluster建立的长连接数量是38400,会造成cluster上连接数过多。
而对于前端机而言,则可能会造成端口不够用。端口范围由net.ipv4.ip_local_port_range决定)。
为了避免出现这个问题,我们使用短连接而不是长连接问题2:TIMEWAIT
TIMEWAIT出现原因:发送方主动断开连接时,收到接收方发来的FIN以后处于的状态,这是TCP四次挥手的正常状态。但是由于TIMEWAIT状态时,连接依然占有端口和fd,因此可能会出现端口不够用或套接字不够用的情况,从而导致连接失败。
TIMEWAIT解决方法:我们采用的是修改net.ipv4.tcp_max_tw_buckets = 5000。