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,这次问题定位发生在一年前,当时我以为它的工作方式是像上面划掉的方式,其实,它可以归纳为三种工作方式:

  1. 限定容量,超了进行删除;
  2. 不限定容量,依赖SoftReference的特性,由jvm的gc来回收;
  3. 不限定容量,也不删除;
// 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创建sessionCachemaxSize为0(不限定容量),所以使用MemoryCache第二种工作方式。由于我们系统用的是短链接且请求量和并发量很大,导致我们系统会不断并发创建并缓存ssl session,当某次gc删除的ssl session比较多,this.emptyQueue()操作就会进行比较长时间,其他put调用就会同步等待,这就造成了创建ssl socket的卡顿。

问题总结

这次的问题由以下几个因素共同作用下导致的:

  1. MemoryCache的同步put操作;
  2. MemoryCache的tricky工作方式;
  3. gc回收的不定性,导致this.emptyQueue()操作时间不均匀;
  4. 大量创建ssl session,如大量短连接请求;

解决方案

根本的原因是缓存在连接关闭时没有被释放而是由MemoryCache来被动释放。基于这个点,有两个方向:1. 关闭缓存;2. 主动释放,避免this.emptyQueue()出现过长的操作时间;
对于1,查了一下,没有好的关闭方案;
对于2,可以通过手动创建httpclientssl context,并通过sslContext.getClientSessionContext().setSessionCacheSize(maxSize);来设定cache的大小。或者,在不改代码的情况下,增加java启动参数-Djavax.net.ssl.sessionCacheSize=maxSize