Question Description:
问题 1: 我们的消费服务,最近一直频繁fullGc,大概30分钟一次,一次fullGc时间超过500毫秒触发报警
Troubleshooting Process
首先观察了一下监控大盘,看到了老年代在持续直线式上涨,然后登录到相应的服务器,看了下当前服务器的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-
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可以更方便的看出其引用关系。
这里可以按Objects个数排序,很好的帮我们分析出内存泄漏的类,找到那些可达的对象但是无用的对象,以及一些重用创建的对象.图上两处引起了我的注意,DisruptorLogEvent是我们用来推埋点的,另外占用内存比较高的部分都是跟socket连接相关.
在 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
- 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大小的链接。
回收过程:
- 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根名字 |
- 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链接带来的开销