记一次线上fullGc引发的思考


Question Description:

问题 1: 我们的消费服务,最近一直频繁fullGc,大概30分钟一次,一次fullGc时间超过500毫秒触发报警


Troubleshooting Process

  1. 首先观察了一下监控大盘,看到了老年代在持续直线式上涨,然后登录到相应的服务器,看了下当前服务器的CPU、内存分配,然后做了简单的排查,看下有没有死锁,或者批量大对象分配啥的.
    1). top -Hp ProcessId
    观察到4个线程占用了比较高的CPU
    2). printf '%x\n' ThreadId (转16进制,Id)
    3). jstack来分析这几个线程的堆栈信息
    jstack ProcessId|grep -A 30 Id
    通过线程号找到对应服务的owner, 了解到这个线程一直在监听kafka,消费kafka消息,业务代码里面业务逻辑也比较简单,每次批量的pull一批数据,然后只是简单的读写redis

  2. dump文件
    首先拉出一台服务,jmap命令生成dump文件,用内存工具打开dump文件.
    1)首先观察直方图和支配树,做两个维度对比.
    MAT中Histogram的主要作用是查看一个instance的数量,一般用来查看自己创建的类的实例的个数。可以很容易的找出占用内存最多的几个对象,根据Percentage(百分比)来排序。可以分不同维度来查看对象的Dominator Tree视图,Group by class、Group by class loader、Group by package和Histogram类似,时间久了,通过多次对比也可以把溢出对象找出来。Dominator Tree和Histogram的区别是站的角度不一样, Histogram是站在类的角度上去看,Dominator Tree是站的对象实例的角度上看,Dominator Tree可以更方便的看出其引用关系。


    image.png

这里可以按Objects个数排序,很好的帮我们分析出内存泄漏的类,找到那些可达的对象但是无用的对象,以及一些重用创建的对象.图上两处引起了我的注意,DisruptorLogEvent是我们用来推埋点的,另外占用内存比较高的部分都是跟socket连接相关.


image.png

在 dominator_tree 中可以看到大对象 Finalizer 的 Retained Heap 列是指该对象 GC 之后所能回收到内存的总和,可以看出由 Finalizer 关联的引用所占的空间最多。看到这里心里大概有些端倪了,查看 dump 内存中的不可达对象中,org.apache.commons.pool.impl.CursorableLinkedList$Listable 对象非常多.在支配树中查看该对象的引用关系发现 Listable 中存的 value 是 GenericKeyedObjectPool 的内部类 ObjectTimestampPair,ObjectTimestampPair 中存的 value 指向的是 thrift 通信所用的 TSocket, TSocket 中封装着 jdk 的 java.net.Socket。Socket 中使用的 SocketImpl 的实现是 SocksSocketImpl,在 SocksSocketImpl 的父类 AbstractPlainSocketImpl 中,重写了 finalize()方法,从注释可以看出来,该方法的作用是:为了防止用户忘记关闭资源,当 SocksSocketImpl 被回收时,finalize 被调用执行清理工作,SocksSocketImpl 的 close() 方法体中也是直接调用 AbstractPlainSocketImpl 的 close()。

Redis Link Pool Analysis

  1. Redis client 采用的是连接池的方式,实现在apache的commons.pool2里面,翻看源代码发现有个定时任务检测对象。关键代码如下:
@Override
    public void evict() throws Exception {
        assertOpen();

        if (idleObjects.size() > 0) {

            PooledObject underTest = null;
            final EvictionPolicy evictionPolicy = getEvictionPolicy();

            synchronized (evictionLock) {
                final EvictionConfig evictionConfig = new EvictionConfig(
                        getMinEvictableIdleTimeMillis(),
                        getSoftMinEvictableIdleTimeMillis(),
                        getMinIdle());

                final boolean testWhileIdle = getTestWhileIdle();

                for (int i = 0, m = getNumTests(); i < m; i++) {
                    if (evictionIterator == null || !evictionIterator.hasNext()) {
                        evictionIterator = new EvictionIterator(idleObjects);
                    }
                    if (!evictionIterator.hasNext()) {
                        // Pool exhausted, nothing to do here
                        return;
                    }

                    try {
                        underTest = evictionIterator.next();
                    } catch (final NoSuchElementException nsee) {
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        i--;
                        evictionIterator = null;
                        continue;
                    }

                    if (!underTest.startEvictionTest()) {
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        i--;
                        continue;
                    }

                    // User provided eviction policy could throw all sorts of
                    // crazy exceptions. Protect against such an exception
                    // killing the eviction thread.
                    boolean evict;
                    try {
                        evict = evictionPolicy.evict(evictionConfig, underTest,
                                idleObjects.size());
                    } catch (final Throwable t) {
                        // Slightly convoluted as SwallowedExceptionListener
                        // uses Exception rather than Throwable
                        PoolUtils.checkRethrow(t);
                        swallowException(new Exception(t));
                        // Don't evict on error conditions
                        evict = false;
                    }

                    if (evict) {
                        destroy(underTest);
                        destroyedByEvictorCount.incrementAndGet();
                    } else {
                        if (testWhileIdle) {
                            boolean active = false;
                            try {
                                factory.activateObject(underTest);
                                active = true;
                            } catch (final Exception e) {
                                destroy(underTest);
                                destroyedByEvictorCount.incrementAndGet();
                            }
                            if (active) {
                                if (!factory.validateObject(underTest)) {
                                    destroy(underTest);
                                    destroyedByEvictorCount.incrementAndGet();
                                } else {
                                    try {
                                        factory.passivateObject(underTest);
                                    } catch (final Exception e) {
                                        destroy(underTest);
                                        destroyedByEvictorCount.incrementAndGet();
                                    }
                                }
                            }
                        }
                        if (!underTest.endEvictionTest(idleObjects)) {
                            // TODO - May need to add code here once additional
                            // states are used
                        }
                    }
                }
            }
        }
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
            removeAbandoned(ac);
        }
    }

从上面代码我们看出,每隔一段时间,就是检测对象池里面对象,redis连接池采用驱逐对象的手段,会淘汰一些空闲链接,强制回收;当发现链接少于最小空闲链接数时候,又重新创建对象链接,以维持mindle大小的链接。

回收过程:


image.png
  1. Link Pool Parametes
属性 类型 默认值 描述
maxTotal int 8 池中最多可用的实例个数
maxIdle int 8 池中最多可容纳的实例(instances)个数
minIdle int 0 池中最少需要容纳的实例(instances)个数
lifo boolean TRUE 资源的存取数据结构,默认值 true,true 资源按照栈结构存取,false 资源按照队列结构存取
fairness boolean FALSE 当从池中获取资源或者将资源还回池中时 是否使用 java.util.concurrent.locks.ReentrantLock.ReentrantLock 的公平锁机制。 默认值 false, true 使用公平锁,false 不使用公平锁,
maxWaitMillis long -1 获取资源时的等待时间,单位毫秒。当 blockWhenExhausted 配置为 true 时,此值有效。 -1 代表无时间限制,一直阻塞直到有可用的资源。
minEvictableIdleTimeMillis long 1800000毫秒 池中对象处于空闲状态开始到被回收的最短时间
softMinEvictableIdleTimeMillis long 1800000毫秒 池中对象处于空闲状态开始到被回收的最短时间
numTestsPerEvictionRun int 3 资源回收线程执行一次回收操作,回收资源的数量。默认值 3, (int类型)。备注: 当设置为0时,不回收资源。设置为 小于0时,回收资源的个数为 (int)Math.ceil( 池中空闲资源个数 / Math.abs(numTestsPerEvictionRun) ); 设置为 大于0时,回收资源的个数为 Math.min( numTestsPerEvictionRun,池中空闲的资源个数 );
evictionPolicyClassName String org.apache.commons.pool2. impl.DefaultEvictionPolicy 回收策略
testOnCreate boolean FALSE 调用borrowObject方法时,依据此标识判断是否需要对返回的结果进行校验,如果校验失败会删;除当前实例,并尝试再次获取
testOnBorrow boolean FALSE 默认值 false ,当设置为true时,每次从池中获取资源时都会调用 factory.validateObject() 方法
testOnReturn boolean FALSE 调用returnObject方法时,依据此标识判断是否需要对返回的结果进校验
testWhileIdle boolean FALSE 闲置实例校验标识,如果校验失败会删除当前实例
timeBetweenEvictionRunsMillis long -1 回收资源线程的执行周期,单位毫秒。默认值 -1 ,-1 表示不启用线程回收资源
blockWhenExhausted boolean true 当池中对象都被借出后,客户端来租借对象,此时是否进行阻塞还是非阻塞,默认阻塞
jmxEnabled boolean true 开启JMX开关
jmxNamePrefix String pool JMX前缀
jmxNameBase String null JMX根名字
  1. GenericObjectPool Implement
    3.1 borrowObject
 public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
        //判断对象池是否关闭:BaseGenericObjectPool.closed==true
        assertOpen();
        //如果回收泄漏的参数配置不为空,并且removeAbandonedOnBorrow参数配置为true
        //并且Idle数量<2,Active数量>总数Total-3
        //在借用时进行回收泄漏连接(会影响性能)
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
                (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3) ) {
            //回收泄漏对象
            removeAbandoned(ac);
        }

        PooledObject p = null;

        //copy blockWhenExhausted 防止其它线程更改getBlockWhenExhausted值造成并发问题
        //借用对象时如果没有是否阻塞直到有对象产生
        final boolean blockWhenExhausted = getBlockWhenExhausted();
        //创建成功标识
        boolean create;
        //记录当前时间,用作记录借用操作总共花费的时间
        final long waitTime = System.currentTimeMillis();
        //当对象为空时一直获取
        while (p == null) {
            create = false;
            //从双端队列弹出第一个队首对象,为空返回null
            p = idleObjects.pollFirst();
            //如果为空则重新创建一个对象
            if (p == null) {
                //创建对象
                p = create();
                //p==null可能对象池达到上限不能继续创建!
                if (p != null) {
                    create = true;
                }
            }
            //如果对象p还是为空则阻塞等待
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        //没有超时时间则阻塞等待到有对象为止
                        p = idleObjects.takeFirst();
                    } else {
                        //有超时时间
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
                //达到超时时间,还未取到对象,则抛出异常
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
            } else {
                //未取到对象,则抛出异常
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
            }
            //调用PooledObject.allocate()方法分配对象
            //[具体实现请看](https://blog.csdn.net/qq447995687/article/details/80413227)
            if (!p.allocate()) {
                p = null;
            }
            //分配成功
            if (p != null) {
                try {
                    //激活对象,具体请看factory实现,对象重借出到归还整个流程经历的过程图
                    factory.activateObject(p);
                } catch (final Exception e) {
                    try {
                        destroy(p);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                //对象创建成功,是否进行测试
                if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        //校验对象,具体请看factory实现,对象重借出到归还整个流程经历的过程图
                        validate = factory.validateObject(p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    //校验不通过则销毁对象
                    if (!validate) {
                        try {
                            destroy(p);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                    "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }
        //更新对象借用状态
        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

        return p.getObject();
    }

3.2 createObject

private PooledObject create() throws Exception {
        int localMaxTotal = getMaxTotal();
        // 如果最大数量为负数则设置为Integer的最大值
        if (localMaxTotal < 0) {
            localMaxTotal = Integer.MAX_VALUE;
        }

        // 是否创建成功的一个flag:
        // - TRUE:  调用工厂类成功创建一个对象
        // - FALSE: 返回空
        // - null:  重复创建
        Boolean create = null;
        while (create == null) {
            synchronized (makeObjectCountLock) {
                //加上本次操作,总共创建个数
                final long newCreateCount = createCount.incrementAndGet();
                if (newCreateCount > localMaxTotal) {
                    //连接池容量已满,不能继续增长。在对最后一个对象的创建上,
                    //加入了设计较为精妙,需细细揣摩
                    createCount.decrementAndGet();
                    //调用创建对象方法线程数=0
                    if (makeObjectCount == 0) {
                        //容量已满并且没有线程调用makeObject()方法,
                        //表明没有任何可能性再继续创建对象,
                        //返回并等待归还的空闲对象
                        create = Boolean.FALSE;
                    } else {
                        //其它线程调用makeObject()方法在创建对象了。
                        //如果继续创建则可能超过对象池容量,不返回false,因为其它线程也在创建,
                        //但是是否能够创建成功是未知的,如果其它线程没能创建成功,
                        //则此线程可能会抢夺到继续创建的权利。
                        //释放锁,等待其它线程创建结束并唤醒该线程
                        makeObjectCountLock.wait();
                    }
                } else {
                    // 对象池未满,从新创建一个对象
                    makeObjectCount++;
                    create = Boolean.TRUE;
                }
            }
        }
        //对象池容量达到上限,返回null重新等待其它线程归还对象
        if (!create.booleanValue()) {
            return null;
        }

        final PooledObject p;
        try {
            //创建一个新对象
            p = factory.makeObject();
        } catch (final Exception e) {
            createCount.decrementAndGet();
            throw e;
        } finally {
            //与上面wait()方法相呼应,
            //如果上面抛出了异常,唤醒其它线程争夺继续创建最后一个资源的权利
            synchronized (makeObjectCountLock) {
                makeObjectCount--;
                makeObjectCountLock.notifyAll();
            }
        }
        //设置泄漏参数,并加入调用堆栈
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getLogAbandoned()) {
            p.setLogAbandoned(true);
            // TODO: in 3.0, this can use the method defined on PooledObject
            if (p instanceof DefaultPooledObject) {
                ((DefaultPooledObject) p).setRequireFullStackTrace(ac.getRequireFullStackTrace());
            }
        }
        //将创建总数增加,并将对象放入    allObjects  
        createdCount.incrementAndGet();
        allObjects.put(new IdentityWrapper<>(p.getObject()), p);
        return p;
    }

3.3 removeAbandoned

private void removeAbandoned(final AbandonedConfig ac) {
        // Generate a list of abandoned objects to remove
        final long now = System.currentTimeMillis();
        //超时时间=当前时间-配置的超时时间,如果一个对象的上次借用时间在此时间之前,
        //说明上次借用后经过了removeAbandonedTimeout时间限制还未被归还过即可能是泄漏的对象
        final long timeout =
                now - (ac.getRemoveAbandonedTimeout() * 1000L);
        //泄漏的,需要移除的对象列表
        final ArrayList> remove = new ArrayList<>();
        final Iterator> it = allObjects.values().iterator();
        //遍历池中对象依次判断是否需要移除
        while (it.hasNext()) {
            final PooledObject pooledObject = it.next();
            synchronized (pooledObject) {
                //如果对象的状态为已分配ALLOCATED ,并且已经超过泄漏定义时间则添加到需要移除队列进行统一移除
                if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
                        pooledObject.getLastUsedTime() <= timeout) {
                    pooledObject.markAbandoned();
                    remove.add(pooledObject);
                }
            }
        }
        // 移除泄漏连接,如果配置了打印堆栈,则打印调用堆栈信息
        final Iterator> itr = remove.iterator();
        while (itr.hasNext()) {
            final PooledObject pooledObject = itr.next();
            if (ac.getLogAbandoned()) {
                pooledObject.printStackTrace(ac.getLogWriter());
            }
            try {
                //销毁对象
                invalidateObject(pooledObject.getObject());
            } catch (final Exception e) {
                e.printStackTrace();
            }
        }
    }

3.3 invalidateObject

public void invalidateObject(final T obj) throws Exception {
        //从所有对象中取出该对象,如果不存在则抛出异常
        final PooledObject p = allObjects.get(new IdentityWrapper<>(obj));
        if (p == null) {
            if (isAbandonedConfig()) {
                return;
            }
            throw new IllegalStateException(
                    "Invalidated object not currently part of this pool");
        }
        //如果对象不是无效状态PooledObjectState.INVALID,则销毁此对象
        synchronized (p) {
            if (p.getState() != PooledObjectState.INVALID) {
                destroy(p);
            }
        }
        //
        ensureIdle(1, false);
    }

3.4 destroyObject

private void destroy(final PooledObject toDestroy) throws Exception {
        toDestroy.invalidate();
        idleObjects.remove(toDestroy);
        allObjects.remove(new IdentityWrapper<>(toDestroy.getObject()));
        try {
            factory.destroyObject(toDestroy);
        } finally {
            destroyedCount.incrementAndGet();
            createCount.decrementAndGet();
        }
    }

3.5 ensureIdle

    //!idleObjects.hasTakeWaiters()如果idleObjects队列还有线程等待获取对象则由最后一个
        //等待者确保最小空闲数量
        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
            return;
        }
        //一直创建空闲对象知道空闲对象数量>总空闲数量阈值
        while (idleObjects.size() < idleCount) {
            final PooledObject p = create();
            if (p == null) {
                // Can't create objects, no reason to think another call to
                // create will work. Give up.
                break;
            }
            //根据先进先出参数,添加对象到队首或者队尾
            if (getLifo()) {
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
        }
        //在此过程中如果连接池关闭则clear所有对象
        if (isClosed()) {
            clear();
        }
    }

3.6 returnObject

   public void returnObject(final T obj) {
        final PooledObject p = allObjects.get(new IdentityWrapper<>(obj));

        if (p == null) {
            //如果对象为空,并且没有配置泄漏参数则抛出异常,表明该对象不是连接池中的对象
            if (!isAbandonedConfig()) {
                throw new IllegalStateException(
                        "Returned object not currently part of this pool");
            }
            //如果对象为空,表明该对象是abandoned并且已被销毁
            return; 
        }

        synchronized(p) {
            final PooledObjectState state = p.getState();
            //如果被归还的对象不是已分配状态,抛出异常
            if (state != PooledObjectState.ALLOCATED) {
                throw new IllegalStateException(
                        "Object has already been returned to this pool or is invalid");
            }
            //更改状态为returning,避免在此过程中被标记为被遗弃。
            p.markReturning();
        }

        final long activeTime = p.getActiveTimeMillis();
        //是否在归还时测试该对象
        if (getTestOnReturn()) {
            //校验对象
            if (!factory.validateObject(p)) {
                try {
                    //校验不通过则destroy对象
                    destroy(p);
                } catch (final Exception e) {
                    swallowException(e);
                }
                try {
                    //确保最小空闲数量
                    ensureIdle(1, false);
                } catch (final Exception e) {
                    swallowException(e);
                }
                //更新连接池归还信息BaseGenericObjectPool#returnedCount,activeTimes
                updateStatsReturn(activeTime);
                return;
            }
        }
        //校验通过
        try {
            //钝化(卸载)对象
            factory.passivateObject(p);
        } catch (final Exception e1) {
            swallowException(e1);
            try {
                destroy(p);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }
        //结束分配,如果对象为ALLOCATED或者RETURNING更改对象为空闲IDLE状态
        //具体看org.apache.commons.pool2.impl.DefaultPooledObject#deallocate方法
        if (!p.deallocate()) {
            throw new IllegalStateException(
                    "Object has already been returned to this pool or is invalid");
        }

        final int maxIdleSave = getMaxIdle();
        //如果对象池已经关闭或者空闲数量达到上限,则销毁该对象
        if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {
                destroy(p);
            } catch (final Exception e) {
                swallowException(e);
            }
        } else {
            //否则将归还的对象添加到空闲队列,连接池的最终目的:重用一个连接
            if (getLifo()) {
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
            if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left
                // in the idle object pool (which would effectively be a leak)
                clear();
            }
        }
        updateStatsReturn(activeTime);
    }

3.7 clear Link

public void clear() {
        PooledObject p = idleObjects.poll();

        while (p != null) {
            try {
                destroy(p);
            } catch (final Exception e) {
                swallowException(e);
            }
            p = idleObjects.poll();
        }
    }

Solution

查看我们阿波罗上面配置的mindle配置较大,导致链接池里有大量空闲
maxTotal和maxTotal都配置一样,减少redis伸缩带来的性能影响
jvm启动增加并行回收加快回收速度,-XX:+ParallelRefProcEnabled
提升 SurvivorRatio 目标存活率调整Survivor区参数, 减少分配担保晋升
重启其中一台节点观察运行一段时间
优化后,线上大概一天fullGc一次,一次耗时大概在300毫秒.有了明显的改观.

Q & A

1.链接池的配置一定要匹配上当前的并发数
2.慎用Finalizer
3.能否考略用NettyIo,减少频繁创建socket链接带来的开销

你可能感兴趣的:(记一次线上fullGc引发的思考)