SSLSessionContext内存占用分析

3,793 阅读3分钟

现象

  • 线上服务一台实例出现OOM
  • 个别实例出现内存使用情况较高

问题排查

表象

image-20200608165215231

SSLSessionImpl 占用大部分空间。

查看相关代码,代码中使用了 org.apache.http.impl.client.CloseableHttpClient 。(在某些博客中表示使用 CloseableHttpClient 可解决问题,但此处明显无效)。

HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setMaxConnTotal(100);
        httpClient = builder.build();

深入 HttpClientBuilder.build()

image-20200608165645955

可以看到当没有指定 SSLContext 时,内部使用了默认的 SSLContext

根因

同时,根据MAT的分析报告,我们是 SSLSessionContextImpl 中的 LinkedHashMap 属性造成了内存大量占用。在代码中,我们也是没有相关配置的,深入看下相关的默认配置。

image-20200608173216437

可以看到 SSLSessionContextImpl 中维护了一个 Session 的缓存,同时有两个相关的配置 cacheLimit / timeout ,这两个配置结合官方文档的解释,原因就明了了。

  • timeout:默认为 86400s ,即 24h。

image-20200608173356269

  • cacheLimit:默认为0,即无限制。

image-20200608173420768

结合两个参数,那么默认的 SSLSessionContextImpl 就是对24小时以内的 SSL 连接无限制进行缓存。

疑点

真的是不设置参数,SSLSession 缓存就不会回收了吗?

实际上,sessionCache 是软引用对象(在后续会详细说明),当内存不足时,GC最终还是可以回收掉的。虽然不致死,但它还是会带来问题:

  • 频繁触发GC
  • GC过程中回收该对象,需要遍历Cache中的 ReferenceQueuecacheMap
  • cache 中每个对象对于GC 都是 一视同仁的,所以无论是刚创建的还是已经保留了接近一天的缓存,都会被清除(实际场景中,我们会希望保留刚创建的缓存)

解决方案

综上,实例OOM的原因并非是 SSLSession 的缓存在允许时间内的无限堆积,实际上缓存是可以被回收的。 而由于没有更多的线索,暂无法确定OOM的原因。

此处仅提供了缓兵之策,用于降低 SSLSession 缓存的内存占用。

SSLContext sslContext = SSLContext.getInstance("SSL");
// 根据业务场景,填写一个大于0的合适值
sslContext.getServerSessionContext().setSessionCacheSize(1000);

扩展

CloseableHttpClient 能解决什么问题?

为什么 CloseableHttpClient 不能解决问题?

image-20200608175729498

CloseableHttpClient 实现了 Closeable 接口特性。

Closeable 的定义是:

/**
 * A {@code Closeable} is a source or destination of data that can be closed.
 * The close method is invoked to release resources that the object is
 * holding (such as open files).
 *
 * @since 1.5
 */
public interface Closeable extends AutoCloseable {}

Closeable 是定义一个可关闭的资源,类似文件流。但实际上Http 连接是被 SSLSessionContextImplsessionCache 属性引用了,所以即使连接关了,但由于被引用了,资源还是不能被回收。

SSLSessionImpl 缓存是如何被回收的?

SSLSessionContextImpl 中使用了 MemoryCache 作为缓存实现,MemoryCache 里面是使用了 ReferenceQueue 来监听缓存对象引用的状态,会清理即将被回收的 Reference 对象。

ReferenceReferenceQueue 的深入分析可参考另一篇文章:Java - Reference 小记

final class SSLSessionContextImpl implements SSLSessionContext {
    private Cache<SessionId, SSLSessionImpl> sessionCache;
  
    SSLSessionContextImpl() {
        this.sessionCache = Cache.newSoftMemoryCache(this.cacheLimit, this.timeout);
        this.sessionHostPortCache = Cache.newSoftMemoryCache(this.cacheLimit, this.timeout);
    }
}

public abstract class Cache<K, V> {
  public static <K, V> Cache<K, V> newSoftMemoryCache(int var0, int var1) {
      return new MemoryCache(true, var0, var1)
  }
}

class MemoryCache<K, V> extends Cache<K, V> {
    private static final float LOAD_FACTOR = 0.75F;
    private static final boolean DEBUG = false;
    private final Map<K, MemoryCache.CacheEntry<K, V>> cacheMap;
    private int maxSize;
    private long lifetime;
    private final ReferenceQueue<V> queue;

    public MemoryCache(boolean var1, int var2, int var3) {
        this.maxSize = var2;
        this.lifetime = (long)(var3 * 1000);
        if (var1) {
          this.queue = new ReferenceQueue();
        } else {
            this.queue = null;
        }

        int var4 = (int)((float)var2 / 0.75F) + 1;
        this.cacheMap = new LinkedHashMap(var4, 0.75F, true);
    }
  	
    protected MemoryCache.CacheEntry<K, V> newEntry(K var1, V var2, long var3, ReferenceQueue<V> var5) {
      // 当 ReferenceQueue 存在时,则生成软引用节点
      // 同时在初始化SoftCacheEntry 时传入了 RQ
      return (MemoryCache.CacheEntry)(var5 != null ? new MemoryCache.SoftCacheEntry(var1, var2, var3, var5) : new MemoryCache.HardCacheEntry(var1, var2, var3));
    }
  
  	// SoftCacheEntry 继承了 SoftReference
		private static class SoftCacheEntry<K, V> extends SoftReference<V> implements MemoryCache.CacheEntry<K, V> {
        SoftCacheEntry(K var1, V var2, long var3, ReferenceQueue<V> var5) {
          	// 使用父类初始化方法,指定了 RQ
            super(var2, var5);
            this.key = var1;
            this.expirationTime = var3;
        }
    }
}

参考