HttpClient大量https短连接导致新https请求卡顿
公司的线上服务偶尔会出现请求某个地址几秒中的卡顿,刚好那段时间腾讯云的网络经常抖动,我们还以为这是网络抖动造成的。最后,通过抓包,我们发现卡顿都是在*httpclient*创建*sslsocket*时发生的。究竟是什么问题呢
相关版本
jre: 1.6.0_45-b06
httpclient: 4.3.5
背景
公司的线上服务偶尔会出现请求某个地址几秒中的卡顿,刚好那段时间腾讯云的网络经常抖动,我们还以为这是网络抖动造成的。最后,通过抓包,我们发现卡顿都是在httpclient创建ssl socket时发生的。究竟是什么问题呢?
问题分析
httpclient的使用SSLSessionContextImpl
(注意,这个类在openjdk6与jdk6中实现是不一样)来创建ssl socket,这个类会把每个创建完成的ssl session存放到sessionCache
。
在ClientHandshaker
的实现中,当握手完成后,程序会把当前session
缓存到sessionCache
中,即956行。
// ClientHandshaker缓存session代码
/* 954 */ if (!this.resumingSession) {
/* 955 */ if (this.session.isRejoinable()) {
/* 956 */ ((SSLSessionContextImpl)this.sslContext.engineGetClientSessionContext()).put(this.session);
/* */
/* */
/* 959 */ if (ClientHandshaker.debug != null && Debug.isOn("session")) {
/* 960 */ System.out.println("%% Cached client session: " + this.session);
/* */ } }
/* 962 */ else if (ClientHandshaker.debug != null && Debug.isOn("session")) {
/* 963 */ System.out.println("%% Didn't cache non-resumable client session: " + this.session);
/* */
/* */ }
/* */
/* */ }
sessionCache
使用MemoryCache
作为缓存实现,它是一个有生命周期、限定容量且同步添加的cache,当添加的时候,它的容量达到最大,它就会进行清理工作。
到这里,估计大家就知道什么触发卡顿了。
MemoryCache
的实现比较tricky,这次问题定位发生在一年前,当时我以为它的工作方式是像上面划掉的方式,其实,它可以归纳为三种工作方式:
- 限定容量,超了进行删除;
- 不限定容量,依赖
SoftReference
的特性,由jvm的gc来回收; - 不限定容量,也不删除;
// MemoryCache的put操作
/* */ public synchronized void put(final Object o, final Object o2) {
/* 336 */ this.emptyQueue();
/* */
/* */
/* */
/* 340 */ final CacheEntry cacheEntry = this.cacheMap.put(o, this.newEntry(o, o2, (this.lifetime == 0L) ? 0L : (System.currentTimeMillis() + this.lifetime), this.queue));
/* 341 */ if (cacheEntry != null) {
/* 342 */ cacheEntry.invalidate();
/* 343 */ return;
/* */ }
/* 345 */ if (this.maxSize > 0 && this.cacheMap.size() > this.maxSize) {
/* 346 */ this.expungeExpiredEntries();
/* 347 */ if (this.cacheMap.size() > this.maxSize) {
/* 348 */ final Iterator<CacheEntry> iterator = this.cacheMap.values().iterator();
/* 349 */ final CacheEntry cacheEntry2 = iterator.next();
/* */
/* */
/* */
/* */
/* 354 */ iterator.remove();
/* 355 */ cacheEntry2.invalidate();
/* */ }
/* */ }
/* */
httpclient使用的是SSLSessionContextImpl
创建sessionCache
时maxSize
为0(不限定容量),所以使用MemoryCache
第二种工作方式。由于我们系统用的是短链接且请求量和并发量很大,导致我们系统会不断并发创建并缓存ssl session,当某次gc删除的ssl session比较多,this.emptyQueue()
操作就会进行比较长时间,其他put
调用就会同步等待,这就造成了创建ssl socket的卡顿。
问题总结
这次的问题由以下几个因素共同作用下导致的:
MemoryCache
的同步put操作;MemoryCache
的tricky工作方式;- gc回收的不定性,导致
this.emptyQueue()
操作时间不均匀; - 大量创建
ssl session
,如大量短连接请求;
解决方案
根本的原因是缓存在连接关闭时没有被释放而是由MemoryCache
来被动释放。基于这个点,有两个方向:1. 关闭缓存;2. 主动释放,避免this.emptyQueue()
出现过长的操作时间;
对于1,查了一下,没有好的关闭方案;
对于2,可以通过手动创建httpclient的ssl context,并通过sslContext.getClientSessionContext().setSessionCacheSize(maxSize);
来设定cache的大小。或者,在不改代码的情况下,增加java启动参数-Djavax.net.ssl.sessionCacheSize=maxSize
。