开源有坑,使用谨慎:缓存连接池开源组件剖析

Posted 唯品会质量工程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开源有坑,使用谨慎:缓存连接池开源组件剖析相关的知识,希望对你有一定的参考价值。

引言

某域在使用 Venus-cache时发现使用Connection模式(缓存并一直使用 Template中的连接)后,性能比直接使用Template模式有所上升。

       对于两种模式的使用区别,示例如下:

1.Connection模式:

MemcachedTemplate.getConnectionFactory().getConnection().get(“key”,serializer)

2.Template模式:

MemcachedTemplate.get(“key”)

针对此问题进行了详细的性能测试,并对其中的问题进行了深入的分析。

 

测试调研

测试场景

由于目前生产上使用Venus-cache 的有些域已经上了云,所以同时在物理机和虚拟机环境下进行压测,以下为测试环境参数:


测试结果 

物理机(Template的最大连接数100):

开源有坑,使用谨慎:缓存连接池开源组件剖析

云主机测试场景(Template的最大连接数20):

开源有坑,使用谨慎:缓存连接池开源组件剖析

可以看到,虽然平台不同,但是测试结果是有共性的:

  • Template的吞吐量大于Connection

  • Template能够快速进入到QPS的峰值

  • Template在到达拐点后,会随着线程的增多,QPS大幅下降

  • Connection随着线程数量的增多,QPS一直稳定上升,并最终超过Template

这里有几个问题:

  • 为什么 Template 会随着线程的增多性能下降?

  • 为什么 Connection 会随着线程的增多性能反而上升?

  • 为什么 Template 的吞吐量比 Connection 的大?


报告分析

上一节的三个问题,将通过源码分析,JMC观察,以及JMH进行各种微测试逐个进行解答。

这里先解释一些前置条件:

·        Memcached 我们使用的是 SpyMemcached 的框架,该框架和 Jedis 最大的不同是采用了 NIO 模型。对于JedisPool来说,对象池里的对象,是一个封装过的 Socket 对象,但是 SpyMemcaced MemcahcedClient本身是一个线程对象,并且其自身会不停的进行 selector.select()操作。
也就是说,一个对象池里的对象就是一个线程,这点需要注意,相比传统的连接池,Template里池的对象会重很多。

1. 为什么 Template 的吞吐量比 Connection 的大?

这个问题比较好回答,在没有达到CPU核数之前,只使用一个 MemcachedConnection也就是一个线程来处理 NIO并没有发挥出完整的性能,而 Template 能够充分发挥电脑多核的性能优势,从而达到更高的吞吐量。

2. 为什么 Template 会随着线程的增多性能下降,而Connection却随着线程数上升性能上升?

先排除掉池中对象本身的开销,Template主要就是增加了Commons-pool的获取和归还开销。那第一个问题如果单一的来看,其实可以很简单的回答,因为锁的原因,获取对象需要获取锁,线程越多,获取锁的难度越大,并且随着线程增多,上下文切换也势必会增多。但是,如果结合起来看,就会变得很奇怪,因为 Connection 随着线程的增多,反而性能上升,而且 Connection 中也有着锁,为什么相同的场景和条件,缺截然相反的两种结果呢?

首先想到,其问题可能出现在Commons-pool上,于是采用JMH进行了微基准测试,得到了如下结果(最大对象为100)

borrowObject,再 returnObject 为一次操作,压测出如下结果:

开源有坑,使用谨慎:缓存连接池开源组件剖析

多线程进行offer操作,单线程进行drainTo操作,一次offer为一次操作:可以看到,正如之前的猜想,Commons-pool果然随着线程的增大,性能大幅下降。为了进行对比,研究了SpyMemcached的源码,并发现其多线程竞争主要是在一个LinkedBlockingQueue上,于是继续用 JMH 有模拟了Spy Queue操作。

开源有坑,使用谨慎:缓存连接池开源组件剖析

继续怀疑是否是Commons-pool中由于锁的关系导致了等待时间的增长,从而导致QPS的下降,于是观察JMC中的线程等待时间观察到LinkedBlockingQueue随着线程的增多,QPS虽然有所减少,但是减少的很有限。

开源有坑,使用谨慎:缓存连接池开源组件剖析

Commons-pool

开源有坑,使用谨慎:缓存连接池开源组件剖析

LinkedBlockingQueue


·        LinkedBlockingQueue               等待了6,224,464

·        Commons-pool                          等待了5,745,601

Commons-pool的锁等待次数竟然比LinkedBlockingQueue 还要少,为什么会出现如此反差?需要进一步深入Commons-pool源码查看原因。

通过查看Commons-pool源码发现,Commons-pool内部使用的是 LinkedBlockingDeque

并且发现,一次操作(borrow& return),需要获取4次锁!分别是:

  1. borrow过程中,先进行pollFirst

  2. 如果pollFirst不成功,则进行pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS) 调用

  3. return过程中,需要进行判断 size(),如果小于则放入池中

  4. 如果池未满,则需要 addLast(p)

LinkedBlockingDeque 中只有一把lock,生产者和消费者共用了一把锁。而LinkedBlockingQueue,里面有两把锁,一把putLock,一把takeLock,也就是说,offer drainTo两个操作不互相阻塞。

结合代码中的实现:Commons-pool之所以随着线程的增多,性能大幅下降,主要是因为,一次操作需要抢四次锁,并且没有puttake锁分离,导致了需要归还对象的线程抢到锁很难,而抢到锁的线程可能此时池子里又没有对象,大量的锁操作以及无用切换导致了QPS大幅降低。

LinkedBlockingQueue之所以线程增多性能不怎么变化,是因为,只要抢到锁,一定就完成了一次操作(业务线程只抢put)。无需再等待其他操作。

通过以上分析进一步认为,如果Commons-pool中存放了1500个对象,那由于每次抢到锁,也能保证一次操作的完成,而不存在我归还对象时抢不到锁,获取对象时抢到锁却没对象的尴尬场景,通过JMH再次实验:

最后总结下,Commons-pool之所以这么慢,首先是一次操作需要获取锁的次数多,其次是锁没有分为puttake两把,导致了returnborrow都会互相阻塞对方。果然如分析的那样,QPS又回到了200W的上限。但是,由于Template中存放的是一个Thread对象,再不改变 TLAB 大小的情况下,一个线程差不多就要1M,并且还需要大量的上下文切换,再考虑上GC的影响,是绝对不可能开的这么大的。


解决方案

使用多个Connection放在数组中,采用轮询或者Thread-Id取模的方式来获取Connection使用。


以上是关于开源有坑,使用谨慎:缓存连接池开源组件剖析的主要内容,如果未能解决你的问题,请参考以下文章

数据库连接池为什么首选Druid

开源组件分布式缓存---Memcached

开源RabbitMQ操作组件

开源交流丨一站式大数据平台运维管家ChengYing安装原理剖析

[重磅开源] 比SingleR更适合的websocket 即时通讯组件---ImCore开源了

常用 iOS 开源库和第三方组件