第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_rmemtcp_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_reusetcp_tw_recycle一起开了。结果NAT环境下的客户端连接全断了。记住,tcp_tw_recycle在NAT环境下有严重问题,别用。

知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以对照着看,心里有个整体框架。

硬件与操作系统优化知识体系 系统性能优化 CPU优化 CPU绑定 (taskset) NUMA感知 (numactl) 缓存亲和性 内存优化 Huge Pages (2MB/1GB) TLB命中率提升 内存分配策略 磁盘I/O优化 I/O调度器选择 CFQ / Deadline / NOOP SSD vs 机械硬盘 网络协议栈优化 tcp_tw_reuse tcp_rmem / tcp_wmem tcp_fin_timeout somaxconn 四维优化,缺一不可

嗯,这一章的内容就这些。CPU绑定和NUMA感知,解决的是计算资源局部性问题。Huge Pages解决的是内存访问效率问题。磁盘I/O调度器,解决的是存储性能问题。网络协议栈调优,解决的是通信瓶颈问题。

这四个维度,你都得关注。别只盯着一个点猛调,其他方面成了短板,整体性能还是上不去。

下一章,我们聊聊更细粒度的东西——锁优化和无锁编程。那又是另一片天地了。


公众号:蓝海资料掘金营,微信deep3321