背景
本文基于 StarRocks 3.3.5
最近在做一些 StarRocks 相关的指标监控的时候,看到了FE master的CPU使用率相对其他FE节点是比较高的,且 呈现周期性的变化(周期为8分钟),
于此同时FE master节点的GC频率相对于其他节点高出很多倍,于是我们利用arthas采集了大约15分钟CPU的火焰图。如下:
对应的FE master节点的CPU使用率的变化如下图:
FE 其他节点的CPU使用率的变化如下图:
对应的FE master节点的Young GC的变化如下图:
FE 其他节点的Young GC变化如下图:
结论
CPU使用率高的主要集中在三个点:
- StatisticAutoCollector(占用28%)
- MemoryUsageTracker (占用9%)
- JVM GC(占用57%)
因为在我们的场景下,实时写入的任务比较多,且写入的是分区表(由于业务的场景问题,会更新以前分区的数据),所以会导致 StatisticAutoCollector 进行相关统计信息的收集,而这个统计信息的收集,会触发System.gc
操作,从而导致FE master节点的 gc频率比其他节点高很多。
分析
StatisticAutoCollector
StatisticAutoCollector 这个类只有在FE Master才会被调用,且调用的频率为statistic_collect_interval_sec
,也就是5分钟。
该线路数据流为:
StatisticAutoCollector.runAfterCatalogReady
||
\/
runJobs
||
\/
StatisticExecutor.collectStatistics
||
\/
FullStatisticsCollectJob.collect
||
\/
collectStatisticSync
||
\/
StmtExecutor.executeStatisticDQL
||
\/
StmtExecutor.executeDQL
||
\/
StatementPlanner.plan //走到 生成计划
||
\/
createQueryPlanWithReTry
||
\/
collectOriginalOlapTables
||
\/
OlapTable.copyOnlyForQuery
||
\/
partitionInfo.clone()
partitionInfo.clone()
会初始化HashMap来复制partiiton的信息:
protected Object clone() {
try {
PartitionInfo p = (PartitionInfo) super.clone();
p.type = this.type;
p.idToDataProperty = new HashMap<>(this.idToDataProperty);
p.idToReplicationNum = new HashMap<>(this.idToReplicationNum);
p.isMultiColumnPartition = this.isMultiColumnPartition;
p.idToInMemory = new HashMap<>(this.idToInMemory);
p.idToTabletType = new HashMap<>(this.idToTabletType);
p.idToStorageCacheInfo = new HashMap<>(this.idToStorageCacheInfo);
return p;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
所以说在这种要收集的分区信息很多的情况下,HashMap的初始化,就很消耗CPU。
再者,在collectStatistics 之前会通过 StatisticsCollectJobFactory.buildStatisticsCollectJob 这个方法计算出要收集的 FullStatisticsCollectJob ,这里会通过执行select $quoteColumnName as column_key from $dbName.$tableName partition $partitionName
这种方法收集每个分区中某些字段的信息,这里后续会详细说
MemoryUsageTracker
StatisticAutoCollector 这个类只有在FE Master才会被调用,且调用的频率为 memory_tracker_interval_seconds
,也就是1分钟。
该类的数据流为:
MemoryUsageTracker.runAfterCatalogReady
||
\/
MemoryUsageTracker.trackMemory
||
\/
MemoryTrackable.estimateSize
||
\/
SizeEstimator.estimate
这里会根据初始化方法initMemoryTracker
涉及到的对象进行内存的评估,具体的对象如下:
private void initMemoryTracker() {
GlobalStateMgr currentState = GlobalStateMgr.getCurrentState();
registerMemoryTracker("Load", currentState.getLoadMgr());
registerMemoryTracker("Load", currentState.getRoutineLoadMgr());
registerMemoryTracker("Load", currentState.getStreamLoadMgr());
registerMemoryTracker("Load", currentState.getInsertOverwriteJobMgr());
registerMemoryTracker("Compaction", currentState.getCompactionMgr());
registerMemoryTracker("Export", currentState.getExportMgr());
registerMemoryTracker("Delete", currentState.getDeleteMgr());
registerMemoryTracker("Transaction", currentState.getGlobalTransactionMgr());
registerMemoryTracker("Backup", currentState.getBackupHandler());
registerMemoryTracker("Task", currentState.getTaskManager());
registerMemoryTracker("Task", currentState.getTaskManager().getTaskRunManager());
registerMemoryTracker("TabletInvertedIndex", currentState.getTabletInvertedIndex());
registerMemoryTracker("LocalMetastore", currentState.getLocalMetastore());
registerMemoryTracker("Query", new QueryTracker());
registerMemoryTracker("Profile", ProfileManager.getInstance());
registerMemoryTracker("Agent", new AgentTaskTracker());
QeProcessor qeProcessor = QeProcessorImpl.INSTANCE;
if (qeProcessor instanceof QeProcessorImpl) {
registerMemoryTracker("Coordinator", (QeProcessorImpl) qeProcessor);
}
IDictManager dictManager = IDictManager.getInstance();
if (dictManager instanceof CacheDictManager) {
registerMemoryTracker("Dict", (CacheDictManager) dictManager);
}
memoryMXBean = ManagementFactory.getMemoryMXBean();
LOG.info("Memory usage tracker init success");
initialize = true;
}
这里会对里面涉及到的所有对象进行内存的评估,用来后续的内存使用指标显示。
JVM GC
这个方法是在每个SQL执行完后就会触发的,具体的数据流为:
StatisticAutoCollector.runJobs
||
\/
StatisticExecutor.collectStatistics
||
\/
FullStatisticsCollectJob.collect
||
\/
FullStatisticsCollectJob.collectStatisticSync
||
\/
flushInsertStatisticsData
||
\/
StmtExecutor.execute()
||
\/
GlobalStateMgr.getCurrentState().getMetadataMgr().removeQueryMetadata();
||
\/
queryMetadatas.metadatas.values().forEach(ConnectorMetadata::clear)
||
\/
LocalMetaStore.clear -> System.gc()
当然这也只是该 StatisticAutoCollector 定时的触发的,还有如果有查询SQL的话,也会进行触发。具体看 StmtExecutor.execute方法:
public void execute() throws Exception {
...
try {
...
} finally {
GlobalStateMgr.getCurrentState().getMetadataMgr().removeQueryMetadata();
if (context.getState().isError() && coord != null) {
coord.cancel(PPlanFragmentCancelReason.INTERNAL_ERROR, context.getState().getErrorMessage());
}
if (parsedStmt != null && parsedStmt.isExistQueryScopeHint()) {
clearQueryScopeHintContext();
}
// restore session variable in connect context
context.setSessionVariable(sessionVariableBackup);
}
}