Linux 性能优化实战:打通磁盘、内存、TLB 与 Cache
> 来源:基于线上服务调优经验与 Linux 常用性能工具链整理。
一、为什么很多优化会失败
线上性能问题经常不是单点瓶颈,而是链路瓶颈。
一个典型误区是:
- 看到
iowait高就只改磁盘参数。 - 看到 CPU 高就只扩容 CPU。
- 看到延迟抖动就只调线程池。
结果往往“局部变快,全局变慢”。根因是没有把链路打通看。
二、先建立链路视角:磁盘 -> 页缓存 -> 内存回收 -> TLB -> CPU Cache
1. 读请求链路
应用 read()/mmap() 读文件
-> 先查页缓存(Page Cache)
-> 命中:直接返回(内存带宽和 CPU cache 行为主导)
-> 未命中:触发块设备 IO(磁盘/NVMe)
-> 页面进入内存后,需要页表映射
-> CPU 通过 TLB 缓存虚拟地址转换
-> 最终访问 L1/L2/L3 cache 与 DRAM
2. 写请求链路
应用 write()
-> 先写页缓存(脏页)
-> 内核后台回写(writeback)到磁盘
-> 脏页比例过高时,前台线程可能被迫回写(卡顿)
3. 链路上的关键耦合
- 页缓存不足:读命中率下降,磁盘压力上升。
- 脏页过多:回写拥塞,应用线程阻塞。
- 频繁缺页:页表与 TLB 压力上升。
- TLB miss 高:CPU 在做地址翻译,真实业务指令占比下降。
- CPU cache miss 高:每次访问都要去更慢层级,延迟拉长。
所以优化要按链路做,不要按单点做。
三、内存优化深挖:这是性能稳定性的核心
1. 先分清“内存在干什么”
Linux 内存大致分三块视角:
- 匿名页(anonymous):堆、栈、进程私有内存。
- 文件页(file-backed):页缓存、mmap 文件页。
- 内核内存(slab/vmalloc 等):内核对象、网络缓冲等。
很多“内存占用高”其实是页缓存高,这本身不一定是坏事。 坏信号是:回收抖动、swap 抖动、major fault 持续升高。
2. 必看的内存指标与含义
全局指标
vmstat 1
cat /proc/meminfo
sar -B 1
sar -W 1
重点观察:
free低但Cached高:常见且正常。si/so持续非零:在发生 swap 抖动。pgmajfault/s高:进程在付出“磁盘级”缺页代价。pgscan/pgsteal急升:内核在激烈回收页面。kswapd高占用:内存水位长期紧张。
进程级指标
pidstat -r -p <pid> 1
cat /proc/<pid>/status
cat /proc/<pid>/smaps_rollup
重点看:
- RSS 增长速度和峰值。
- 匿名页与文件页比例。
- minor/major fault 变化趋势。
3. 回收路径:kswapd 与 direct reclaim
两类回收行为会带来非常不同的体感:
kswapd:后台回收,抖动相对可控。direct reclaim:业务线程亲自回收,延迟会突然尖刺。
如果线上出现“偶发几百毫秒到秒级卡顿”,要优先排查 direct reclaim。
可观察:
cat /proc/vmstat | egrep 'pgscan|pgsteal|allocstall|kswapd'
allocstall 上升通常意味着应用线程被迫进入 direct reclaim。
4. swap 策略不是越低越好
常见经验是把 vm.swappiness 拉低,但这要看业务形态:
- 低延迟在线服务:通常倾向
1~10,尽量避免匿名页被换出。 - 批处理任务:可适度提高,换取更高内存利用率。
建议:先压测再定,不要用单机经验直接套所有节点。
5. 脏页与回写参数
写多场景里,脏页策略会直接影响尾延迟。
关键参数:
vm.dirty_background_ratio或vm.dirty_background_bytesvm.dirty_ratio或vm.dirty_bytesvm.dirty_expire_centisecsvm.dirty_writeback_centisecs
理解方式:
background到阈值:后台开始慢慢刷。dirty_ratio到阈值:前台写线程可能被限速甚至阻塞。
如果你看到“平时很快,偶发写入卡顿”,通常是脏页回写波峰过大。
6. THP / HugePage 与 TLB 的关系
TLB 缓存“虚拟地址 -> 物理地址”映射。
- 页越小(4KB),同样内存范围需要更多 TLB 项。
- 页越大(2MB/1GB),TLB 覆盖范围更大,TLB miss 可能下降。
所以 THP 有机会提升性能,但不是无脑开启就一定好:
- 受益场景:大块顺序内存访问、数据库 buffer、大模型推理内存区。
- 风险场景:内存碎片严重、频繁内存伸缩、延迟敏感且 compact 抖动明显。
建议用 A/B 压测验证 always/madvise/never,同时看 P99 而不只看平均值。
7. NUMA:远程内存访问会“偷走”吞吐
多路服务器常见问题:线程跑在 NUMA0,内存分配在 NUMA1。
后果:
- 远程访存延迟更高。
- 内存带宽利用不均。
- 同样 CPU 利用率下吞吐更低。
工具:
numactl --hardware
numastat -p <pid>
优化方向:
- 关键服务做 CPU 与内存绑核绑节点。
- 减少线程跨 NUMA 漂移。
- 对高性能场景采用“分片+本地化”设计。
8. 容器场景:cgroup 内存限制与 OOM
在 Kubernetes 或容器环境里,很多“系统还有空闲内存但服务被杀”的问题,本质是 cgroup 内存超限。
重点排查:
memory.current是否频繁触顶memory.maxmemory.events中high/max/oom/oom_kill是否持续增加- 应用是否把页缓存与匿名内存都算进了同一限制导致互相挤压
常见策略:
- 给关键服务预留 headroom,不要把 limit 贴得过紧
- 区分请求内存与限制内存,避免调度后天然处于高压
- 结合应用级缓存上限,避免“业务缓存把自己挤死”
9. 内存碎片与 compaction 抖动
如果需要大块连续页(THP 或驱动分配),内存碎片会触发 compaction,造成短时延迟尖峰。
可观测信号:
compact_stall、compact_fail增长- THP 分配失败率升高
- P99 在流量不变时周期性抖动
实战建议:
- 对延迟敏感服务优先用
madvise而非always - 启动期做预热分配,减少运行期大块分配
- 周期性大对象创建销毁要改为复用池化
四、TLB 与 CPU Cache:很多“CPU 高”本质是访存低效
1. TLB miss 为什么会拖慢系统
每次虚拟地址访问都需要地址翻译:
- TLB 命中:很快。
- TLB 未命中:需要页表遍历,额外内存访问,延迟增加。
在随机访问、工作集远大于 TLB 覆盖范围时,性能会明显下滑。
2. CPU Cache miss 的代价
访问层级延迟大致是:L1 < L2 < L3 << DRAM。
如果数据局部性不好:
- 指令 cache miss 上升:执行流水线受影响。
- 数据 cache miss 上升:等待内存,IPC 下降。
常见诱因:
- 数据结构过度分散(链表跳转多)。
- 热路径对象太大,跨 cache line 频繁。
- 多线程伪共享(false sharing)。
3. 可用观测手段
perf stat -e cycles,instructions,cache-references,cache-misses,dTLB-load-misses,iTLB-load-misses -p <pid> -- sleep 30
perf top -p <pid>
可以重点看:
- IPC(instructions per cycle)
cache-misses / cache-referencesdTLB-load-misses
这些指标能帮你判断是“算力不足”还是“访存效率低”。
五、磁盘优化要和内存联动看
1. 不要只盯 %util
iostat -x 里更有价值的是:
r/sw/s:IOPS 负载rkB/swkB/s:吞吐await:端到端等待avgqu-sz:队列积压
%util 接近 100% 在 NVMe 上不总是“已到极限”,要结合延迟与队列一起看。
2. 页缓存命中率决定了磁盘压力上限
如果业务随机读多,内存给页缓存不够,命中率会下降,磁盘 await 会抬高。
优化不是“一味加磁盘”,而是:
- 调整工作集,提升热数据驻留。
- 改访问模式,减少随机小 IO。
- 给关键读路径预读或应用层缓存。
3. 写路径的典型抖动
写入突发时:
- 脏页快速堆积。
- 回写线程追不上。
- 前台线程被迫参与回写。
你会看到业务延迟突然拉长,同时磁盘带宽并不一定跑满。
六、CPU 优化:把“争用”和“抖动”优先消掉
核心思路:
- 先降上下文切换,再谈提高利用率。
- 先看锁竞争,再看纯计算热点。
- 先稳住 P99,再追求平均吞吐。
常用命令:
mpstat -P ALL 1
pidstat -u -t -w -p <pid> 1
perf top -p <pid>
高频收益项:
- 线程数匹配核数,避免过度并发。
- 缩短锁持有时间,降低大锁覆盖范围。
- 核心线程 CPU 亲和性绑定,减少迁移。
七、网络与内存也有强耦合
网络吞吐上去后,经常是内存子系统先告警:
- skb 分配回收频繁,slab 压力上升。
- socket 缓冲区过大或过小都可能引发抖动。
- 中断与 softirq 分布不均导致单核热点。
观测:
ss -s
sar -n TCP,DEV 1
ethtool -S <nic>
cat /proc/softirqs
八、一个可执行的排障流程(建议固化到值班手册)
1. 先确认症状:吞吐下降、P99 升高、错误率变化。 2. 快速分层:CPU、内存、磁盘、网络谁先异常。 3. 拉链路指标: - 磁盘:iostat -x 1 - 内存:vmstat 1 + /proc/vmstat - TLB/Cache:perf stat 4. 建立因果:例如“页缓存下降 -> 磁盘 await 升高 -> 请求延迟升高”。 5. 小步调参:每次只改一类参数,保留回滚。 6. 压测复验:看 P95/P99、稳定性和波动幅度。
链路判定示例:
pgmajfault上升 +await上升:通常是页缓存失守,读请求回落到磁盘。allocstall上升 + P99 尖刺:通常是 direct reclaim 影响前台线程。cache-miss与dTLB-load-misses同升:通常是工作集过大或局部性变差。
九、参数建议(起点,不是标准答案)
# 低延迟服务常见起点
vm.swappiness = 10
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
# 高并发网络服务常见起点
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
# 文件句柄上限
fs.file-max = 1048576
这些参数必须结合你的业务负载验证,尤其是写密集和内存敏感场景。
十、优化手段全集(按层落地)
1. 数据结构与代码层
- 热字段前置:把高频访问字段放到结构体前半段,提升 cache 命中。
- 缩小对象体积:减少指针跳转和不必要字段,提升单位 cache line 有效数据比例。
- SoA 替代 AoS(按场景):批处理场景中常更利于向量化与顺序访问。
- 避免伪共享:多线程频繁写的计数器/状态变量分离到不同 cache line。
- 减少内存分配抖动:对象池、arena、批量分配,降低 allocator 开销与碎片。
- 降低分支失预测:热路径减少复杂分支,把异常路径后置。
2. 结构体 cache line 对齐:什么时候会更快
你的判断方向是对的,但要加边界条件。
结论:
- 对“多线程写热点字段”来说,按 cache line 对齐通常能明显降低伪共享,延迟会更稳。
- 对“只读或低频写字段”来说,盲目对齐会增大内存占用,可能让 cache 命中变差,反而变慢。
示例(C/C++):
#include <atomic>
// 常见 x86 cache line 为 64B(不同平台可能不同)
struct alignas(64) ShardCounter {
std::atomic<uint64_t> value;
char pad[64 - sizeof(std::atomic<uint64_t>)];
};
适用场景:
- 每个线程维护独立计数器,再周期性汇总。
- 高频写状态位(队列 head/tail、统计计数器)。
不建议场景:
- 大量对象实例都做 64B 对齐但写入并不频繁。
- 对象本来很小,强行对齐导致内存膨胀和 TLB 压力增大。
3. 内存访问局部性优化
- 顺序访问优先:把随机访问改为批量顺序扫描。
- 分块处理(blocking):大数组按块处理,减少 cache 污染。
- 预取(prefetch):对可预测访问模式可试探性预取(需实测)。
- 减少跨页跳转:大对象图结构可做内存池化,降低 TLB miss。
4. 内存分配器与运行时策略
- 通用服务可对比
glibc malloc与jemalloc/tcmalloc(看 tail latency)。 - 减少小对象频繁分配释放,优先复用。
- 观察 allocator 竞争:线程缓存(tcache)参数按场景调优。
- GC 语言(Java/Go)要把 GC 参数纳入性能链路,避免只看内核参数。
5. 页缓存与回写链路
- 读多业务:提升页缓存命中率比盲目提磁盘规格更有效。
- 写多业务:控制脏页峰值,避免前台线程被迫回写。
- 对日志写入做批量和异步化,减少 fsync 风暴。
- 数据与日志分离,降低互相干扰。
6. TLB/THP/NUMA 联动优化
- 大工作集优先评估 THP
madvise,关注dTLB-load-misses与 P99。 - NUMA 机器要做“线程-内存本地化”,避免远程内存访问。
- 极端延迟场景可考虑预热映射,减少冷启动 page fault。
7. CPU 与调度层
- 线程数与核心数匹配,避免 run queue 长期过高。
- 降低锁争用:分段锁、读写分离、无锁结构(按复杂度选择)。
- 关键线程绑定 CPU,减少迁移与 cache 热数据丢失。
- 中断和业务线程分核,避免互相抢占。
8. 磁盘与文件系统层
- 识别 IO 模式:随机读写与顺序读写采用不同策略。
- NVMe 场景重点看
await、队列深度,而不只看%util。 - 合并小 IO,控制写放大,减少 metadata 高频更新。
- 评估文件系统挂载参数(atime、barrier 等)对延迟的影响。
9. 网络栈层
- backlog、rmem/wmem、连接复用策略配套调优。
- RPS/RFS/XPS 与 IRQ 亲和性协同设置,降低单核热点。
- 减少不必要拷贝与序列化次数,降低 CPU 与 cache 压力。
10. 工程方法层
- 每次只改一类因素,必须留回滚。
- 结论以 P95/P99 和稳定性为准,不以单次峰值为准。
- 建立“指标 -> 根因 -> 变更 -> 结果”台账,沉淀团队知识。
十一、总结
Linux 性能优化最怕“单点调优”,最有效的是“链路调优”。
一条实用原则:
- 先看磁盘与页缓存关系。
- 再看内存回收与缺页行为。
- 再看 TLB/Cache 是否在吞噬 CPU 周期。
把这三层打通,绝大多数线上性能问题都能更快定位、更稳优化。
评论