Java技术控Java技术升华面试精选

记一次线上突发频繁fullGC的分析与解决

2021-10-26  本文已影响0人  谁叫我土豆了

前情概要

4月份某天下午刚上班,春困之际,整个人还不是非常的清醒,结果钉钉开始收到告警,线上一台服务在非常频繁fullGC,一下子,整个人清醒多了,这个不是一个简单的告警,对服务的影响非常大。确实如此,没过几分钟,下游服务开始调用超时告警

我们公司内部的APM工具是pinpoint,可以看到服务超时13:50~14:03这段时间内服务响应时间有很多超过了5000ms

找到出问题的那台"那台实例

红线表示fullGC,基本上这个实例处于不可用的状态,分发到这个实例的请求基本上也就是超时,其他实例此时正常,我们服务总共部署了五个实例,只有这个实例出了问题

快速恢复

问题分析

JVM参数调整

整个调整的思路是尽可能最小化"短暂对象"移动到老年代的数量,避免老年代快速膨胀,触发majorGC或者fullGC,进而导致服务STW,影响业务,但是这个调整也无法避免代码导致的极端情况

-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

代码逻辑调整

这里的解决思路是,限制代码大批量数据查询,找出代码里大批量查询数据库的坏代码并修复

我这里采用了第二种方案,插件代码如下:

@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))
@Slf4j
public class QueryDataSizeInterceptor implements Interceptor {

    /**
     * 查询条数限制,超过打印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) {

    }
}

大部分代码都是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");
            // 某次查询超过配置的条数时,打印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));
        }
    }

代码逻辑: 某次查询超过配置的条数时,打印warn日志。并在日志平台配置对应日志的钉钉告警

再次出现

有了日志,通过traceId马上就能找到对应代码了,可以看到这里从数据库查询30多万数据到内存,触发fullgc也是正常的

Long total = protocolQualificationManager.count(criteria);

            if (total == 0) {
                return Response.ok(new Paging<>(0L, Collections.EMPTY_LIST));
            }
            //List<AgProtocolQualification> result = agProtocolQualificationDao.paging(criteria);
            List<AgProtocolQualification> result = protocolQualificationManager.paging(criteria);
            Set<Long> protocolIds = FluentIterable.from(result).transform(k -> k.getProtocolId()).toSet();

            // 这个查询出了问题
            List<AgProtocol> protocols = agProtocolDao.queryByIds(Lists.newArrayList(protocolIds));

代码看起来没啥问题呀,<typo id="typo-5521" data-origin="在" ignoretag="true">在</typo>看对应的查询的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>

坑点和教训

作者:政采云技术团队
链接:https://juejin.cn/post/7023164662187294733

上一篇 下一篇

猜你喜欢

热点阅读