1. 先弄清楚这三者到底在吵架什么内存、CPU、磁盘这三样东西只要你在电脑前坐超过三年就一定被它们三兄弟联合教训过。任务管理器里某个进程莫名其妙吃掉几个G内存CPU占用率飙到100%但机器卡得连鼠标都拖不动或者磁盘指示灯长亮不灭、打开一个文件夹要等半分钟——这些现象背后其实都是同一个故事的不同章节计算速度极快的那一方被迫等待速度极慢的那一方而中间那个和事佬还经常不够用。我写这篇东西的目的很直接把CPU、内存、磁盘的交互链条从头到尾捋一遍讲清楚数据是怎么在这三者之间流动的为什么要有缓存为什么要有虚拟内存为什么磁盘满了会连带把数据库搞崩。适合谁看写过代码但对底层只有模糊印象的后端同学运维过程中只会看任务管理器数字的运维同学还有那些被内存溢出磁盘爆满折腾过但说不清原理的人。看完之后你至少能做到一件事看到一个性能现象能大致判断是哪一层出了问题而不是无脑重启或者加内存。先说一句总纲性质的话现代计算机的性能设计本质上是一场关于速度差的妥协艺术。CPU快得像光速内存慢一个数量级磁盘再慢两到三个数量级。如果让CPU每次都直接找磁盘要数据那我们现在用的就不是电脑是一台昂贵的电子闹钟。所以整个体系的核心思路就是——用快的存储去挡住对慢存储的访问挡不住的再想办法提前搬、批量搬、异步搬。1.1 用厨房来类比灶台、操作台和仓库我把这套体系类比成一个厨房。CPU是厨师他的手速极快一秒能颠十下锅内存是灶台旁边的操作台切好的菜、调料都摆在这上面伸手就能拿磁盘是楼下的大仓库东西齐全但拿一次要走楼梯、签字、再走回来。厨师做一道菜需要葱姜蒜。理想情况是这些东西已经在操作台上他伸手就拿。如果操作台没有他要么自己下楼去仓库取——这一趟来回够他炒糊三盘菜了要么喊一个跑腿的小工去取自己继续处理手头的食材。这个跑腿的小工就是DMA控制器我们后面会专门讲。操作台的大小就是内存容量。操作台越大能同时摆的食材越多厨师下楼取东西的次数就越少整体出菜速度就越快。但操作台不是越大越好因为越大越贵而且厨师找东西的速度也会受一点影响寻址开销。这就是为什么内存容量的选择永远是够用留余量而不是无限加。仓库则是持久化存储。它的特点是断电不丢东西但速度慢。所有你要长期保存的数据最终都得落到仓库里。厨师下班前得把今天用过的、以后还要用的东西写进仓库账本这就是磁盘写入。理解了这套类比后面所有的技术细节都只是往这个框架里填肉。缓存是操作台上再放的一个小托盘只放最常用的几样虚拟内存是当操作台不够用时临时把一部分食材挪到仓库腾出地方缺页中断就是厨师伸手发现托盘里没东西被迫停下来等。1.2 存储层次的本质是速度和容量的跷跷板计算机存储是一个典型的金字塔结构越往上越快、越小、越贵越往下越慢、越大、越便宜。这个结构不是谁拍脑袋设计的而是被物理规律逼出来的越靠近CPU的存储制造工艺越苛刻单位成本越高所以只能用很小的容量换取极高的速度。层级典型容量访问延迟数量级谁来管理寄存器几百字节亚纳秒级编译器/CPUL1 缓存32~64 KB/核1 纳秒级CPU 硬件L2 缓存256 KB~2 MB/核数纳秒CPU 硬件L3 缓存8~64 MB十几到几十纳秒CPU 硬件内存8~512 GB80~120 纳秒操作系统固态硬盘数百 GB~数 TB微秒级操作系统/固件机械硬盘数 TB毫秒级操作系统/固件这张表里最该被记住的是量级差距。从内存到固态硬盘延迟差了大概两个数量级到机械硬盘差了四个数量级。也就是说CPU等一次机械硬盘的随机读相当于它执行了几十万条指令。这个比例关系解释了几乎所有性能优化的方向减少慢速存储的访问次数比优化计算逻辑更有价值。顺带说一个热词里经常出现的现象为什么有些机器GPU、CPU、内存占用都不高但就是卡。这类问题很大概率卡在I/O等待上——任务管理器只显示CPU的忙碌时间不显示CPU在等待I/O时被阻塞的时间。Windows的任务管理器里看磁盘那一栏的响应时间如果长期在几百毫秒以上那基本就是存储层在拖后腿跟CPU和内存没关系。1.3 谁在指挥这场交互操作系统是总调度CPU、内存、磁盘三者本身并不会自动协作它们之间所有交互都由操作系统内核调度。内核提供三套抽象进程拿到的是虚拟地址空间文件系统拿到的是块设备CPU拿到的是页表。这三套抽象之间的转换就是数据流动的全部秘密。进程读写内存时用的是虚拟地址内核通过页表把它翻译成物理地址再由内存管理单元MMU落到真正的内存条上。进程读写文件时用的是文件描述符内核通过文件系统把它翻译成磁盘块号再交给块设备驱动最后通过总线送到磁盘控制器。这里有个关键点很多人忽略进程永远不能直接访问物理内存也不能直接访问磁盘。所有访问都必须经过内核翻译。这个翻译层的存在既带来了安全隔离也带来了性能开销。很多性能优化技术比如大页内存、直接I/O、零拷贝本质上都是在削减翻译层数或者降低单次翻译成本。2. CPU与内存的交互从寄存器到缓存行CPU和内存的关系是三者里最紧密、也最容易被误解的一段。很多人以为CPU是从内存直接读变量的其实绝大多数情况下不是。CPU真正读写的是缓存只有当缓存里找不到时才会去内存取。现代CPU核心和内存之间隔了三层缓存而且数据不是按字节搬运的是按缓存行搬运的主流架构一行是64字节。这意味着一件听起来有点荒唐的事你只读了1个字节硬件却顺手把周围63个字节也搬进了缓存。这个设计看起来浪费但它恰恰是性能的关键来源。2.1 一条简单赋值语句在硬件上发生了什么写一句counter counter 1;编译器会翻译成类似从内存地址加载到寄存器、寄存器加一、写回内存的指令序列。但硬件执行时第一步的从内存加载会被缓存接管CPU先去L1找找不到去L2再找不到去L3最后才真正向内存控制器发请求。这个过程有个专业名字叫缓存命中率。L1命中几个时钟周期搞定L3命中几十个周期真的要去内存上百个周期。所以同一段代码数据布局不同性能能差出好几倍。我在实际项目里见过一个典型例子一个统计程序遍历二维数组行优先和列优先两种写法逻辑完全一样运行时间差了接近4倍。原因就是列优先遍历破坏了空间局部性每一行数据都要重新从内存搬缓存几乎全miss。这类问题不看硬件层根本解释不通只知道慢。2.2 缓存行、局部性和伪共享缓存行的存在带来了两个直接后果。好的方面是空间局部性收益顺序访问数组时一次搬运能让后面63个字节全部命中实际带宽被放大。坏的方面是伪共享两个不同变量如果落在同一个缓存行里被不同CPU核心分别修改就会导致缓存行在两个核心之间反复失效和同步。伪共享的典型现象是多线程程序加了线程数反而变慢而且CPU占用不高但吞吐上不去。排查手段是看缓存行填充常见做法是在变量之间插入填充字节让它们落在不同的缓存行里。这个技巧在高频交易、游戏引擎这类对延迟极敏感的领域用得很多。提示伪共享是看不见的性能杀手。如果你的多线程程序扩展性很差先怀疑共享数据的布局再怀疑锁。2.3 内存屏障和可见性为什么需要volatile缓存带来了一个新的问题每个核心都有自己的一份数据副本那么一个核心改了数据另一个核心什么时候能看到答案是——不保证立刻看到。硬件只在缓存一致性协议允许的时机同步而这个时机对上层是透明的。这就是Java内存模型JVM内存模型要解决的问题。Java里的volatile关键字本质上是告诉编译器和CPU这个变量的读写不能被优化掉并且要插入内存屏障强制让其他核心看到最新值。没有它一个线程写的标志位可能永远停在另一个线程的缓存里程序就这么死循环下去。理解这一点之后再看JVM的happens-before规则就不会觉得是死记硬背的规定了。它其实是把硬件层的缓存同步行为用一套语言规范表达出来让开发者不用直接面对CPU的乱序执行和缓存刷新。2.4 内存频率和时序到底怎么算买内存条时会看到3200MHz CL16这类参数。这两个数字的含义值得说清楚因为它们直接关系到内存的实际带宽和延迟。频率决定带宽。DDR是双倍数据率所以标称3200MHz的实际时钟是1600MHz每个时钟周期传输两次数据。带宽的计算公式是带宽(GB/s) 频率(MHz) × 位宽(bit) ÷ 8 ÷ 1000以双通道DDR4-3200为例位宽是64位×2128位3200 × 128 ÷ 8 ÷ 1000 51.2 GB/s时序决定延迟。CL16的意思是列地址选通延迟为16个时钟周期。换算成绝对时间16 ÷ 1600MHz 10 纳秒这个10纳秒就是一次内存访问的CAS延迟。所以你会看到一个反直觉的现象高频内存如果时序放松太多实际延迟可能比低频紧时序的还差。选内存时不能只看频率得把两个参数一起看。这也是为什么很多玩家会花时间手调时序——在特定频率下把时序压到能稳定运行的最低值能实打实降低延迟。3. 内存与磁盘的交互虚拟内存这套障眼法如果说CPU和内存的交互是硬件层的默契那内存和磁盘的交互就是操作系统层的魔术。这套魔术的名字叫虚拟内存它的核心目标只有一个让每个进程都以为自己独占了一整块巨大的、连续的内存而实际上物理内存可能只有它的几分之一大。虚拟内存不是新概念从1960年代就开始用了但很多人对它的理解停留在内存不够就用硬盘凑这其实只说对了一半。虚拟内存更重要的意义是隔离和抽象每个进程有独立的地址空间互相看不到也改不了程序崩溃不会连坐这才有了现代操作系统的稳定性。3.1 虚拟地址到物理地址的映射过程进程里一个指针的值比如0x7f4a2c001000这是虚拟地址。CPU执行访问时MMU拿这个地址去查页表翻译成物理地址再去访问内存条。页表本身也存在内存里所以一次地址翻译可能变成两次内存访问一次查页表一次取数据。为了减少这个开销硬件里加了转址旁路缓存TLB专门缓存最近用过的地址映射。TLB命中就一步到位TLB未命中才去查页表。这也是为什么程序的内存访问模式要尽量集中——访问的页越少TLB越容易命中。页的大小通常是4KB大页是2MB或1GB。大页的意义就在于同样的内存范围用2MB页只需要一张页表项TLB能覆盖的内存范围大512倍。数据库这类需要频繁访问大块内存的应用开大页往往能拿到可观的性能提升。3.2 缺页中断一次被迫停下来的等待当进程访问的虚拟地址页不在物理内存里时硬件会触发缺页中断CPU暂停当前指令切到内核的缺页处理程序。内核会去磁盘上找这个页读进内存更新页表然后返回让CPU重新执行刚才那条指令。这一来一回就是从内存级延迟跳到磁盘级延迟。固态硬盘大概几十微秒机械硬盘就是几毫秒。如果程序频繁缺页表现就是CPU占用不高、磁盘狂转、程序卡顿——这正是任务管理器里磁盘100%但CPU很闲的典型画面。这类问题的排查工具在Linux下是vmstat和pidstat# 每秒输出一次重点看 si/so换入换出和 majflt主缺页 vmstat 1 # 按进程看主缺页次数 pidstat -r -p pid 1如果si/so长期非零或者主缺页次数持续增长说明物理内存真的不够了进程在磁盘和内存之间反复倒腾。这个状态在运维圈里有个形象的说法叫抖动性能会呈断崖式下跌。3.3 交换分区和内存压缩怎么选内存不够时操作系统有两条路把不常用的页写到磁盘交换或者把多个页压缩后塞在内存里内存压缩。交换的优点是能腾出大量空间缺点是慢——写出去再读回来都是磁盘级操作。Windows和Linux都默认开启交换但在固态硬盘上频繁交换会加速磨损在机械硬盘上则会让系统卡到无法忍受。内存压缩的思路不同它把不活跃的页压缩后留在内存里需要时解压还原。压缩比通常能到2:1到3:1优点是快缺点是消耗CPU。Windows 10和Windows 11默认开启内存压缩对应的是任务管理器里内存那一栏的已压缩数字。如果发现压缩内存占比很高同时又觉得CPU占用上来了可以权衡是否关闭——热词里win10关闭内存压缩win11内存占用过高怎么解决这两个搜索词背后就是这个问题。我的建议是物理内存16GB以下、还开着机械硬盘的机器保留内存压缩物理内存32GB以上、固态硬盘的机器压缩带来的收益有限可以考虑关掉换取一点CPU。但要注意关闭压缩不等于内存够用只是把压力从CPU转移回硬盘。3.4 脏页和写回什么时候数据才真的落盘进程调用write()写文件时数据通常只是被复制到内核的页缓存里标记为脏页并没有立刻写到磁盘。内核会在合适的时机批量刷盘这个机制叫写回。好处是合并小写入、减少磁盘次数坏处是断电时可能丢数据。控制写回行为的几个关键参数在Linux下是# 脏页占总内存的比例超过就触发后台刷盘 vm.dirty_background_ratio 10 # 脏页硬上限超过就阻塞写入 vm.dirty_ratio 20 # 脏页最长驻留时间 vm.dirty_expire_centisecs 3000这几个值调小会让数据更快落盘、掉电更安全但磁盘写入次数增加调大则相反。数据库服务器上通常会把它们调小因为数据丢失的代价远高于磁盘性能损耗。顺带解答一个热词里的困惑word保存显示磁盘已满。这个报错的真实原因往往不是磁盘真的满了而是临时目录所在的分区满了或者磁盘配额用尽。Word在保存时会先写临时文件再改名临时目录空间不足就会报这个错。排查时要看的是%TEMP%指向哪个盘而不是文档所在的那个盘。4. CPU与磁盘的交互DMA这条绕开CPU的捷径按前面的逻辑磁盘数据要进内存似乎应该由CPU一条条搬运。如果真是这样那CPU会被I/O彻底拖死。早期计算机确实这么干叫PIO模式CPU要盯着磁盘控制器的状态寄存器一个字节一个字节地读进来。这种方式下CPU几乎不能做别的事。DMA直接内存访问就是为解决这个问题而生的。它让外设控制器直接和内存打交道数据搬运不经过CPU的寄存器CPU只需要在开始和结束时各介入一次。这套机制是今天所有高速I/O的基础。4.1 PIO和DMA的差别有多大用数字说话一次磁盘读取假设传输4KB数据。PIO模式下CPU要执行几千次搬字节的指令加上轮询状态的开销CPU可能被占满几十微秒。DMA模式下CPU只发一条命令然后就可以去执行别的线程等中断通知结果就行占用时间降到微秒以下。这个差距在多任务环境下会被放大。一台服务器同时跑几百个线程每个线程都在读文件如果都用PIOCPU的时间全花在搬字节上真正做计算的份额少得可怜。DMA的价值不在于单次传输更快而在于把CPU从搬运工的角色里解放出来。4.2 从磁盘到进程内存的完整数据链路现在把整个流程串起来。一个进程调用read()读文件内核做的事情大致是查页缓存如果数据已经在缓存里直接从内存复制到进程缓冲区全程不碰磁盘。如果不在缓存里向块设备层发起读请求进程进入睡眠。块设备驱动把请求转成磁盘命令交给磁盘控制器。磁盘控制器用DMA把数据写进内核指定的内存缓冲区。传输完成磁盘发中断CPU执行中断处理程序唤醒睡眠的进程。内核把数据从页缓存复制到进程的用户空间缓冲区read()返回。这条链路上有两处值得注意的性能点。一处是第1步的页缓存命中命中就是内存级操作未命中就是磁盘级操作差距三个数量级。另一处是第6步的数据复制数据在内核空间和用户空间之间被搬了一次这是纯粹的额外开销。4.3 零拷贝和mmap为什么能省时间针对第6步的复制开销就有了mmap和零拷贝技术。mmap的做法是把文件映射到进程的虚拟地址空间进程读写这块内存就相当于读写文件页缓存的页直接被映射到用户空间省掉了内核缓冲区到用户缓冲区这一趟复制。零拷贝比如Linux的sendfile更进一步数据从磁盘读进页缓存后直接由网络协议栈从页缓存取走发出去全程不经过用户空间。对文件服务器、消息队列这类应用收益非常明显。不过要注意这些技术不是万能药。mmap在小文件、频繁变更的文件上可能因为缺页中断反而更慢零拷贝则受限于具体的使用场景。选型时得看数据规模和访问模式不能看到零拷贝三个字就往上套。5. 真实场景排查从现象反推是哪一层出了问题讲完原理最实用的部分来了。性能问题排查的核心思路是先定位是哪一层成为瓶颈再在这一层里找具体原因。下面按现象分类给出我实际处理过的几个典型案例。5.1 CPU高、磁盘低先看是谁在烧CPUCPU占用高但磁盘很闲说明瓶颈在计算层。这时候分两种情况如果是某个业务进程占用高通常是代码问题比如死循环、正则回溯爆炸、频繁GC如果不是业务进程就要看系统进程。热词里出现的几个进程名都很有代表性。Antimalware Service Executable是Windows Defender的扫描进程它在全盘扫描时会持续吃CPU可以在排除目录里把开发目录、编译产物目录加进去。服务主机DCOM占用CPU高通常是某个组件在反复请求DCOM服务需要具体定位是哪个子进程。aceguardclient占用CPU很高这类防护客户端的问题基本只能靠版本更新或换配置解决。排查手段上Linux下用top按CPU排序再pidstat -u -p pid 1看线程级占用找到具体线程后用jstack或perf定位到代码位置。5.2 内存高、CPU不高、磁盘偶尔转怀疑漏和抖动内存持续增长不下降第一种可能是内存泄漏第二种可能是缓存没有上限。区分的方法是看进程重启后是否恢复正常重启就好转的是泄漏重启后慢慢又涨回去的可能是缓存策略问题。如果同时伴随磁盘轻量但持续的读写那就可能是虚拟内存抖动的前兆。这时候要算一笔账物理内存总量减去已经确认的常驻需求剩下的余量够不够应对峰值。不够就得扩容或者优化数据结构的驻留时间。热词里wechatappex占用内存过高xssfworkbook内存溢出都属于这一类。前者是客户端进程的缓存管理问题后者是典型的API使用不当——用XSSFWorkbook处理大Excel文件时会一次性把所有单元格对象驻留内存换个流式写入库或者改用CSV内存占用能降一个数量级。5.3 磁盘爆满MySQL三大故障场景里最要命的一个磁盘爆满在数据库运维里属于一等事故因为它会导致写入失败、事务回滚、主从延迟甚至实例直接不可用。热词里mysql 三大故障场景磁盘爆满这个说法很准确另外两个通常是连接打满和慢查询堆积。磁盘爆满的常见原因有这么几类binlog没有定期清理慢查询日志无限增长临时表写爆磁盘还有大事务产生的undo日志堆积。处理顺序是先用du找出大目录确认是哪一类再针对性清理。# 找出根目录下占用最大的前20个目录 du -h --max-depth1 / | sort -rh | head -20 # 查看binlog大小 ls -lh /var/lib/mysql/ | grep binlog清理binlog要用PURGE BINARY LOGS命令不能直接rm否则索引文件会对不上。这是踩过的坑直接删文件会导致主从复制出错。注意生产环境磁盘用量建议保持在70%以下超过85%就该告警。磁盘不像内存有交换空间兜底写不进去就是写不进去。5.4 一张表看完常见现象和对应层级现象最可能卡在哪一层先看什么指标CPU 100%磁盘空闲计算层进程级CPU占用、线程栈CPU 30%磁盘响应时间上千毫秒存储层磁盘队列长度、IOPS内存持续上涨不回落内存层进程RSS、堆内存曲线系统整体卡顿硬盘灯长亮虚拟内存抖动换页次数、主缺页率程序响应波动大无明显峰值缓存命中率低缓存命中率、TLB miss大文件写入后系统卡几分钟脏页刷盘脏页比例、写回阈值这张表不是绝对的但能覆盖八成场景。用它的方法是先量指标再对现象最后才改配置。顺序反了就容易做无用功。6. 自己动手观测一次完整的三层交互原理讲再多不如自己看一次真实数据。下面这套操作在Linux上可以直接复现能让你亲眼看到CPU、内存、磁盘是怎么联动起来的。6.1 准备观测环境需要三个终端窗口。第一个跑监控第二个跑测试程序第三个做对比实验。监控命令用vmstat和iostat组合# 终端1每2秒输出一次观察CPU、内存、换页 vmstat 2 # 同时另一个窗口看磁盘 iostat -x 2vmstat输出里重点关注这几列r是运行队列长度大于CPU核数说明有排队si/so是换入换出非零就要警惕us/sy/wa分别是用户态、内核态、I/O等待占用的CPU时间。wa高就是典型的I/O瓶颈。6.2 做一次能看出差异的对比实验先用一个小文件测试数据量小于内存# 生成一个 1GB 的测试文件 dd if/dev/zero of/tmp/testfile bs1M count1024 # 第一次读数据要从磁盘进内存 time cat /tmp/testfile /dev/null # 第二次读数据可能还在页缓存里 time cat /tmp/testfile /dev/null两次耗时会有明显差异第二次通常快好几倍这个差距就是页缓存的作用。如果想强制清空缓存再对比可以用sync echo 3 /proc/sys/vm/drop_caches第二条命令需要root权限而且只在测试环境用生产环境执行会瞬间打满磁盘I/O。6.3 观测时容易忽略的几个细节第一free命令里的available才是真正可用的内存free那一列低不代表内存紧张因为Linux会把空闲内存拿去做缓存。第二iostat里的%util接近100%只说明设备忙碌不等于性能瓶颈还要看await平均等待时间。第三vmstat的wa并不包含所有I/O等待某些异步I/O不会计入所以要结合iostat一起看。7. 踩过坑之后总结的几条经验关于内存分配器很多人不知道glibc默认的分配器在多线程场景下会有锁竞争表现是多线程程序线程数上去后性能反而下降。换成jemalloc或tcmalloc往往能缓解这不是玄学是分配器实现差异导致的真实差距。关于容器环境free看到的内存和容器实际能用的内存不是一回事。容器里跑的应用要读 cgroup 的限额文件才知道自己的真实上限Java应用要特别注意JVM默认会按宿主机内存来算堆大小容器限额不显式指定就容易OOM被杀。关于磁盘管理扩容和合并分区这类操作前一定要备份。热词里磁盘1合并之后里面的东西不见了vm指定文件不是虚拟磁盘这两个本质都是操作前没确认对象和后果。存储操作不可逆的比例远高于内存和CPU类的操作谨慎一点没坏处。我个人在实际操作中的体会是这套体系里最值得花时间理解的就是速度差这一个概念。CPU快、内存中、磁盘慢所有的缓存、预取、异步、批量都是为了抹平这个差距。你把这个差距的数量级记牢遇到性能问题时先问自己这次访问落在哪一层方向基本上不会错。剩下的事情就是查工具、看数据、验证猜想一步步把范围缩小。