累积技术沉淀经验 2015-02-07
前段时间,上线了新的 Redis缓存(Cache)服务,准备替换掉 Memcached。
为什么要将 Memcached 替换掉?
原因是 业务数据是压缩后的列表型数据,缓存中保存最新的3000条数据。对于新数据追加操作,需要拆解成[get + unzip + append + zip + set]这5步操作。若列表长度在O(1k)级别的,其耗时至少在50ms+。而在并发环境下,这样会存在“数据更新覆盖问题”,因为追加操作不是原子操作。(线上也确实遇到了这个问题)
针对“追加操作不是原子操作”的问题,我们就开始调研有哪些可以解决这个问题同时又满足业务数据类型的分布式缓存解决方案。
当前,业界常用的一些 key-value分布式缓存系统如下:
参考自:
通过对比、筛选分析,我们最终选择了 Redis。原因有以下几个:
啰嗦了一些其它东西,现在言归正传。
Redis 服务上线当天,就密切关注 Redis 的一些重要监控指标(clients:客户端连接数、memory、stats:服务器每秒钟执行的命令数量、commandstats:一些关键命令的执行统计信息、redis.error.log:异常日志)。(参考自《Redis监控方案》)
观察到下午5点左右,发现“客户端连接数”一直在增长,最高时都超过了2000个(见下图),即使减少也就减1~2个。但应用的QPS却在 10 个左右,而线上应用服务器不超过10台。按理说,服务器肯定不会有这么高的连接数,肯定哪里使用有问题。
现在只能通过逆向思维反向来推测问题:
由于在使用Jedis的过程中,就对Apache Commons Pool摸了一次底。对最后的两个疑惑都比较了解,Jedis的以下这些配置与对象池管理“空闲池对象”相关:
redis.max.idle.num=32768
redis.min.idle.num=30
redis.pool.behaviour=FIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
redis.max.evictable.idle.time.minutes=1440
在上面说“每台应用的Jedis连接数在60个左右是正常的”的理由是:线上共部署了2台Redis服务器,Jedis的“最小空闲池对象个数”配置为30 (redis.min.idle.num=30)。
GenericObjectPool是通过“驱逐者线程Evictor”管理“空闲池对象”的,详见《Apache Commons Pool之空闲对象的驱逐检测机制》一文。最下方的5个配置都是与“驱逐者线程Evictor”相关的,表示对象池的空闲队列行为为FIFO“先进先出”队列方式,每秒钟(1)检测10个空闲池对象,空闲池对象的空闲时间只有超过5分钟后,才有资格被驱逐检测,若空闲时间超过一天(1440),将被强制驱逐。
因为“驱逐者线程Evictor”会无限制循环地对“池对象空闲队列”进行迭代式地驱逐检测。空闲队列的行为有两种方式:LIFO“后进先出”栈方式、FIFO“先进先出”队列方式,默认使用LIFO。下面通过两幅图来展示这两种方式的实际运作方式:
一、LIFO“后进先出”栈方式
二、FIFO“先进先出”队列方式
从上面这两幅图可以看出,LIFO“后进先出”栈方式 有效地利用了空闲队列里的热点池对象资源,随着流量的下降会使一些池对象长时间未被使用而空闲着,最终它们将被淘汰驱逐;而 FIFO“先进先出”队列方式 虽然使空闲队列里所有池对象都能在一段时间里被使用,看起来它好像分散了资源的请求,但其实这不利于资源的释放。而这也是“客户端连接数一直降不下来”的根源之一。
redis.pool.behaviour=FIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
按照上述配置,我们可以计算一下,5分钟里到底有多少个空闲池对象被循环地使用过。
根据应用QPS 10个/s计算,5分钟里大概有10*5*60=3000个空闲池对象被使用过,正好与上面的“连接数尽然达到了3000+”符合,这样就说得通了。至此,整个问题终于水落石出了。(从监控图也可以看出,在21号晚上6点左右修改配置重启服务后,连接数就比较平稳了)
这里还要解释一下为什么使用FIFO“先进先出”队列方式的空闲队列行为?
因为我们在Jedis的基础上开发了“故障节点自动摘除,恢复正常的节点自动添加”的功能,本来想使用FIFO“先进先出”队列方式在节点故障时,对象池能快速更新整个集群信息,没想到弄巧成拙了。
修复后的Jedis配置如下:
redis.max.idle.num=32768
redis.min.idle.num=30
redis.pool.behaviour=LIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
redis.max.evictable.idle.time.minutes=30
综上所述,这个问题发生有两方面的原因: