此文已由作者张洪箫授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
问题描述
随着考拉业务的增长和规模的扩大,很多的应用都开始重度依赖缓存服务,也就是杭研的nkv。但是在使用过程中,发现服务端压力并不是特别大的情况下,客户端的rt却很高,导致应用在到达一定并发的情况下,服务的质量下降的非常严重,甚至出现不可用的情况。这个在大促阶段尤其严重,所以在杭研性能测试团队(这里特别感谢霞姐和小宝的大力支持)的协助下,在线下进行了压测和问题定位。
问题发现过程
测试使用了dubbo接口调用形式对fee工程进行加压,fee会在被调用后通过nkv客户端访问nkv服务端。
第一阶段
在逐步加压的过程中,发现并发到达150时,rt已经涨到50ms左右,此时使用jstack打印堆栈,发现大量的线程在等待消息的返回。反观此时服务端的情况,基本无压力,故此时定位,瓶颈在客户端。
review客户端代码,客户端调用服务端采用的是netty的异步方式,同步接口会在调用netty发送消息后使用future来获得响应;异步则将future返回给应用,交由应用处理。简单的过了一遍代码,并没有发现什么异常。
因为从哨兵监控上观察应用并没有异常,只是上下文切换的次数稍多,差不多有8w左右,怀疑有可能是因为线程数过多导致上下文切换过于频繁导致,所以将dubbo的线程数从400降低到了200,再次压测,发现在同等并发数下,rt比之前低,在40ms左右。
因为40ms并不能满足要求,而且不过压测机的并发数提高多少,qps最高只能在1500-1700左右徘徊,无法继续加压,所以只能继续找问题。
第二阶段
在之后的阶段卡了很久。后来观察到qps不高,load却比较高,邵峰提出是否是netty的一个bug,即在并发不高的情况下,占用大量的cpu,导致响应较慢,尝试把netty的更换到一个较高的版本,再次尝试,问题依然。
第三阶段
百思不得其解的时候,霞姐偶然在监控堆栈的地方看到有几率出现在一个锁的地方停留,然后回过头仔细看了下那段代码,终于发现了问题。 nkv在调用netty发送消息前会做这两个操作,第一个是将channal和seq的信息放到一个DelayQueue中,然后再将seq和channal的对应放到一个ConcurrentHashMap里
然后再收到响应后,从DelayQueue中删除收到消息对应的信息,再根据seq找到channal,然后唤醒等待的线程。

DelayQueue在这里主要是为了防止在某些异常情况下,导致channal未释放,会有另一个线程定时扫描DelayQueue清除超时对象。
但是在大并发下,由于DelayQueue的删除是上锁后遍历整个列表的,所以收到响应后的清除操作,会将网络回调线程和业务的发送线程阻塞,虽然很快就收到服务端的响应,但是回调线程却无法回调,所以导致整体响应非常慢。
第四阶段
找到原因后,解决方案的选择就比较多,后来选择的方案是将DelayQueue去掉,在调用发送后,将获取future的结果的操作放入try-finally中,释放chnnal的操作放在finally,则同步操作时释放肯定会被执行。异步操作则是在网络线程的回调后清除channal。
修改完成后,再次压测,再qps接近3倍的情况下,平均rt降低到原来的1/4,基本满足了本次调优的目标。
第五阶段
调优是没有止境的,所以再过了一遍代码,发现在调用netty发送后,客户端调用了await来等待网络的回调,

但是因为nkv的rt正常情况下非常低,很多操作都是毫秒内的,但是await的单位是毫秒,这样很有可能导致有些情况下本来需要1.1.毫秒的操作,却变成了2ms。 所以就将await改成了awaitNanos,纳秒级等待。 修改后测试,在使用batch操作时,平均rt降低1.6ms,可能有些人觉得才1.6ms,差别不大,但是在99%响应在10ms内的缓存服务中,降低1.6毫秒的影响已经很大了。
总结
需要好的工具,像霞姐他们的性能测试工具,这样事半功倍
往往优化的点就是备忽略的点,还是得自己review
优化无止境,小的点也会有很大的影响
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 限时购校验小工具&dubbo异步调用实现限
【推荐】 一个体验好的Windows 任务栏缩略图开发心得