☰
RHEL 9.7系统性能优化实战:从内核参数到应用侧调优
2026/10/8 2:50:16 网站建设 项目流程

上周我刚把一批承载Java网关的服务器从RHEL 8.10原地升级到RHEL 9.7,升级过程顺利得有点意外,业务方也没来找麻烦。结果压测报告一出来,对面直接甩了句“P99比之前高了一截”。我第一反应是怀疑内核调度策略变了,可真把数据拉出来看,发现事情没那么简单——RHEL 9.x默认的调优思路和8.x不完全一样,而我们那套部署长期吃的是8.x时代攒下来的“参数红利”。升级之后,一部分红利失效了,另一部分被新机制接管,问题就这么暴露出来了。

这篇文章不打算写成参数堆砌清单,而是把我在RHEL 9.7上做系统优化的一整条链路拆开来讲:从内核引导参数、tuned配置集,到网络栈、存储、内存NUMA、进程调度,再到应用侧编译和性能剖析,最后是效果度量与回归验证。适合正在从8.x往9.x迁移、或者新装RHEL 9.7准备承载生产负载的运维、SRE、后端性能优化同学参考。我会把每一步为什么这么做、踩过哪些坑都写清楚,你可以按章节直接落地。

1. 升级后的第一件事:先看默认配置把系统带到了哪里

1.1 升级后先看这四样东西

拿到一台9.7之后,我习惯先检查四件事,而不是急着改参数:

  • /proc/cmdline:内核命令行实际生效的参数,很多优化都从这里开始。
  • /sys/kernel/mm/transparent_hugepage/enabled:透明大页当前策略。
  • /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor:CPU频率调节器是performance还是powersave。
  • tuned-adm active_profile:当前生效的tuned配置集。

这四项决定了CPU、内存、电源管理的基础行为,它们没摸清楚,后面所有优化都是把楼盖在沙子上。RHEL 9.7的内核基于5.14系列持续迭代,调度器、内存管理、网络栈都在不断变化,默认值不一定差,但一定不是为了你的业务定制的。升级之后第一步不是“加参数”,而是确认系统现在处于什么状态,再决定往哪个方向调整。

举个例子,RHEL默认透明大页策略是always,对绝大多数Java、数据库类应用来说并不友好。khugepaged后台线程会持续扫描内存并尝试合并大页,合并过程中可能造成额外的CPU占用和内存碎片,甚至导致个别时候出现让人摸不着头脑的延迟毛刺。这个默认策略不是9.7的错,但如果你从8.x带来的服务没有显式处理THP,升级后问题就会一直在那里。

1.2 内核命令行参数:该动的和不该动的

通过grubby修改内核命令行参数,而不是直接改/etc/default/grub再跑grub2-mkconfig,在RHEL 9.x上更安全、也更直观。比如我这样操作:

grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="transparent_hugepage=never nmi_watchdog=0"

改完以后用grubby --info=ALL确认,重启后看/proc/cmdline。这里说几个我实际使用中认为值得考虑的选项:

  • transparent_hugepage=never或madvise:前者彻底关闭THP,后者只对显式调用madvise(MADV_HUGEPAGE)的应用启用。数据库、Java服务我一般用madvise或never,把内存合并的决策权还给应用。
  • nmi_watchdog=0:关闭NMI看门狗周期性中断,能减少一点系统噪声。对延迟敏感的服务有帮助,但如果是需要硬件故障快速发现的关键节点,保留它更稳妥。
  • intel_idle.max_cstate=1 processor.max_cstate=1:限制CPU进入深度空闲状态,降低从C state唤醒的延迟。适用于在线低延迟场景,代价是空闲功耗上升。纯计算型批处理反而没必要。
  • loglevel=3:减少控制台日志输出,对性能影响非常小,但可以让启动阶段和运行期的中断噪声少一点。

也有几个参数我明确不建议动:isolcpus只在确定要做CPU隔离、并且有配套中断绑定和进程绑核方案时才碰,否则容易把业务线程“关在笼子里”却没人喂食;audit=0能减少一点auditd开销,但会牺牲审计能力,等保和合规场景直接禁用;numa=off这种暴力参数更是想都不要想,代价远大于收益。

1.3 内核参数不是跑分工具

经常有人问我“这些参数能不能直接抄到生产”,我的回答是:抄可以,但必须理解参数之间的联动。比如transparent_hugepage=never在某些大内存读多写少的场景反而会降低TLB命中率,因为我关掉了大页映射。这时候正确做法是用madvise,让应用自己决定。同样,nmi_watchdog=0对大多数机器影响很小,但在特定硬件平台上有用,换一批机器可能毫无感知。

所以内核命令行调整的核心原则是:一次只改一个目标,重启验证,记录前后差异。别一口气塞五六个参数进去,出了问题你根本不知道是谁的锅。

2. tuned配置集:把调优沉淀成配置而不是临时命令

2.1 为什么我强烈建议用tuned而不是堆sysctl

每个团队里几乎都有一个“老运维”的服务器上躺着一份几十行的/etc/sysctl.conf,每行还带着注释“某年某月为了某事故加的”。这种做法的最大问题是不可验证、不可按负载切换。systemd-sysctl只在开机时加载一次,运行期你改动还得手动执行sysctl --system,而且不同机器之间配置漂移严重。

RHEL 9.7携带的tuned已经是2.x系列,本身是一个systemd守护进程,通过profile的方式管理一组调优参数。它不仅仅能改sysctl,还能调整CPU governor、透明大页、内核命令行参数、磁盘设备的读取ahead大小,甚至可以根据网卡流量动态切换参数。这才是我理解的“调优沉淀成代码”:一个profile就是一个可版本管理、可回滚、可复制的调优单位。

2.2 按负载选型:默认profile怎么挑

RHEL 9.7自带了一组profile,我简单整理一下使用场景:

Profile适用场景我的一般用法
balanced默认均衡配置开发测试机、低负载混合服务
throughput-performance吞吐优先,关闭省电批处理、大数据分析、文件传输
latency-performance低延迟优先在线接口、网关、数据库主节点
network-latency网络低延迟增强高频交易、实时音视频信令
virtual-guest虚拟机guest侧云上虚拟机实例

选型建议是:在线服务优先latency-performance或以其为基础做自定义;离线吞吐型任务用throughput-performance;云上虚拟机用virtual-guest。注意network-latency不要无脑启用,它会把busy_poll等参数开得比较大,某些网卡驱动不支持时反而增加CPU占用。

2.3 自定义profile实战:一个Java在线服务的例子

通常我不会直接套默认profile,而是在它基础上include一层自己的配置。下面是我在9.7上给一个Java在线服务做的profile:

[main] summary=Latency optimized profile for Java online services include=latency-performance [cpu] governor=performance force_latency=1 [vm] transparent_hugepages=madvise [sysctl] vm.swappiness=10 net.core.somaxconn=4096 net.ipv4.tcp_fin_timeout=15

把这段配置放到/etc/tuned/latency-java/tuned.conf,然后执行:

tuned-adm profile latency-java tuned-adm active

重点看三个验证命令:

cat /sys/kernel/mm/transparent_hugepage/enabled cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor sysctl vm.swappiness net.core.somaxconn

这个profile的运行逻辑是:先继承latency-performance的默认调优,再把CPU锁定在performance governor,THP改为madvise,最后按业务需求调整swap和连接队列参数。整条链路清晰,哪天不想用了,tuned-adm profile balanced一条命令回滚。

2.4 tuned使用中的两个坑

第一,修改了/etc/tuned/下的profile之后,必须重启tuned或者切换再切回profile才会重新加载,只改文件不会热生效。我一般用tuned-adm off && tuned-adm profile latency-java来强制重载,避免因为缓存导致配置不生效。第二,include只是把父profile的参数作为基础,同一参数的子profile覆盖关系要梳理清楚。如果发现改完没生效,先去看tuned-adm active确认当前profile路径,再确认是否存在两个profile同时指向同一个sysctl参数。

tuned不是银弹,但它是RHEL体系中管理调优的最佳载体。我的习惯是:所有运行期需要持久化的参数,能进tuned就进tuned,不进/etc/sysctl.conf。

3. 网络与存储:把中断和队列安排明白

3.1 网卡多队列、ring buffer与中断处理

网络优化里最容易立竿见影的是网卡队列。很多机器默认网卡队列数等于CPU核数,但ring buffer通常是自动协商的较小值。先用ethtool -l eth0看当前最大值和实际值,再决定要不要调:

ethtool -L eth0 combined 8 ethtool -G eth0 rx 4096 tx 4096

ethtool -L调整队列数量,ethtool -G调整ring buffer大小。注意ring buffer调大会占用内存,并且有些网卡驱动对数值有上限要求,先看ethtool -l的输出再动手。

中断绑定方面,RHEL 9.7默认运行irqbalance服务,通常不需要手动绑中断,让系统动态分配应对突发流量更合理。但如果你的业务要求极低延迟、或者发现单核softirq占用接近100%,才考虑关闭irqbalance手动把队列中断绑定到指定CPU。手动绑定的代价是后续扩缩容和硬件变更都要手动维护,我一般只在数据库和实时音视频场景做。

判断是否需要调整网卡队列,主要看mpstat -P ALL 1里有没有某个核的softirq占比明显高于其他核,以及ethtool -S eth0 | grep -i rx里有没有rx_missed或rx_dropped持续增长。

3.2 TCP栈参数:先调对再调大

RHEL 9.7的内核对TCP默认参数已经比较合理,盲目把缓冲区调大反而会导致内存占用飙升、丢包后恢复变慢。我优先关注这几个参数:

net.core.somaxconn = 4096 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_max_syn_backlog = 8192

如果服务端短连接特别多,TIME_WAIT堆积明显,优先调tcp_fin_timeout和应用层连接复用,而不是依赖tcp_tw_reuse。在RHEL 9.x的内核版本下,TIME_WAIT快速回收机制已经变化,简单开tcp_tw_reuse可能造成连接混淆,我的经验是不依赖它。

拥塞控制算法方面,默认cubic在绝大多数数据中心内网已经足够。BBR在跨地域、高丢包链路上有优势,但在同机房低延迟网络中收益不大,还可能改变重传行为。如果一定要试BBR,先确认内核有tcp_bbr模块:

modprobe tcp_bbr sysctl -w net.ipv4.tcp_congestion_control=bbr

如果内核模块本身没编译进去,这条路就到此为止,别折腾。另外,RPS/XPS在网卡不支持多队列时可以用,但9.7上手动配置/sys/class/net/eth0/queues/rx-0/rps_cpus后要注意它和irqbalance的协作关系,两者目标相同但机制不同,容易互相干扰。

3.3 块设备与XFS挂载选项

RHEL 9.7的默认文件系统是XFS,新一代内核搭配默认挂载参数已经对SSD优化得不错。我极少改动底层调度器:NVMe保持none,机械盘保持mq-deadline,这是RHEL 9的默认选择,没必要再叠加udev规则去改。真正能在挂载层面做的是这几点:

  • 加noatime,nodiratime,避免每次读文件都更新atime,减少不必要的写IO。
  • NVMe SSD可以加discard=async,在写入时异步执行TRIM,减少后台fstrim周期性扫描带来的毛刺。
  • XFS的largeio和allocsize针对特定IO模式有效,但需要压测验证,别迷信。

验证存储优化效果,用fio跑一轮基准:

fio --name=randread --ioengine=libaio --direct=1 --bs=4k --rw=randread --size=4G --numjobs=4 --runtime=30 --group_reporting

重点对比IOPS和延迟分布,而不是只看平均值。我在实际项目中见过有人把SSD的调度器从none改成mq-deadline后,4K随机读延迟分布明显变宽,因为压缩和合并逻辑在NVMe上根本没收益,反而增加路径开销。

4. 内存、NUMA与进程调度:别让数据在CPU之间乱跑

4.1 跨NUMA节点的隐形代价

现代多路服务器的内存访问不是等价的,每个CPU访问本地内存和远端内存的延迟差异可能达到30%以上。RHEL 9.7默认启用了NUMA感知调度,但应用本身如果不做任何设置,线程还是会比较自由地在各个核之间流动,内存也会因为“就近分配”原则散布到多个节点。这个问题的严重性在线服务上会被放大——一次简单的远端内存访问,可能在每次数据库查询里反复出现。

用numactl --hardware看拓扑,重点看节点数量和距离矩阵。然后对关键进程做绑定,以Java服务为例:

numactl --cpunodebind=0 --membind=0 java -jar app.jar

这条命令把进程的CPU和内存都限制在node 0,保证线程调度和内存分配都在同一个节点内。用numastat -p <pid>验证执行效果,观察进程在remote node内存占比是否降到5%以下。

4.2 THP、脏页回收与swap的联动

透明大页在第四章又出现了一次,因为内存层面的优化往往是联动关系:THP策略决定大页分配方式,脏页阈值决定写回频率,swappiness决定匿名内存和文件缓存的回收倾向。任何单独调一项都可能打破平衡。

RHEL 9.7默认的vm.swappiness是30,对在线服务我一般调到10左右。vm.dirty_ratio和vm.dirty_background_ratio默认值在大内存机器上容易导致脏页积累到较高水位后一次性刷盘,出现IO毛刺。我通常把background ratio调低到5,ratio调低到15,让写回更平缓。注意这两个参数必须配合业务写入模式验证,纯读场景调它们没有意义。

THP的取舍再展开一点:Java应用通常维护大堆内存,THP导致的耗时波动有时比想象中明显。我在一台PostgreSQL 16主机上做过对比,从always改为madvise并重启后,pgbench读取延迟的P99从原先偶发几十毫秒尖峰变成了相对平稳的几毫秒级别,原因就是后台khugepaged的扫描和拆页不再频繁干扰业务线程。

4.3 cgroup v2下的CPU限额与隔离

RHEL 9.7默认启用cgroup v2,这改变了很多人从cgroup v1继承的习惯。现在CPU配额的表达方式变成cpu.max文件,格式是“配额值 period值”。比如限制一个服务最多用2个CPU核,写入200000 100000。

systemd-run --unit=limit-app --slice=workload.slice -p CPUQuota=200% -p MemoryMax=8G ./app

这种做法的好处是:限制和释放都是动态的,不需要重启服务,也不需要手动编辑/sys/fs/cgroup下的文件。对于混部场景,把在线业务和离线任务放进不同slice,再配合memory.high做内存水位预警,可以避免离线任务把在线服务的CPU和内存争抢到不可控。

4.4 一个真实的NUMA案例

还是上面那台PostgreSQL主机,升级后让我困惑了两天。pgbench跑同一份基准脚本,RHEL 8.10时代读写混合TPS稳定在某个区间,升级到9.7后整体低了一截。mpstat看CPU利用率并不高,vmstat也没有明显si/so,后来用numastat -p一看才发现,PostgreSQL主进程有接近45%的内存落在remote node。原因是我们用了systemd启动PG,而systemd在9.x的cgroup v2环境下对NUMA亲和性的处理与预期不同。用numactl包裹启动命令之后,remote内存占比降到5%,TPS回到了升级前的水平。这个案例告诉我:升级不只是内核版本号变了,关联组件的行为也在变,调优必须从实际观测出发。

5. 应用侧优化:从编译到热点的完整链路

5.1 编译器优化:别只盯着-O3

如果业务里包含自研的C/C++模块,RHEL 9.7自带的GCC 11够用,但想要更激进的优化可以用Software Collection里的gcc-toolset-13或更新的工具链版本,安装后启用:

dnf install gcc-toolset-13 scl enable gcc-toolset-13 bash

编译选项上,-O2和-O3的区别对不同应用差异很大。计算密集型的代码可以用-O3 -march=x86-64-v3,但如果程序要分发到不同代的CPU混跑,-march=native反而是给自己埋雷,目标机器缺某个指令集就直接崩。LTO(链接时优化)和PGO(profile-guided optimization)收益通常比无脑加-O3更稳。PGO的基本流程是:

gcc -O3 -fprofile-generate -o app app.c ./app -i workload_data # 运行代表性负载 gcc -O3 -fprofile-use -o app_final app.c

我做过的一个图像处理模块,-O3基线加上LTO和PGO之后,核心函数耗时减少了大约12%,而且没有引入兼容性问题。注意PGO的profile-data依赖输入负载的特征,换了一类数据效果可能打折扣。

5.2 用perf和bpftrace找到真正的热点

调优最忌讳的就是“我觉得这里慢”。RHEL 9.7自带的工具链足够做完整的剖析。先把perf的用户态采样权限放开:

sysctl -w kernel.perf_event_paranoid=1

然后对目标进程做CPU热点采样:

perf record -g -p <pid> -- sleep 30 perf report

如果服务是Java应用,perf看到的是JVM的native栈,配合-XX:+PreserveFramePointer和JIT符号映射会有更好的效果。但快速定位阶段,直接用perf top看哪个符号占比最高就够了。

除了CPU热点,还要关注off-CPU时间——进程在等待什么。bpftrace是4.x内核时代就常用的工具,在RHEL 9.7上可以这样统计任务切换时的等待原因:

bpftrace -e 'k:finish_task_switch { @[comm] = count(); }'

实际使用中,我经常组合这两类工具:先用perf看CPU上有哪些函数在烧火,再用bpftrace看线程到底在什么内核路径上排队。比如一次高延迟排查,perf显示cgroup相关的函数占用异常高,顺着这个方向才发现是cgroup v2的cpu.max配置导致线程频繁被throttle,跟应用代码一点关系都没有。

5.3 一个快速的诊断判断顺序

面对一个“变慢了”的服务器,我通常按照这个顺序走,每一步都能排除一个大类:

  1. 看uptime负载与CPU核数的比例,判断是过载还是正常。
  2. 看vmstat 1的r队列、us/sy占比、si/so情况,区分CPU忙、内核忙还是内存回收忙。
  3. 如果sys占用高,用perf top看内核热点;如果wa高,用iostat -x 1看块设备队列。
  4. 如果一切正常但业务依然慢,再考虑跨NUMA访问、锁竞争、网络重传这些深水区。

这个顺序帮我避开了一个常见的坑:直接在sysctl上做文章,结果最后发现是网络重传导致接收窗口收缩,业务端的延迟只是表象。

6. 优化不是改完就结束:全套验证与回归

6.1 用PCP搭一套可回溯的监控基线

RHEL 9.7自带Performance Co-Pilot(PCP),这是我在优化后必搭的监控层。安装并启动采集:

dnf install pcp pcp-system-tools systemctl enable --now pmcd pmlogger

默认情况下它就在后台持续采集CPU、内存、网络、文件系统等关键指标,按时间归档。等下次需要复盘某次变更前后差异时,直接用pmchart或pmrep拉历史数据,不用临时抱佛脚去找监控。性能优化最怕的是“当时忘了记录”,PCP的存在就是为了解决这个痛点。

6.2 基准测试要做横向对比

优化前和优化后必须用同一套基准脚本、同一批硬件、同一个时间段跑,才有可比性。我这里给出一个模板:

场景指标优化前优化后备注
网络TCP单流吞吐量12.1 Gbps12.6 Gbpsnetperf TCP_STREAM
网络TCP并发连接建连速率8.4k/s9.1k/swrk压测
4K随机读IOPS98k104kfio libaio
PostgreSQL读写混合TPS63206940pgbench scale=500

注意这里的数据只是示例,不是给你做预期参考。真正重要的是“优化前后”的差异,以及差异是否稳定。一次跑出来的结果可能是噪声,至少要三轮取中位数。

6.3 变更回滚与长期回归

每一次优化都需要能回滚。tuned的profile切换是天然的回滚机制,内核命令行参数通过grubby移除,sysctl直接恢复默认值。但回滚的前提是有记录,我建议每个参数都记录三行信息:为什么改、改之前的值、怎么验证。

长期观察方面要防止“局部优化导致全局劣化”。比如把vm.swappiness调成0确实短期内减少了swap IO,但长时间运行后Java堆内存不再被主动回收,最终触发OOM。这种问题在基准测试两小时内看不出来,必须让业务跑上几天再回看趋势。我惯用的做法是每周拉一次PCP归档的周报,对比CPU idle、内存回收、软中断、IO等待趋势,出现异常波动及时回滚到上一版profile。

优化的终点从来不是“参数调完的那一刻”,而是业务稳定运行一段时间后仍然成立。RHEL 9.7这批机器如今已经在生产环境跑了一阵,每个参数的来龙去脉我都写进了团队的runbook,下次再遇到类似问题,不用重新从内核命令行开始查一遍。希望这套流程能帮你在自己的9.7优化路上少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询