阅读 755

在高并发环境下Reids做缓存踩坑记录

近期随着公司部分项目迁移,服务器压力集中于某些业务上,导致原本稳定的项目出现了频繁的卡死现象。

通过逐步调试发现,此时项目的性能瓶颈出现在做缓存的Redis上。以最简单的get指令,一次调用耗时可以达到数百毫秒,显然是不可思议的。

对Redis本身做性能测试,发现Redis性能很正常,远远没有达到极限。

服务卡死时服务器本身资源也很正常,没有出现性能瓶颈问题。

基本判定问题出现在服务器代码本身。

服务器程序结构: SpringMVC+Spring+Mybatis+Jedis

经过数周的排查,最终发现了两点使用不正确的地方。

Redis调用次数过多

在调试过程中,首先发现的就是此问题。

初步调试中利用Spring的AOP功能,对Jedis的每次调用都做了记录,发现原有的业务逻辑中,每次接口请求,需要调用Redis至少五十次,简直丧心病狂。

不需要数据支持,仅凭常识就能够想到如此大量的Redis调用会很大程度上影响接口的性能。

根据Redis监控分析,当服务卡死,Redis的连接数远远没有达到极限,仅为6000+,说明此时其实是Jedis连接池不足以支持如此频繁的请求。

于是第一步就是精简代码,减少Redis请求。

鉴于前人都不是傻子,其实精简代码的成效不是很好。使用hmget替换了循环的hget之后,性能略有提升但是仍旧无法满足需求。最后的解决方案是使用MongoDb再增加一层缓存数据库。

部分修改频繁的统计、计数性质数据与内容数据一起作为一个文档存储于MongoDb,再间隔几分钟定时从MongoDb再同步到Redis缓存中。省去了80%的Redis请求数量。

最后从Redis使用get获取内容id列表,再通过hmget一次批量获取数据,两次请求完成原本数十上百次请求的工作,性能得到极大提升。

在测试环境中测试,Jmeter,300线程。3秒超时

原有代码仅有不足200的QPS,延迟极高,有很高的失败率。

新代码可以达到800+QPS,而且平均响应时间在50ms左右,最高仅有500ms

Redis缓存数据过大

然而代码上线不久就再次卡死。只能继续寻找原因。

Redis的性能强大是经过无数项目实践证明的。但是Redis也有它适用场景的限制。在原本的代码中将大量的Json数据缓存在Redis中,是导致Reids性能缓慢的元凶之一。

根据Redis的性能测试,在数据量超过1K时开始出现较为明显的性能下滑,超过100K时性能下滑已经极为严重。

在原本的业务逻辑中,Redis每次读取数据可达300KB,堪称海量。此时使用Jmeter进行压测,在200线程下仅仅有250QPS的吞吐量。

在调试期间,曾经想到过这个问题,并将数据量折半,约为170KB每次,但是因为数据量仍然远超100KB大关,性能提升并不明显。

最后通过减少数据量,将数据减少至80KB,发现性能出现较大提升,最后进一步精简数据达到40KB,得到了700+QPS的吞吐量,提升十分显著。

减少Redis请求量的另一种思路

在这个项目上线并且平稳运行之后,很容易联想到公司另一个项目也出现了极为相似的性能问题。

这个项目并不存在数据量过大的问题。每次返回的数据不足10KB,虽然Redis请求量没有前一个丧心病狂,但是仍然较多而且连读带写。更严重的问题则是,由于业务逻辑问题,不能便捷的切换为MongoDb进行数据整合。

第一步将一部分请求用Hmget的方式做了整合,性能提升很不明显,大概只有5%左右。

Redis请求量大导致速度慢,并不是Redis本身无法处理大量请求,而是大量请求占用连接池资源,并且有固定的网络请求消耗。Redis中有一个神器pipeline可以解决此问题。

根据初步的测试,通过pipeline的方式对统计数据的Redis请求做了整合之后,在200线程下的吞吐量由300QPS提升到了500QPS,提升十分显著。

补充 :

经过进一步的调试,通过hmget和pipeline的结合,成功将吞吐量提升至1000+QPS,进一步证明了合理使用Redis,程序的性能都是有很大潜力可以挖掘的。

总结

Redis是性能强大的缓存服务。但是也不能无条件的相信Redis的性能。最后错误的使用方法将会成为服务器性能的陷阱,而且会付出大量的精力去修正原有的程序。

缓存是高性能服务的保障,但是缓存和数据库资源一样是需要精细的算计的。节约每一点资源,谁知道哪里就会成为压垮服务器的最后一根稻草呢。

评论