近期随着公司部分项目迁移,服务器压力集中于某些业务上,导致原本稳定的项目出现了频繁的卡死现象。
通过逐步调试发现,此时项目的性能瓶颈出现在做缓存的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的性能。最后错误的使用方法将会成为服务器性能的陷阱,而且会付出大量的精力去修正原有的程序。
缓存是高性能服务的保障,但是缓存和数据库资源一样是需要精细的算计的。节约每一点资源,谁知道哪里就会成为压垮服务器的最后一根稻草呢。