一次線上突發(fā)頻繁fullGC的分析與解決
前情概要

4月份某天下午剛上班,春困之際,整個人還不是非常的清醒,結(jié)果釘釘開始收到告警,線上一臺服務(wù)在非常頻繁fullGC,一下子,整個人清醒多了,這個不是一個簡單的告警,對服務(wù)的影響非常大。確實如此,沒過幾分鐘,下游服務(wù)開始調(diào)用超時告警


我們公司內(nèi)部的APM工具是pinpoint,可以看到服務(wù)超時13:50~14:03這段時間內(nèi)服務(wù)響應(yīng)時間有很多超過了5000ms

找到出問題的那臺實例

紅線表示fullGC,基本上這個實例處于不可用的狀態(tài),分發(fā)到這個實例的請求基本上也就是超時,其他實例此時正常,我們服務(wù)總共部署了五個實例,只有這個實例出了問題
快速恢復(fù)
下線出問題的實例,記得這里先dump堆文件
問題分析
原因分析
根據(jù)以上現(xiàn)象,猜測應(yīng)該是某個不常用的請求或者某種特殊的場景導(dǎo)致內(nèi)存加載了大量數(shù)據(jù),正好這個請求是由出問題的這個實例來處理的。
因為服務(wù)了過了一會就恢復(fù)了正常,服務(wù)日志里也找不到任何的有用的信息,分析陷入了瓶頸,但這個問題只要出現(xiàn)一次,就會導(dǎo)致服務(wù)基本上不可用,所以還是要找到根本的原因,徹底的根治這個問題,避免后續(xù)產(chǎn)生更大的影響。
我們的服務(wù)加載數(shù)據(jù)的途徑有限,要么是數(shù)據(jù)庫查詢,要么是外部接口返回,根據(jù)dump文件其實可以看出來對象其實大部分都是我們內(nèi)部的實體對象(這里忘記截圖了),所以應(yīng)該是數(shù)據(jù)庫查詢返回了大批量數(shù)據(jù)。
解決思路
JVM參數(shù)調(diào)整: 調(diào)整JVM參數(shù),盡可能避免出現(xiàn)該問題
代碼邏輯調(diào)整: 找到問題代碼并修復(fù)
JVM參數(shù)調(diào)整
整個調(diào)整的思路是盡可能最小化"短暫對象"移動到老年代的數(shù)量,避免老年代快速膨脹,觸發(fā)majorGC或者fullGC,進(jìn)而導(dǎo)致服務(wù)STW,影響業(yè)務(wù),但是這個調(diào)整也無法避免代碼導(dǎo)致的極端情況
-Xmx5g
-Xms5g
-XX:MaxMetaspaceSize=512M
-XX:MaxTenuringThreshold=15
-XX:MetaspaceSize=512M
-XX:NewSize=2560M
-XX:MaxNewSize=2560M
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
-XX:+PrintGCApplicationStoppedTime
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=85
-Xloggc:/opt/zcy/modules/agreement-center/gc.log
-XX:CMSFullGCsBeforeCompaction=2
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
復(fù)制代碼調(diào)整新生代的大小:
-xx:NewSize=2560M,-xx:MaxNewSize=2560M, 我們堆大小為5g,調(diào)整新生代大小到2560M,為整個堆大小的一半,盡可能的讓更多的類可以放到新生代調(diào)整對象晉升到老年代的年齡閾值:
-XX:MaxTenuringThreshold=15, CMS中該值默認(rèn)為6,調(diào)整到15,讓對象盡可能保留在新生代,在新生代完成回收調(diào)整survivor區(qū)與Eden區(qū)的比例:
-xx:SurvivorRatio=8, 換算一下,Eden區(qū)大小等于2560M*0.8 = 2048M
代碼邏輯調(diào)整
這里的解決思路是,限制代碼大批量數(shù)據(jù)查詢,找出代碼里大批量查詢數(shù)據(jù)庫的壞代碼并修復(fù)
方案一:通過mybatis插件,全局查詢語句加上limit,限制最大的返回數(shù)據(jù),但是我們的業(yè)務(wù)中,經(jīng)常有關(guān)聯(lián)數(shù)據(jù)好幾萬條,這里其實數(shù)據(jù)結(jié)構(gòu)設(shè)計是不合理,這個limit大于好幾萬也就失去了意義,因為有些表單行記錄比較大,幾萬條記錄也有幾百兆,請求量大的時候,也會出現(xiàn)這個問題,而且也不能發(fā)現(xiàn)出問題的代碼,項目代碼太多了,看代碼找問題只能看緣分,不靠譜
方案二:也是通過mybatis插件,統(tǒng)計每次查詢結(jié)果的數(shù)量,大于某個閾值打印告警日志,實時監(jiān)控該日志,根據(jù)日志找到整個鏈路,進(jìn)而找到出問題的代碼
我這里采用了第二種方案,插件代碼如下:
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
@Slf4j
public class QueryDataSizeInterceptor implements Interceptor {
/**
* 查詢條數(shù)限制,超過打印warn日志
*/
private Integer querySizeLimit;
/**
* 是否開啟
*/
private Boolean isOpen;
public QueryDataSizeInterceptor(Integer querySizeLimit, Boolean isOpen) {
this.querySizeLimit = querySizeLimit;
this.isOpen =isOpen;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
if (isOpen) {
processIntercept(invocation.getArgs());
}
} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
return invocation.proceed();
}
private void processIntercept(final Object[] queryArgs) {
Statement statement = (Statement) queryArgs[0];
try {
HikariProxyResultSet resultSet = (HikariProxyResultSet) statement.getResultSet();
MetaObject metaObject1 = SystemMetaObject.forObject(resultSet);
RowDataStatic rs = (RowDataStatic) metaObject1.getValue("delegate.rowData");
if (Objects.nonNull(rs) && !rs.wasEmpty() && rs.size() >= querySizeLimit) {
MetaObject metaObject2 = SystemMetaObject.forObject(statement);
String sql = (String) metaObject2.getValue("delegate.originalSql");
log.warn("current.query.size.is.too.large,size:{},sql:{}",rs.size(), sql);
}
} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
復(fù)制代碼大部分代碼都是mybatis的插件模版代碼,核心代碼很簡單
private void processIntercept(final Object[] queryArgs) {
Statement statement = (Statement) queryArgs[0];
try {
HikariProxyResultSet resultSet = (HikariProxyResultSet) statement.getResultSet();
MetaObject metaObject1 = SystemMetaObject.forObject(resultSet);
RowDataStatic rs = (RowDataStatic) metaObject1.getValue("delegate.rowData");
// 某次查詢超過配置的條數(shù)時,打印warn日志
if (Objects.nonNull(rs) && !rs.wasEmpty() && rs.size() >= querySizeLimit) {
MetaObject metaObject2 = SystemMetaObject.forObject(statement);
String sql = (String) metaObject2.getValue("delegate.originalSql");
log.warn("current.query.size.is.too.large,size:{},sql:{}",rs.size(), sql);
}
} catch (Throwable throwable) {
log.warn("QueryDataSizeInterceptor.failed,cause:{}", Throwables.getStackTraceAsString(throwable));
}
}
復(fù)制代碼代碼邏輯: 某次查詢超過配置的條數(shù)時,打印warn日志。并在日志平臺配置對應(yīng)日志的釘釘告警
再次出現(xiàn)

有了日志,通過traceId馬上就能找到對應(yīng)代碼了,可以看到這里從數(shù)據(jù)庫查詢30多萬數(shù)據(jù)到內(nèi)存,觸發(fā)fullgc也是正常的
Long total = protocolQualificationManager.count(criteria);
if (total == 0) {
return Response.ok(new Paging<>(0L, Collections.EMPTY_LIST));
}
//List result = agProtocolQualificationDao.paging(criteria);
List result = protocolQualificationManager.paging(criteria);
Set protocolIds = FluentIterable.from(result).transform(k -> k.getProtocolId()).toSet();
// 這個查詢出了問題
List protocols = agProtocolDao.queryByIds(Lists.newArrayList(protocolIds));
復(fù)制代碼 代碼看起來沒啥問題呀,在看對應(yīng)的查詢的mapper
<select id="queryByIds" parameterType="java.util.List" resultMap="defaultResultMap">
SELECT
<include refid="allColumns"/>
FROM
ag_protocol
<where>
<if test="ids != null and ids.size != 0" >
and id in
<foreach collection="ids" open="(" close=")" separator="," item="id">
#{id}
foreach>
if>
<if test="ids == null or ids.size == 0" >
and false
if>
<include refid="not_deleted"/>
where>
select>
復(fù)制代碼這里沒有做限制,當(dāng)ids為null,全表查詢not_deleted的數(shù)據(jù),30多萬條記錄全部返回
坑點(diǎn)和教訓(xùn)
動態(tài)sql 如果所有條件都未匹配,不能直接查詢?nèi)恚瑧?yīng)該返回為空,要在代碼里或者mapper sql中加以限制
優(yōu)化業(yè)務(wù)數(shù)據(jù)結(jié)構(gòu),在代碼里加上limit限制
數(shù)據(jù)庫層面也要做限制,如果這里是大批量的刪除,可能業(yè)務(wù)影響會更大
作者:政采云技術(shù)團(tuán)隊
鏈接:https://juejin.cn/post/7023164662187294733
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
