ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux服务器性能优化实战:从基线建立到瓶颈定位

Linux服务器性能优化实战:从基线建立到瓶颈定位

凌晨两点,监控大屏突然开始连环报警,某业务接口的响应时间从80毫秒一路飙到3秒。我爬起来登录服务器,uptime一看,负载从平时的2.5涨到了12,top里有一堆进程挤在D状态。那是我这几年做过的最痛苦的一次性能排查,也是后来让我养成了“先打基线再动手调优”这个习惯的开始。

这篇文章想聊的就是Linux服务器性能优化这件事。不是让你背参数列表,也不是丢给你一百个内核调优项,而是把我自己压测、排障、调优时的完整思路和判断逻辑讲清楚——怎么快速定位瓶颈在CPU、内存、磁盘还是网络,怎么改配置才算有效,以及哪些网上流传很广的操作其实是给自己埋雷。适合刚接手服务器运维的工程师,也适合那些“top命令会看,但不知道下一步该干嘛”的朋友。

1. 先摸清底牌:没有基线的优化都是耍流氓

很多人拿到一台服务器就开始调内核参数,这跟没看病历就开药没什么区别。性能优化第一步不是改配置,而是先搞清这台机器正常时候长什么样,异常的时候又长什么样。

1.1 三分钟快速体检:从登录开始做些什么

我登录服务器后习惯先跑一组基础命令,不看别的,就看这五个数:

uptime top vmstat 1 5 free -m df -h

uptime给出1分钟、5分钟、15分钟的负载均值。这里有个基本判断逻辑:如果1分钟负载明显高于15分钟,说明系统正承受短时压力;如果三个数都居高不下,说明问题已经持续很久了。单看负载还不够,还要配合top里的CPU百分比看这个负载是由CPU繁忙还是进程阻塞带来的。

vmstat 1 5输出比较有讲究。重点看r(运行队列)、b(阻塞进程数)、si/so(换入换出)、cs(上下文切换)和wa(I/O等待)。如果b列长期大于0,说明有进程卡在I/O或者锁上;如果cs高到几万甚至十几万,说明系统在上下文切换上烧掉大量CPU。看完这步,基本能判断问题的大方向。

free -m看内存时我重点关注available这一列,而不是free。Linux会把空闲内存拿去做page cache,这是一种正常使用,不是内存泄漏。

1.2 把“峰值”变成基线:建立长期监控视角

临时体检只能发现问题,解决不了“这算不算异常”的争议。后来我要求团队在每台服务器上统一部署sysstat,让sar定时采集历史数据:

# Debian/Ubuntu apt install sysstat # RHEL/CentOS yum install sysstat

装好后确认/etc/cron.d/sysstat里的采集周期开着,一般10分钟一次足够。这样做的价值在于:线上出问题时,可以直接用sar -q看当时的负载、用sar -r看当时的内存、用sar -d看当时的磁盘I/O,而不需要问业务“当时到底发生了什么”。

这个习惯帮我们排查过好几次诡异故障。有一次某服务高峰期必抖,开发怀疑数据库慢查询,结果sar历史数据显示数据库服务器CPU和磁盘都很稳,反倒是应用服务器的plr(运行队列长度)在P99时段明显上涨,最终定位到了应用线程池配置问题。没有历史数据,这种跨天对比做不出来。

给个建议:每台机器至少保留30天历史数据。如果你的机器超过100台,还是把数据集中起来比较好,不然单机查起来也很痛苦。

2. CPU优化:用户态与系统态的区分,才是最值钱的判断

CPU问题是最常见的性能瓶颈,也是最容易被误解的指标。很多人一看到top里CPU 100%就喊“CPU爆了”,但100%可能发生在us(用户态),也可能发生在sy(系统态),这两个的调优方向完全不一样。

2.1 用mpstat和pidstat把问题定位到具体核、具体进程

top只是全局视角,进一步就得细分。我常用的两个工具是mpstat和pidstat:

mpstat -P ALL 1 pidstat -u 1 5

mpstat -P ALL 1能告诉你每个CPU核的使用情况,如果只有个别核跑到100%,而其他核很闲,多半是单线程热点、中断绑核不均或者锁竞争。如果所有核一起满载,那就要考虑整体算力不足或者发生了死循环。

pidstat -u 1 5可以看到具体进程的CPU消耗。注意看%usr和%system两个子项:%usr高说明是业务代码在算,%system高说明进程在内核态消耗CPU,比如频繁系统调用、收发网络包、读写文件。我曾经遇到过一个日志框架,在高IOPS下每秒产生上万次write系统调用,%sy直接飙到40%,整个服务的吞吐被拖垮,换了一个异步日志组件后%sy降到5%以下。

2.2 上下文切换高,系统态一定不低

vmstat里的cs列如果持续超过5万,这里面一定有可以优化的东西。上下文切换分两类:线程切换和中断处理。线程切换需要加锁、切栈、刷新cache,成本很高。

最常见的上下文切换杀手有两个:一是线程池开得太大,几百个线程在锁上互相等待;二是业务代码里的自旋锁或者频繁的sleep(0)之类的写法。

排查方法:

pidstat -w 1 5

这一步会显示每个进程的cswch/s(自愿切换)和nvcswch/s(非自愿切换)。nvcswch/s猛增,基本可以断定线程数量多且竞争激烈。我遇到过一个案例,某服务的线程池配置了400个线程,实际上并发只有几十,结果大量线程在锁上排队。把核心线程数压到和CPU核数相当之后,吞吐量反而翻了一倍。线程不是越多越好,这是CPU优化里最容易踩的坑。

2.3 用户态CPU高:别急着加机器,先找热点函数

us高但整体负载还没爆的时候,别急着扩容。用perf top看内核里的热点函数,能帮你判断代码到底在忙什么。

perf top

如果你的进程是Java的,优先用async-profiler这类工具采集火焰图;如果是C/C++,直接用perf record/report,定位到具体函数再决定怎么改。有一次我们压测某同步接口,perf显示热点在一个字符串序列化函数上,改成更高效的序列化库后CPU消耗降了40%。这种优化比盲目加机器有用得多。

调整进程优先级也有应用场景:对延迟敏感但有备份性质的服务,比如日志采集、离线计算任务,可以用renice降低它的CPU优先级,保证主业务在资源紧张时优先获得调度:

renice -n 10 -p <pid>

但优先级只影响调度顺序,不影响配额。如果你希望硬性限制某个进程最多只能用2颗核,配合systemd配置更稳妥:

[Service] CPUQuota=200%

CPUQuota=200%代表最多使用2个完整CPU核心。这个在混部场景下很好用,能防止某个失控进程把整机CPU打满。

2.4 软中断与RPS:一个经常被忽略的方向

top里如果si(软中断)长期偏高,比如超过10%,就要关注是哪个中断在消耗CPU。可以看:

cat /proc/interrupts

网卡收发数据包会产生大量中断,如果IRQ都绑定在同一个核上,那个核就会被打满。现代网卡一般支持多队列,配合irqbalance或者手动把IRQ分散到多个核,能明显改善多核利用率。如果网卡不支持多队列,还可以用内核的RPS(Receive Packet Steering)机制,把收包软中断均匀分发到各个CPU:

echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

这个f是把第0到3号CPU都放进接收队列(二进制1111)。具体掩码要根据你核数来算,别照搬。改完后用mpstat -P ALL 1重新观察,很直观。

3. 内存与Swap:别一看到缓存高就想去“清理”

内存优化是误操作重灾区,尤其那些到处流传的“清理缓存”命令。先理解清楚,Linux的内存管理不是你想象的“空闲内存越多越好”。

3.1 正确读内存:available才是真正可用内存

先解释一个概念,top里的RES(常驻内存)是进程实际占用的物理内存,VIRT(虚拟内存)是进程映射的虚拟地址空间,通常远大于RES。真正决定一个进程能否启动的,是系统还有多少available内存。

Linux尽可能把空闲物理内存用来做page cache,磁盘读过的文件都缓存在里面,下次命中就直接从内存读,开销小得多。所以free显示free只有几百MB、但buff/cache很大时,这是正常状态,不要慌张。

我见过有同事一看free不足就开始“清理缓存”,这其实是在损害性能。page cache是性能加速器,不是负担。等到真有内存压力时,内核会自动回收cache来满足新分配。只有一种场景需要手动drop_caches,就是做了某些基准测试,需要把页缓存清空以测量冷启动性能:

echo 3 > /proc/sys/vm/drop_caches

注意生产环境没事别这么干。

3.2 Swap不是洪水猛兽:关键是别让正常进程被换出

Swap用了一点不代表内存就不够了。真正的问题是:你的活跃进程是不是被换到Swap里去了。如果swappiness设得太大(默认60),内核在内存还有富余时也可能倾向于把冷页换出。对于数据库、缓存类应用,建议把vm.swappiness调低:

sysctl vm.swappiness=10

这个值越低,越偏向于保留匿名内存,但不要直接改成0,因为在极端内存紧张时,Swap可以挽救整个系统免于直接OOM。

检查有没有进程被换出,可以不停vmstat看si/so列。如果si和so经常大于0,说明系统一直在做换入换出,这种抖动非常伤性能。这种情况下杀掉一些不用的进程或增加内存,才是根本解法。

3.3 OOM Killer的判定与规避

当内存耗尽,内核OOM Killer会按oom_score选一个进程杀掉。问题来了:很多时候OOM杀掉的不是内存最大的进程,而是分数最高的,分数受进程优先级、运行模式影响。

遇到OOM先看证据:

dmesg -T | grep -i oom

这里能看到是哪类进程触发了OOM、当时的分配请求是多少。如果单体进程内存本身就超过物理内存,说明是要扩容;如果是由于另一个进程把内存吃光导致的无差别误杀,那就要先处理“元凶”。给Java应用配置-Xmx时我习惯留出30%的余地给堆外内存和线程栈,很多OOM其实是堆外内存爆炸,单纯调-Xmx没用。

还有一个操作很多人忽略:为关键进程设置systemd的内存上限:

[Service] MemoryMax=4G

这样即使某个进程失控,系统也能把它限制在配额内,不会拖垮整机。

3.4 内存问题排查方向:从RSS和线程数入手

如果观察到一个进程内存持续上涨,先分清是物理内存增长还是虚拟内存增长。ps aux里的RSS列是物理内存,VSZ是虚拟内存。线程栈引起的虚拟内存增长一般无害,物理内存增长才是问题。

排查物理内存增长可以从三处入手:是否有线程创建过多(每线程默认栈大小就是8MB的虚拟地址空间)、是否有堆外缓存、是否有内存池不在GC范围内。Java应用出现RSS比-Xmx明显大时,多半是堆外或DirectByteBuffer,这种情况换一个更大堆的机器或调小堆外容量才是正途。

我遇到过一个比较隐蔽的内存泄漏:某个基础库的缓存Map只进不出,也没人去查,内存慢慢从2G涨到7G再OOM。最后是靠监控RSS趋势发现的。所以强调一下:内存问题必须要有趋势监控,不能只看一时快照。

4. 磁盘I/O:iostat是个好工具,但别用错指标

磁盘I/O出问题时的表现很典型:top里的wa很高,很多进程处于D状态,接口响应时间明显变慢,但CPU使用率其实不高。这种“CPU闲着、系统却在等待”的状态,多半是I/O瓶颈。

4.1 iostat的正确读法

先跑iostat -x 1,输出里有几个关键列:

指标含义判读经验
%util磁盘忙碌时间占比对机械盘接近100%很危险;对SSD并行能力强,100%也不代表一定慢
await平均I/O响应时间,含排队和实际服务时间超过20ms一般就有感知;SSD应在1ms附近
svctmI/O设备服务时间排队时间长而svctm短,说明是请求太多
avgqu-sz平均队列长度持续大于设备并发能力的2倍,已经过压
r/s, w/s每秒读写次数结合块大小能算出真实吞吐

真正要盯的是await和avgqu-sz,不在于%util是不是100%。有次我们压测一个基于固态硬盘的数据库,%util长期95%以上,但await只有2ms,请求延迟体验完全没有劣化。当时如果只看%util就会误判为磁盘瓶颈。

如果await高而svctm很低,说明瓶颈在请求堆积而不是磁盘硬件本身。这时优化方向应该是减少I/O次数:加缓存、合并写请求、减少日志同步频率。

4.2 挂载参数与文件系统策略

文件系统层面的优化很多人容易忽略。默认挂载选项一般是relatime,每次读文件会更新访问时间,对高IOPS场景有一定开销,可以改成noatime:

# /etc/fstab /dev/sdb1 /data ext4 defaults,noatime 0 2

日志模式也值得注意。Ext4默认data=ordered保证元数据和数据的一致性,但对写性能敏感的业务可以评估是否使用更激进的方式,不过这个要确认业务能否容忍部分异常情况下数据丢失,别盲目改。

4.3 脏页回写与I/O调度器

内核写数据会先落到page cache,再由后台线程刷到磁盘。两个参数控制回写节奏:

sysctl vm.dirty_ratio=20 sysctl vm.dirty_background_ratio=10

dirty_background_ratio是后台开始回写的阈值,dirty_ratio是同步阻塞写的阈值。跑批量写任务时如果看到top里偶尔卡一下、随后wa飙高,往往是dirty_ratio太小、大量脏页堆积后触发同步刷盘造成的。可以适当调低dirty_background_ratio让回写更平缓,避免突刺。

I/O调度器在机械盘和SSD上的选择策略也不一样。机械盘推荐mq-deadline或bfq,SSD/NVMe可以直接用none(即noop,不做调度),减少无意义合并开销。查看和切换:

cat /sys/block/sda/queue/scheduler echo none > /sys/block/sda/queue/scheduler

生产环境修改请写入udev规则持久化,不要只改运行时。

4.4 用fio做一次规范的磁盘压测

调优前先知道磁盘本身的性能上限。fio是最常用的御三家工具之一,一套典型配置:

fio --name=write_test --rw=write --bs=4k --size=1G --numjobs=4 --runtime=60 --ioengine=libaio --iodepth=32 --direct=1

direct=1是为了绕过page cache,直接测试真实磁盘能力。分别测4k随机读、4k随机写、128k顺序读、128k顺序写,得到的IOPS和带宽就是这台机器的硬指标。后面的所有优化效果,都要和这个基线对比。

我之前测过一台云主机,4k随机读只有8000 IOPS,换一块云盘后到5万。业务没改一行代码,延迟降了几个数量级——硬件选型有时候才是最大的优化空间。

5. 网络与内核参数:改动之前先读懂这几个

很多网上的“性能优化命令大全”喜欢一次性丢几十个sysctl参数,里面有一半根本不适合你的场景,甚至有的还埋着坑。网络相关的内核参数其实是把双刃剑。

5.1 TIME_WAIT多,先别急着开tcp_tw_reuse

连接数高的服务(比如Nginx、网关)经常出现大量TIME_WAIT连接。看到这个,很多教程让你开net.ipv4.tcp_tw_reuse=1,但这需要理解前提。

tcp_tw_reuse只对主动关闭方有效,且主要解决的是“主动连接方发起新连接时重用TIME_WAIT”,不是万能钥匙。更糟糕的是tcp_tw_recycle,在高版本内核已经移除,老版本开启后很容易造成NAT环境下连接被随机丢弃,网上很多“优化后服务间歇性不可用”的案例就是它干的。我的建议是:tcp_tw_recycle坚决不碰,tcp_tw_reuse在明确是客户端主动发大量短连接的时候再考虑。

更多时候,TIME_WAIT多并不是性能问题,而是连接关闭策略问题。优化方向应该是:让服务端合理开启keepalive复用连接,减少短连接频次。看连接状态:

ss -s

这个命令会汇总各状态连接数,比netstat更高效。

5.2 队列长度与握手保护

瞬时高并发时,如果ss -lnt看到Send-Q积压,或者客户端频繁出现连接超时,需要看两个参数:

sysctl net.core.somaxconn=4096 sysctl net.ipv4.tcp_max_syn_backlog=8192

somaxconn是每个监听socket的accept队列上限,像Nginx这种高并发服务必须调大;tcp_max_syn_backlog是半连接队列上限,承接SYN洪水时的缓冲。调完记得在Nginx配置里也把listen的backlog同步调大,两边不管哪边不够都白搭:

listen 80 backlog=4096;

5.3 文件描述符上限,最容易被“连接数上不去”卡住

网络连接数上去的前提是文件描述符够用。很多人只调了ulimit -n,但系统级的fs.file-max没改,照样受限。

查看当前打开文件数上限:

cat /proc/sys/fs/file-max ulimit -n

进程级临时调大:

ulimit -n 65535

持久化写法:编辑/etc/security/limits.conf,加一行:

* soft nofile 65535 * hard nofile 65535

如果用的是systemd管理的服务,还要在service文件里加LimitNOFILE=65535,否则limits.conf对systemd服务不生效。这个坑我踩过两次,调了半天防火墙,结果就是fd不够。

5.4 软中断多核分散与网卡队列

第2章提过RPS,这里补充一下:高性能网关类服务通常建议开启网卡多队列。用ethtool -L查看当前队列数:

ethtool -l eth0

如果Combined只有1,而这个网卡硬件支持多队列,可以调大:

ethtool -L eth0 combined 4

配合RPS或者硬件RSS,可以把网络收包中断分散到多核。实测中,单队列网卡高流量时某个核si飙到50%甚至更高,开启多队列后全局CPU使用率反而下降,因为不再有一个核在疯狂处理中断。

6. 一次典型性能事故的排障复盘:从报警到定位再到落地方案

纸上谈兵再多,不如完整复盘一次真实的排查过程。这个案例我改写了细节,但排查链路是原汁原味的,希望能帮你建立“判断顺序”。

6.1 现象:高峰期接口响应变慢,CPU却不高

某内部系统的网关服务,每天下午3点开始,接口P99延迟从50ms涨到800ms。数据库CPU正常,应用服务器CPU也不高,但用户已经明显感知卡顿。一开始团队怀疑是数据库慢查询,加索引后没有改善,才转到我这边。

接手后我先跑了top,确认了CPU不高、load却比平时高出一倍。紧接着vmstat 1 5,发现b列有大量进程处于阻塞状态,同时wa到了30%。直觉判断是I/O或者锁。先用iostat -x 1看磁盘,%util居然很低,await也正常。

这说明不是物理磁盘的问题。那是什么?进程被什么阻塞了?

6.2 排查链路:文件锁、网络、还是线程池耗尽

我用pidstat -d 1查看进程I/O,发现有个进程的kB_wr/s并不高,但它持续处于D状态。D状态代表不可中断睡眠,要么是I/O等待,要么是内核内等待。

再进一步查看/proc/<pid>/stack和/proc/<pid>/status里的voluntary_ctxt_switches,发现切换数极速增长。我意识到这不是物理I/O,而更像是某种锁等待。切换到sar -n DEV 1看网络流量,发现这个网关的收包速率并不高,但/proc/net/softnet_stat里的dropped字段在持续增长。

这一下就通了:软中断处理不过来,网络包在协议栈排队,进程等待数据包,表现就是D状态。根因是网卡单队列,所有收包软中断都压在一个CPU核上。

6.3 修复与验证:一次改动,立竿见影

确认多队列支持后,执行:

ethtool -l eth0 ethtool -L eth0 combined 4

同时配合RPS设置,把收包中断分散到4个核,然后再看mpstat -P ALL 1,软中断从单核70%变成了4个核均匀分摊。接口P99延迟回落到60ms左右,全程没有改一行业务代码。

后面的复盘我们做了一件事:把所有网关类服务器都加了网卡队列和RPS的持久化配置,并在监控里加上了软中断占比的看板,防止单核软中断再次成为隐形瓶颈。这个案例让我深刻体会到,性能排查的路径比技巧重要:top定位方向、vmstat判断状态、iostat排除硬件、最后落到协议栈和中断。

7. 这些“优化操作”我劝你别在线上乱试

最后写点反面的东西。有些命令和参数在社区里传得很广,看着很“高级”,实际是定时炸弹。我把这些年见过的、踩过的坑列出来,每条都是真实教训。

常见操作为什么有风险正确姿势
echo 3 > /proc/sys/vm/drop_caches清掉page cache会拖慢所有读请求,生产上尤其危险除非做冷启动基准测试,否则不要在生产环境主动清cache
把vm.swappiness改成0内存紧张时完全失去Swap缓冲,更容易触发OOM Killer调成10左右,给极端情况留条命
开启tcp_tw_recycleNAT环境下大量丢包,服务间歇性不可用不要用;优先从连接复用角度解决TIME_WAIT
调低dirty_ratio却不测写吞吐频繁触发同步刷盘,写性能反而下降用fio压测后再动,改完观察wa是否平稳
盲目加线程池线程数上下文切换暴涨,吞吐不升反降原则:线程数≈CPU核数×(1+等待时间/计算时间),算清楚再改
修改内核参数不持久化重启后配置丢失,还可能导致系统不一致统一写到/etc/sysctl.conf,执行sysctl -p生效并验证
调优完不做压测对比改完不知道效果,甚至不知道有没有副作用每项改动前后各跑一轮压测,记录数据再决定去留
单改ulimit -n不查file-max系统级文件描述符不足时,改进程上限形同虚设两个都查、两个都调,系统服务还要注意LimitNOFILE

还有几个操作层面的提醒:

  1. 每次改动只改一个参数,改完验证一轮再改下一个。同时改十个参数,如果出问题,你根本不知道是哪个引起的。
  2. 所有参数改动前先备份当前配置,sysctl -a > /tmp/sysctl.bak这个命令花不了几秒钟,关键时刻能救命。
  3. 调优不是找最大值,而是找最平滑的区间。比如线程池和队列长度,理想状态是系统承受压力时延迟缓慢上升,而不是像断崖一样突然不可用。

我发现很多性能问题最终不是某个参数不对,而是监控缺失带来的“事后被动”。如果你有条件,优先把基础的CPU、内存、磁盘、网络四项指标接入监控,再配合systat历史数据,做到有据可查。有了这些底子,任何调优改动都能量化验证,你才能从一个“照抄命令的人”变成真正会做性能优化的人。

最后分享一个我个人的小习惯:每台服务器在调优完成后,我会把改动清单、原因、压测结果写成一个简单的README放在/etc/sysctl.d/同级目录下。几个月后回头看,这个文档比任何命令都值钱——它记录的不是参数,而是当时为什么做了这个决定。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进