1. 为什么平均利用率曲线会骗人——热点图要解决的真实问题
先说一件我最近碰到的事。我们某个计算平台,登录节点、网络、存储都正常,调度队列里却积压了一批任务,某同事把监控大屏上的“平均CPU利用率”曲线调出来看,过去一周基本稳定在35%上下,看起来既不饱和也不空闲,但任务排队时间就是下不来。后来我把同一时间段的核心利用率热点图拉出来一看,问题立刻清楚了:96核的节点上有32个核几乎整段都是深色高占用,剩下64个核基本躺平。平均下来确实只有三成多,但真正能跑任务的核早就在排队了。
这就是我为什么坚持用热点图做核心效能分析,而不是只盯着平均值。平均利用率这种汇总指标,在做资源规划和容量评估时能给你一个宏观体感,但一旦进入调优环节,需要回答“瓶颈到底出在哪”的时候,它会毫不犹豫地把你带偏。这篇文章就围绕热点图这件事,把我自己从数据准备、画图、读图到落地调优的整套方法和踩过的坑,完整梳理一遍。
1.1 一个96核节点的典型骗局
上面说的这个场景不是个例。假设一个计算节点是96核,上面跑了4个16线程的单机程序,每个程序都是计算密集型的。理论上这4个任务刚好占满64个核,剩下32个核空着。平均利用率算出来是64/96,也就是66.7%,看起来还有三分之一的余量。
但如果你把96个核各自的时间序列铺开,会发现一个更残酷的事实:这4个程序如果恰好被调度到了同样的物理核心上,或者因为超线程、亲和性设置不当,统统挤在前32个核上运行,那么前32个核的利用率是100%,后64个核是0%。平台整体的“平均利用率”依然是33.3%——和前面“看着不饱和”的场景一模一样。但实际表现差别巨大:前者新任务可以立刻找到空闲核,后者新任务只能排队等前32个核上的任务结束。
这个例子说明一个道理:平均值丢失了分布信息。同样一个33%,可能是均匀铺开,也可能是严重倾斜,这两者对调度系统、对任务排队时间的影响完全不同。热点图的价值,就是把“分布”这件事重新画出来让你看到。
1.2 平均利用率的三个统计盲区
在继续讲热点图之前,我想先把平均利用率为什么不够用这件事说透。至少有三个盲区,是纯看平均值永远看不出来的:
第一,峰值与谷值被抹平。一个核一整天都在30%~40%之间波动,和另一个核每10分钟冲到100%再跌回0%,日平均利用率可能都是35%。但前者对系统是稳定的背景负载,后者意味着可能存在周期性的短任务、定时任务或锁竞争。平均值完全无法区分这两种截然不同的运行特征。
第二,核心间的离散程度被隐藏。同一时刻,A机器96个核都是50%,B机器48个核是100%、48个核是0%。瞬时平均值都是50%,但A机器还能接新任务,B机器已经有一半核在排队。离散度这个信息,必须靠分布图才能看到。
第三,时间上的相关性和持续性被忽略。某些模式是有“形状”的,比如一行核心持续亮半小时再熄灭,或者整个节点每隔一段时间集体亮一下。这种形状对应的是具体的行为特征——前者像单个长任务占核,后者像任务启动风暴。平均值把这些形状全部压成一条平滑曲线,形状信息就丢了。
1.3 热点图到底在画什么
热点图,本质上就是把“核心利用率”这个三维信息压到二维平面上。横轴是时间,纵轴是逻辑核心编号,每个格子的颜色深浅代表那个时刻、那个核心的利用率高低。你可以把它想象成一块电路板的红外温度图:哪个区域一直发烫,哪个区域常年冰凉,哪个区域有规律地闪烁,一眼就能看出来。
这种图我一般按三种粒度看:
- 秒级短窗口(比如5分钟到1小时),用来排查当时的抖动、排队、卡顿问题。
- 单日视图,用来观察业务周期、定时任务、夜间批处理等常规负载规律。
- 周级视图,用来发现缓慢漂移的趋势,比如某条任务线占用的核心数越来越多,或者某类任务结束后核心释放越来越慢。
三种窗口侧重点不同,但看图的逻辑是一致的:先看整体明暗节奏,再看异常亮块或暗块的形状、位置、持续时间,然后顺着这些线索去查任务、查调度、查代码。下面的几节,我会从数据准备开始,把这个过程完整过一遍。
2. 从原始采样到热点图:画图前的数据准备与关键参数
很多人在热点图这步翻车,不是绘图水平不行,而是数据准备根本没做对。热点图的细节程度直接取决于采样粒度和数据质量,这两点没控制好,后面所有图像分析都是空中楼阁。
2.1 采样、清洗与时间窗口:别让脏数据毁掉图像
先说采样。要画核心级别的热点图,监控系统至少得能拿出“每个逻辑核心”在“每个采样时刻”的利用率。常用的做法是让监控采集器每分钟、每30秒或每10秒读一次/proc/stat或操作系统提供的性能计数接口,按核心拆分后按时间戳写入时序数据库。
我自己的经验是:日常看趋势用60秒粒度足够,做问题定位至少要用10秒粒度,再细就没有必要了。低于10秒的采样对时序数据库的存储开销压力很大,而且热点图本身是给人眼看的,太细的粒度会被噪声淹没。如果你怀疑某个问题只持续几秒钟,更好的办法是去翻性能分析工具的记录,而不是指望监控热点图能捕捉到。
数据清洗这一步经常被忽视。实际环境中几类脏数据很常见:节点重启或监控采集器重启导致的重复时间戳、虚拟机热迁移导致的采样缺失、物理机离线期间的空白段。画图之前我会先做三步处理:
- 去重:同一节点同一时刻如果有两条记录,保留后写入的一条。
- 补洞:短暂缺失(小于采样间隔的2倍)用前后平均值填充;长时间的缺失直接标记为空洞,不要强行插值,否则图上会出现一段“伪造的平稳期”。
- 过滤空节点:整段时间都处于下线状态的节点直接排除,否则它会像一段全黑的长条,干扰视觉判断。
时间窗口的选择也有讲究。画一天的热点图,采样粒度和聚合粒度可以是同一个值;但如果要画一周的图,直接把60秒粒度画出来会导致图像像素不够,相邻数据会互相挤压。更合理的做法是先按5分钟或10分钟做聚合,再绘图。聚合策略我推荐取最大值而不是平均值——取平均会把5分钟内的尖峰磨平,取最大值能保留“这个时间段内曾经达到过的高水位”。
2.2 颜色映射与聚合粒度:同一个数据,两种画法
绘图工具其实不重要,重要的是颜色映射方式。我见过太多人把利用率直接线性映射到颜色标尺上,结果90%的区域都是浅色,只有几个亮点,人眼很难分辨有效信息。
我常用的做法是分位颜色映射:先计算所有像素的利用率分布,取90分位、99分位作为标尺上端,让“真正高占用”的格子变成深色,而不是让偶尔的尖峰拉高整体色标。举个具体例子,某平台整体平均利用率30%,最大瞬时利用率100%,如果色标从0到100线性映射,那么大部分格子都是淡蓝色,只有极少数尖峰是红色,整个图看起来像黑白噪声;改用分位映射后,把色标上限定在比如75%,那30%的平均负载会呈现出明显的浅黄到橙色,高负载区域则清晰地凸现出来。
颜色方案建议用单色渐变(深蓝到亮黄,或者黑到亮绿),不要用彩虹色。彩虹色会引入人眼对颜色差异的过度解读,干扰你对数值高低的判断。单色渐变的视觉逻辑更接近“温度高低的直觉”。
聚合粒度方面,除了前面提到的时间聚合,还可以做核心合并展示。节点核数很多(比如一台机器上百个核)时,可以把逻辑核心按物理CPU、NUMA节点、或调度器分区进行分组,每组算一个平均或最大值,再画一张“分组热点图”。这样能看到“哪个CPU区域忙、哪个CPU区域闲”。这个视角在处理NUMA敏感型负载时非常有用。
2.3 一段可复用的绘图脚本
下面给一段我常用的绘图脚本骨架,用Python实现,数据从时序数据库导出为CSV后直接使用。这个脚本不是完整产品代码,但结构足够你改造:
import pandas as pd import matplotlib.pyplot as plt import seaborn as sns def load_data(csv_path): df = pd.read_csv(csv_path, parse_dates=['time']) # 数据透视:行=核心,列=时间,值=利用率 pivot = df.pivot_table(index='core_id', columns='time', values='usage', aggfunc='max') return pivot def plot_heatmap(pivot, title='', cmap='YlGnBu', vmin=0, vmax=None): fig, ax = plt.subplots(figsize=(16, max(4, pivot.shape[0] * 0.15))) # 用分位数控制色标上限,避免尖峰拉高颜色范围 if vmax is None: vmax = pivot.to_numpy()[pivot.notna().to_numpy()].max() # 也可以按分位数,例如 vmax = np.nanpercentile(pivot.values, 99) sns.heatmap(pivot, cmap=cmap, vmin=vmin, vmax=vmax, cbar_kws={'label': 'CPU usage (%)'}, ax=ax) ax.set_xlabel('Time') ax.set_ylabel('Core ID') ax.set_title(title) ax.invert_yaxis() plt.tight_layout() return fig # 用法示例 if __name__ == '__main__': pivot = load_data('core_usage.csv') fig = plot_heatmap(pivot, title='Cluster Core Usage Heatmap') fig.savefig('heatmap.png', dpi=150)这里面有两个小经验要提:一是invert_yaxis()反转 y 轴,让 core 0 在顶部,符合“从上往下看”的阅读习惯;二是如果核数超过128,不要执着于画单张图,先按物理CPU分组聚合再画,否则图像里每一行只有零点几个像素高,视觉上全是模糊的色带。
3. 热点图中最常见的四类利用率异常形态
图准备好之后,接下来的问题就是:怎么读图?我看了大量的热点图之后发现,真实世界的利用率异常形态其实可以归纳成有限的几类,每类都有比较固定的形状和排查思路。把它们认熟了,每次拿到新图就相当于按图索骥。
3.1 形态一:横向条纹“半边全红半边全蓝”
这是最常见的病。具体表现:热点图里上半部分某一段核心长时间深色高利用率,下半部分另一段核心长期浅色低利用率,中间有一条非常明显的分界线。对应到节点上,就是一部分核始终忙,一部分核始终闲。
出现这种图,第一反应是查任务绑定(CPU affinity)。很多科学计算程序默认继承调度器的绑定设置,如果绑定策略只把任务绑到了一部分核上,或者因为超线程逻辑核的分配方式,导致任务全部落在前半段逻辑核上,就会出现这种条纹状。另一个常见诱因是任务自己内部做了线程池初始化,线程数写死成物理核数,但系统上逻辑核数是物理核数的两倍,线程池只占了一半逻辑核,剩下的一半全部空转。
还有一种比较隐蔽的情况:节点上同时有交互式任务和批处理任务,交互式任务长期占着低编号核心,批处理任务只在高编号核心上运行,时间一长就会形成上下分层的横纹。看到这种图,应该立刻把任务列表和时间线拉出来对齐,看纵向边界是否对应某个任务的启停时间。
3.2 形态二:竖向细线“闪断式刷高”
第二种形态是某一行(某个核心)上出现极细的竖向亮条,通常持续一两分钟,然后消失,过一段时间又出现,像心电图上的尖峰。这类亮条的共同特征是“持续时间短、复发有规律”。
能造成这种形态的原因很多,我的排查顺序一般是:
- 周期性短任务:cron任务、节点上的日志压缩、临时文件清理,这些会周期性唤起单线程进程,短暂占满一个核。
- 锁竞争和并行开销:多线程程序在加锁临界区反复争夺,虽然单个线程占不满一个核,但自旋等待会让多个核心同时出现短暂刷高。
- 监控采集自身:监控Agent在抓取指标并做本地聚合时也会消耗CPU,如果采集间隔恰好和某种周期性任务重合,图上会叠加出规律尖峰。
区分这三种情况,最直接的办法是把亮条出现的时间点和系统日志、任务调度日志做对比。如果是秒级甚至亚秒级的规律,优先怀疑锁竞争;如果是小时级或天级的规律,优先怀疑定时任务。
3.3 形态三:局部“亮砖”持续数小时
第三种形态比第二种更醒目:图像中出现一块宽而高的亮色区域,横向覆盖几十分钟到几小时,纵向覆盖一个或几个连续核心,边界清晰,像一块烧红的砖头。
这种形状对应的是持续的单核或少数核高占用计算任务。最典型的是串行程序、单线程Python脚本、或者某个程序的主线程在做不可并行的串行段。它们的特点就是核心数不多但占用时间很长,非常容易成为整个节点甚至整个集群的新任务瓶颈——因为调度器觉得这台节点还有空闲核,实际空闲核也确实有,但你在那个核上起新任务后,会和这个长任务争抢内存带宽、三级缓存等共享资源,最终两个任务都变慢。
遇到这种亮砖,不要急着杀任务,先看几个属性:亮砖的高度(占几个核)、宽度(持续多久)、是否周期重复、是否和某个任务ID关联。曾经有个案例,某图像处理Demo程序里有一个序列化保存结果的环节,全局锁没处理好,导致40分钟的单核瓶颈,但程序其他阶段都能用到64个核。热点图上显示的就是“大部分时间全亮,中间夹了一块纵深感很强的亮砖”——这就是典型的串行瓶颈形状。
3.4 形态四:整机瞬时“暴亮”后一片空白
第四种形态经常出现在固定时间点:整个节点所有核同时亮一下,持续几分钟,然后全部熄火,接着可能所有节点错峰重复这个过程。这种“整体性刷亮”通常和调度行为有关,而不是单个程序的计算特征。
最典型的诱因是任务启动风暴。调度系统一次性在一个节点上同时启动了几十个任务进程,每个进程都要做初始化:加载动态库、读配置文件、申请内存、建立通信连接。一瞬间把所有核全部占满,初始化完成后进入等待状态,利用率又全部掉回接近0。如果集群有几百个节点同时启动大规模作业,这种风暴还会在监控图上形成一条“所有节点同时闪亮”的壮观景象。
这种形态的危害容易被低估。它对平均利用率贡献不大(几分钟),但对节点性能的冲击却很大——启动风暴期间,系统调用密度、锁竞争、内存分配压力都会飙升,任何正在运行的生产任务都会受到干扰。如果你在热点图中频繁看到这种集体暴亮,应该去检查调度器的节点分批策略、任务提交后的初始化序列,或者干脆错峰启动。
4. 从“看到问题”到“解决问题”:热点图驱动的调优动作
读图只是第一步,热点图的最终价值要落到“能不能指导调优”上。这一节我按前面四类形态分别说对应的处理动作,以及如何验证调优到底有没有效果。
4.1 先对齐调度日志:让热点图“带名字”
在动手调优之前,我强烈建议先把热点图和任务的调度日志对齐。具体做法是:把热点图按时间轴截取某一段,然后把该时间段内任务字典翻出来——每个任务的ID、启动时间、结束时间、申请核数、所在节点、运行状态——叠加到图下面看。你会发现很多“不知道为什么”的图案,一旦叠加了任务生命周期就立刻解释清楚了。
这个步骤听起来简单,但不要跳过。一个典型的误区是:看到一个核心持续高占用,直接断定是某任务在计算,结果一核对日志,发现那个时间段该任务根本没在这个节点上跑,高占用其实来自另一个隐藏的后台进程。没有日志对齐,所有后续分析都可能是错的。
4.2 倾斜问题:绑核比增加核心更划算
如果热点图的横向条纹对应的是任务绑定不均,第一步不是去加节点或加核,而是先确认绑核策略。处理办法大体上分三层:
- 调度器层面:在作业提交脚本里显式指定每个任务使用的进程和线程绑定范围,让16线程任务固定在物理核上,而不是散落到超线程逻辑核上。这一步通常能把“任务只占一半核心”的问题直接解决。
- 超线程策略层面:如果确认业务是计算密集型,超线程带来的收益很小甚至为负,可以考虑在节点级别关闭超线程,让操作系统只暴露物理核。热点图上的核心数会减半,但每一列的“含金量”更高。这个决策一定要拿同一业务的基准测试数据来拍板,不要凭感觉。
- NUMA感知层面:绑定核时不只看编号连续,还要看所属的NUMA节点。跨NUMA的内存访问延迟可比同NUMA高一截,对访存密集的程序影响明显。绑核策略要尽量让一个任务的线程落在同一个NUMA节点内。
调完之后别急着下结论,再看一次热点图。倾斜是否消失,空闲核区域是否变得“更均匀”,这些在图上都看得很清楚。
4.3 峰值问题:找后台任务,锁冲突用采样工具确认
对于闪断式尖峰,处理动作取决于根因。如果日志确认是定时任务,优化空间不大,只要错开高峰期就好;但如果是锁竞争,处理起来就更有技术含量了。
我的排查思路是这样的:先用热点图把尖峰出现的时间段锁定,然后在该时间段内抓取程序的性能采样数据,看哪些函数在采样中占的样本数最多。如果发现热点集中在锁相关函数或者原子操作上,基本可以实锤锁竞争。这时候可以做的动作包括:缩小临界区代码范围、用更细粒度的锁替代粗粒度锁、把“单把大锁”改成分段锁、或者对只读数据使用无锁读取方案。
这里给一个印象深刻的例子:某项目的数据处理任务每隔半小时就会出现一次持续几十秒的单核尖峰,热点图上看就像每个定时器到点就会“打一次点”。查了两天没结果,后来通过性能采样定位到是任务在执行中途的栈展开,所有线程都在等待一把全局锁去刷新缓存索引。把锁拆成按分片索引加锁之后,瓶颈直接消失,整体任务耗时缩短了近两成。热点图上那排规律尖峰也随之消失了。
4.4 验证效果:优化前后同名任务对比
调优动作做完,验证手段就用上了。我推荐固定的验证三板斧:
第一,同一任务跑前跑后对比。选一个稳定复现的基准任务,优化前跑一遍记录耗时和热点图,优化后再跑一遍,保持输入数据、节点型号、核数申请完全一致。热点图直接对比,耗时数字作为辅助证据。
第二,关注“排队时间”而不是“运行时间”。不少调优动作平移了瓶颈,比如绑核之后某个任务的运行时间没变,但整个集群的新任务排队时间下降了。这个指标在热点图上看不到,但确实是你调优的成果。
第三,持续观察一周。优化完当天有效不算数,要看一周以上的热点图是否保持健康。有些调整刚做完很漂亮,但碰到周末特有的批处理场景,又暴露出新的问题。持久性验证比一次性对比更重要。
5. 画图之外的几个坑与更远一步的想法
最后这部分,我说说在热点图实践过程中踩过的几个具体坑,以及这套方法后续还能怎么延伸。这些内容不属于教科书上的标准答案,但都是我实际用过之后觉得值得提醒同行的地方。
5.1 几种常见的画图陷阱
坐标轴方向弄反。很多人第一次画热点图,core 0在底部,时间从左往右。看习惯了其实无所谓,但在团队协作时,如果大家的方向不统一,讨论问题会产生大量沟通成本。我们内部约定统一为“核心从上往下、时间从左往右”,简单一致。
单看某一天的热点图做结论。某一天可能恰好有大批任务集中到达,也可能恰好有一台节点在做维护。单独一天的图代表不了常态。我一般至少对比同类型的三天,再确认某个模式是否稳定存在。
忽略“空闲”的语义。热点图的浅色区域不一定是坏事。有些浅色区域是任务还没到达,有些则是因为资源被锁但没有被有效使用。用调度日志对齐之后,你会看到同样一段浅色,其背后的业务含义可能截然不同。读图时一定要带着上下文读,不要只看颜色。
色标被极端值拉爆。这在前文提过,这里再强调一次:一个异常尖峰如果达到100%而大部分数据只有10%,用0到100的色标去画图,你会得到一张几乎没有层次感的图。分位色标在这里不是花哨技巧,而是必需品。
5.2 热点图的边界:它看不到什么
热点图只能看到“核心在忙不忙”,但它看不到忙得有没有价值。同样的100%利用率,可能是高效计算,也可能是缓存未命中导致的内存等待,甚至可能是空转自旋。要区分这些,你得再叠加性能计数器数据,比如IPC(每周期指令数)、缓存未命中率、内存带宽利用率。
另外,热点图也看不到内存占用和网络IO的联动情况。有些任务的瓶颈根本不在CPU,而是在等待数据到达。这时候热点图会显示核心利用率忽高忽低,但没有明显规律。遇到这种图,别在CPU这边死磕,去把网络和存储指标拉出来一起看。
5.3 从人工看图为到自动扫描
看得多了之后,我开始把一些重复模式交给程序去识别,而不是每天人工盯图。比如可以统计每个核心在热点图中的“高占用时长占比”,自动搜索持续高占用超过4小时的单核;也可以统计节点在短时间内整体从低到高的跳变次数,用来发现启动风暴。这些自动扫描不一定能替代人的判断,但能大大缩小人工看图的范围。
更远一步的方向,是把热点图和任务调度日志做关联分析:当图中出现某种异常形状时,自动回溯该时间段有哪些任务在跑,提炼出“这类任务 = 这类形状”的规律。我在某个模拟项目上试过这个思路,将一段时间的历史数据喂给简单的统计分类器,基本能把“倾斜型”“尖峰型”“启动风暴型”几类粗分类做出来。虽然离完备还远,但已经能帮我们自动筛掉相当一部分低价值的排查工作。
我的个人习惯是:每周固定抽出时间,把过去一周的主要节点热点图快速浏览一遍。不需要花太多时间,重点看有没有新出现的异常形状、旧问题有没有复发。这套方法不敢说解决所有性能问题,但至少在核心利用率这件事上,它让我从“觉得系统还行”的状态,变成了“知道系统具体在哪些核心上做了什么”的状态。单是这一点,就值得把图坚持画下去。