<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 5888字,需瀏覽 12分鐘

           ·

          2021-10-29 12:36

          前情概要

          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堆文件

          問題分析

          • 原因分析

            1. 根據(jù)以上現(xiàn)象,猜測應(yīng)該是某個不常用的請求或者某種特殊的場景導(dǎo)致內(nèi)存加載了大量數(shù)據(jù),正好這個請求是由出問題的這個實例來處理的。

            2. 因為服務(wù)了過了一會就恢復(fù)了正常,服務(wù)日志里也找不到任何的有用的信息,分析陷入了瓶頸,但這個問題只要出現(xiàn)一次,就會導(dǎo)致服務(wù)基本上不可用,所以還是要找到根本的原因,徹底的根治這個問題,避免后續(xù)產(chǎn)生更大的影響。

            3. 我們的服務(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)載請注明出處。



          瀏覽 44
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  777婷婷天堂综合区色吧 | 豆花视频成人版WWW18 | 九九九九九九九九九九九九九九十九 免费 琪琪先锋 torrent magnet | 五月天婷婷黄色 | 天天干天天爱天天爽 |