第3章:硬件与操作系统优化
说实话,很多做高并发的同学,一上来就盯着代码层面优化。什么锁竞争、线程池大小、连接池配置……这些当然重要。但我个人经验是,硬件和操作系统层面的调优,往往能带来立竿见影的效果。你想想看,代码写得再漂亮,底层硬件不给力,操作系统调度一团糟,那性能天花板就在那儿摆着。
这一章,我带你看看底层那些事儿。CPU怎么绑定、内存怎么用大页、磁盘I/O调度器选哪个、网络协议栈怎么调。嗯,都是实战中踩过坑才总结出来的。
核心观点:硬件和操作系统优化,是高性能系统的基石。代码优化是「锦上添花」,底层优化是「雪中送炭」。
3.1 CPU绑定与NUMA感知
先说说CPU绑定。说白了,就是让某个进程或线程,只跑在指定的CPU核心上。为什么要这么做?
我遇到过这样一个场景:一个交易系统,核心处理线程在多个CPU核心间来回切换。每次切换,CPU缓存就失效了。性能下降得厉害。后来做了CPU绑定,吞吐量直接提升了15%。
CPU绑定的实现方式,Linux下用taskset命令或者sched_setaffinity系统调用。举个例子:
# 将进程PID绑定到CPU 0和1
taskset -cp 0,1 <PID>
# 启动时直接绑定
taskset -c 0,1 ./myapp
但光绑定CPU还不够。现代服务器都是NUMA架构(非统一内存访问)。什么意思呢?每个CPU有自己的本地内存,访问本地内存快,访问远程内存慢。
我记得有一次排查性能问题,发现某个进程的内存访问延迟特别高。一看,进程跑在CPU 0上,但内存分配到了CPU 1的节点上。这就是典型的NUMA陷阱。
NUMA感知的优化策略:
- 内存分配策略:使用
numactl命令,让进程的内存分配尽量在本地节点 - 线程与内存绑定:核心线程和它使用的内存,尽量在同一个NUMA节点
- 避免跨节点访问:跨节点访问延迟,通常是本地访问的1.5-2倍
# 查看NUMA拓扑
numactl --hardware
# 在指定节点运行程序
numactl --cpunodebind=0 --membind=0 ./myapp
我的建议:对于延迟敏感的交易系统,一定要做CPU绑定+NUMA感知。我一般会预留一个CPU核心给操作系统和中断处理,剩下的核心全部绑定给业务线程。
3.2 内存大页与Huge Pages
内存管理这块,有个经典问题:TLB(页表缓存)缺失。进程访问内存时,CPU先查TLB。如果TLB没命中,就得去查页表,那性能就下来了。
默认情况下,Linux内存页大小是4KB。一个进程如果用了2GB内存,那页表项就有50多万条。TLB根本装不下。怎么办?用大页。
Huge Pages,说白了就是把内存页从4KB变成2MB甚至1GB。页表项数量骤减,TLB命中率大幅提升。
我在一个内存数据库项目中做过测试:启用Huge Pages后,查询延迟降低了20%左右。效果非常明显。
配置方式:
# 查看当前大页配置
cat /proc/meminfo | grep Huge
# 设置2MB大页数量(比如1024个)
echo 1024 > /proc/sys/vm/nr_hugepages
# 挂载hugetlbfs
mount -t hugetlbfs hugetlbfs /dev/hugepages
Java应用启用大页,JVM参数加一行:
-XX:+UseLargePages -XX:LargePageSizeInBytes=2m
注意:Huge Pages需要预先分配,而且不能被换出到磁盘。分配太多会浪费内存,分配太少又不够用。我建议先压测,根据实际内存使用量来配置。
3.3 磁盘I/O调度器选择
磁盘I/O调度器,决定了I/O请求怎么排队、怎么合并、怎么下发。不同的调度器,适合不同的场景。
Linux主流调度器有三种:
| 调度器 | 特点 | 适用场景 |
|---|---|---|
| CFQ(完全公平队列) | 每个进程一个队列,公平分配I/O时间 | 传统机械硬盘,多进程场景 |
| Deadline(截止时间) | 按请求截止时间调度,读写分离 | 数据库、交易系统,延迟敏感 |
| NOOP(先进先出) | 简单合并,不做复杂调度 | SSD、NVMe,硬件自己处理 |
我个人习惯,交易系统用SSD的话,选NOOP或者None。为什么?因为SSD的随机读写性能已经很强了,不需要调度器做太多干预。反而调度器会增加CPU开销和延迟。
如果是机械硬盘,我建议用Deadline。它保证了读请求不会因为写请求太多而被饿死。我在一个日志系统中遇到过,写日志太频繁,读请求等了很久才响应。换成Deadline后,问题解决了。
查看和修改调度器:
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# 修改为deadline
echo deadline > /sys/block/sda/queue/scheduler
3.4 网络协议栈调优
网络这块,坑最多。尤其是高并发场景下,TCP协议栈的默认配置根本扛不住。
先说说tcp_tw_reuse。TIME_WAIT状态,是TCP四次挥手中的最后一个状态。主动关闭连接的一方,会进入TIME_WAIT,默认持续2MSL(约60秒)。
高并发下,短连接特别多。如果服务端主动关闭连接,TIME_WAIT连接会堆积。端口被占满,新连接就建立不了。
我曾经遇到过一个线上事故:交易系统突然大量报错,一看,端口不够用了。TIME_WAIT连接占了上万个端口。后来启用了tcp_tw_reuse,问题解决。
# 允许重用TIME_WAIT状态的连接
echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse
# 注意:tcp_tw_recycle在Linux 4.12之后已废弃,别用了
再说说tcp_rmem和tcp_wmem。这两个参数控制TCP接收和发送缓冲区的大小。
默认值通常偏小。对于高吞吐场景,需要调大:
# 接收缓冲区:最小值、默认值、最大值(字节)
echo "4096 87380 16777216" > /proc/sys/net/ipv4/tcp_rmem
# 发送缓冲区:最小值、默认值、最大值(字节)
echo "4096 65536 16777216" > /proc/sys/net/ipv4/tcp_wmem
其他常用调优参数:
- tcp_fin_timeout:FIN_WAIT_2状态的超时时间,默认60秒,可以调小到15秒
- tcp_tw_buckets:系统最大TIME_WAIT连接数,超过后新连接会被丢弃
- somaxconn:监听队列的最大长度,默认128,高并发下要调大
- netdev_max_backlog:网卡接收队列的最大长度
# 完整调优示例
echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout
echo 20000 > /proc/sys/net/ipv4/tcp_tw_buckets
echo 1024 > /proc/sys/net/core/somaxconn
echo 10000 > /proc/sys/net/core/netdev_max_backlog
避坑指南:我曾经在调优时,把tcp_tw_reuse和tcp_tw_recycle一起开了。结果NAT环境下的客户端连接全断了。记住,tcp_tw_recycle在NAT环境下有严重问题,别用。
知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以对照着看,心里有个整体框架。
嗯,这一章的内容就这些。CPU绑定和NUMA感知,解决的是计算资源局部性问题。Huge Pages解决的是内存访问效率问题。磁盘I/O调度器,解决的是存储性能问题。网络协议栈调优,解决的是通信瓶颈问题。
这四个维度,你都得关注。别只盯着一个点猛调,其他方面成了短板,整体性能还是上不去。
下一章,我们聊聊更细粒度的东西——锁优化和无锁编程。那又是另一片天地了。
公众号:蓝海资料掘金营,微信deep3321