在做模型項目的時候遇到一個問題,由於模型服務裝載一些大模型,大模型對象的大小在 300M 左右,而一台服務器可能裝載多個大模型。在服務啟動和模型更新的時候會遇到 young gc 耗時過長的問題,young gc 所采用的垃圾回收器是 ParNew。通過觀察 GC 日志可以發現,模型對象一開始是存在於年輕代的,當經過 15次 gc 后,這些對象就會進入到老年代,而之后 young gc 的時間縮短到正常可以接受的時間范圍 0.01s ~ 0.02s。而在模型對象尚未進入老年代時,young gc 耗時就會超過 0.3 s,導致線上請求模型會超時。這個原因主要是年輕代回收的過程中,標記過程其實耗時並不長,而長的是 survivor 區復制對象造成的耗時。
既然定位到問題,那么就需要解決問題,解決方案就是讓這種大模型立馬進入老年代(由於在模型加載的時候通過控制調用方的路由表保證了服務器不對外提供服務,因此在這個階段可以讓模型對象進入老年代后再提供服務)。一種方法是調整最大晉升代的閾值,如果調整過小,擔心 old 區增長過快,而導致線上服務 old gc 不可控(因為每天會在非高峰期主動觸發 Old GC, System.gc()),於是采用第二種方式,生產對象主動觸發 Young GC,把模型對象擠入老年代。當然還有一種方法,不停的 System.gc(),這個過程相對耗時較長,而且線上服務一般會對 full gc 次數做監控,為了避免報警,所以最終沒有選擇這種簡單粗暴的方式。
實現第二個方案需要得知 Eden 區 大小,以及最大晉升代數,例如 Eden 區有 2G,最大代數有 10代,那么就可以通過生產 20G 的對象,把模型擠入老年代,因此需要獲得這兩個參數。通過觀察 gc 日志,以及命令都可以得知這些參數,而在運行期程序內部獲得這些參數一直沒有實現過。如果這些參數作為 properties,是可以通過 System.getProperties 這種方式來獲取參數的。但是,如果不作為系統屬性的話,該如何獲取呢。這就要通過 ManagementFactory 了。
先上代碼:
public class GCUtil { /** * 調用 System.gc */ public static void systemGC() { long startTime = System.currentTimeMillis(); LOGGER.info("Call for system gc start..."); System.gc(); LOGGER.info("Call for system gc end, spend {}ms", System.currentTimeMillis() - startTime); } /** * 保證當前年輕代進入老年代的 gc,用於模型更新以及剛上線期間 */ public static void youngPromoteGC() { try { long startTime = System.currentTimeMillis(); LOGGER.info("Call for young promote gc start..."); RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); List<String> args = runtimeMXBean.getInputArguments(); // 獲取 eden 區大小 long edenMSize = 0; for (String arg : args) { LOGGER.info("--------------- Jvm param: {}", arg); if (arg.startsWith(JVM_XMN)) { String edenSizeStr = arg.substring(JVM_XMN.length()); if (edenSizeStr.toLowerCase().endsWith("g")) { long edenGSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1)); edenMSize = edenGSize * 1024; } else if (edenSizeStr.toLowerCase().endsWith("m")) { edenMSize = Long.valueOf(edenSizeStr.substring(0, edenSizeStr.length() - 1)); } else { LOGGER.warn("Cannot recognize -Xmn argument: " + JVM_XMN); } } } if (edenMSize == 0) { edenMSize = getEdenMemorySize(); } if (edenMSize <= 0) { LOGGER.warn("Extract jvm -Xmn failed"); return; } // 獲取 晉升老年代最大次數 int tenuringThreshold = DEFAULT_TENURING_THRESHOLD; for (String arg : args) { if (arg.startsWith(MAX_TENURING_THRESHOLD)) { String tenuringThresholdStr = arg.substring(MAX_TENURING_THRESHOLD.length() + 1); tenuringThreshold = Integer.valueOf(tenuringThresholdStr); if (tenuringThreshold > DEFAULT_TENURING_THRESHOLD) { tenuringThreshold = DEFAULT_TENURING_THRESHOLD; } } } LOGGER.info("Start to young gc, -Xmn={}m, -XX:MaxTenuringThreshold={}", edenMSize, tenuringThreshold); // 手動觸發 Young GC for (int i = 0; i < edenMSize * tenuringThreshold; ++i) { allocate_1M(); } // System GC,清理老年代 System.gc(); LOGGER.info("Call for young promote gc end, spend {}ms", System.currentTimeMillis() - startTime); } catch (Exception e) { LOGGER.error("Trigger young promote gc failed: ", e); } } /** * 獲取新生代大小,單位 M */ private static long getEdenMemorySize() { List<MemoryPoolMXBean> poolMXBeanList = ManagementFactory.getMemoryPoolMXBeans(); for (MemoryPoolMXBean memoryPoolMXBean : poolMXBeanList) { if (memoryPoolMXBean.getName().toLowerCase().contains("eden")) { long maxUsage = memoryPoolMXBean.getUsage().getMax(); return maxUsage >> 20; } } return -1; } /** * 生成個 1M 對象 */ private static void allocate_1M() { byte[] _1M = new byte[1024 * 1024]; } private GCUtil() { } private static final String MAX_TENURING_THRESHOLD = "-XX:MaxTenuringThreshold"; private static final int DEFAULT_TENURING_THRESHOLD = 15; private static final String JVM_XMN = "-Xmn"; private static final Logger LOGGER = LoggerFactory.getLogger(GCUtil.class); }
整體思路就是 ManagementFactory.getRuntimeMXBean().getInputArguments() 獲得 List<String> 所有 JVM 參數。
另一種方法就是通過 getMemoryPoolMXBeans() 獲取所有memoryPoolMXBean,然后找到 Eden 區參數,解析它的設置。而在年代方面,只能通過 RuntimeMXBean.getInputArguments 獲取,如果獲取不到,那么就采用默認值 15,這樣就能夠手動觸發 young gc 了。這是一種粗略的方式,並且能夠保證一定能夠達到效果(觸發 young gc 次數不會少於自己想要觸發的次數),在觸發后再次通過 System.gc() 觸發 full GC,來清理老年代不需要的對象,進而保證在開始提供服務時是一個干凈的環境,模型都存在於老年代中,不會參與 young gc。
如何監控 JVM 運行狀態:
https://www.jianshu.com/p/978522f88ad0。這些知識在實現一個服務監控組件時可能會用到。
接下來,思考一下,希望以后能夠在問題出現之前避免這種問題,而不是事后解決這個問題,所以在功能實現的時候需要清楚自己的對象是否會給 gc 帶來麻煩,老的 JVM 並沒有那么智能,有時候需要人為提供策略來協助它解決一些問題。