从一次凌晨三点的 P0 故障说起
我到现在都记得那个电话。
凌晨三点十七分,枕边的手机像抽风一样震个不停。监控群里红色的告警已经刷屏,订单服务大面积超时,上游的调用方开始连环投诉。我眯着眼爬起来,手指在键盘上敲下 top,CPU 才用了 30%。内存也还好。那问题到底出在哪儿。
那是我职业生涯里最狼狈的一次排查。我在三台机器之间来回跳了快一个小时,最后发现是某个中间件把文件描述符耗尽,连接卡在 CLOSE_WAIT 上死活不释放。一个我「以为自己懂」的 TCP 状态,在真实流量面前把我按在地上摩擦。
从那之后我明白一件事。初级运维和高级运维的差距,不在会不会敲命令。在于你脑子里有没有一张完整的地图,出了问题你知道往哪儿走,而不是瞎蒙。
下面这 9 个必杀技,是我踩了无数坑之后,觉得一个 Linux 运维真正要「长在自己身上」的东西。不是命令清单,是思维方式。
一、性能排查:别再只会 top 了
top 是个好东西,但它只告诉你「现在很忙」,不告诉你「为什么忙」。真正的高级排查,是能顺着调用栈一路摸到内核。
第一步永远是看全局。top 之后看 vmstat 1,重点盯四个数:r(运行队列长度)、b(不可中断睡眠的进程数,磁盘 IO 卡顿就会涨)、si/so(swap 进出,一旦不为 0 系统基本在跪着跑)、cs(上下文切换,过高的话你大概率遇到了锁竞争或者过多线程)。
但 vmstat 只是序幕。定位 CPU 热点要上 perf。一句 perf top -p <pid> 能直接告诉你进程里哪个函数吃得最凶。再狠一点,perf record -g -p <pid> sleep 30 抓一份带调用栈的采样,用下面这条流水线生成火焰图:
perf script | stackcollapse-perf.pl | flamegraph.pl > out.svg
火焰图里横着越宽的函数,占的 CPU 越多。看一眼,瓶颈无所遁形。
很多人卡在 perf 用不出来的阶段,因为忘了内核要开 CONFIG_PERF_EVENTS,而且容器里抓宿主机的栈经常抓不到。我的经验是先在本机裸金属验证过整套流程,再去容器里排雷。
二、把 TCP/IP 协议栈调明白
上面那次 CLOSE_WAIT 事故,根子就在我对 TCP 状态机的理解停在教科书层面。生产环境的网络问题,十有八九绕不开这几个词:TIME_WAIT 堆积、端口耗尽、拥塞控制算法、缓冲区大小。
TIME_WAIT 是主动关闭方在发完最后一个 ACK 后进入的状态,默认要等 2*MSL(Linux 下 60 秒)才回收。高并发短连接的服务,比如 HTTP 代理,机器上很容易堆好几万 TIME_WAIT。缓解手段不是去关它,而是 net.ipv4.tcp_tw_reuse = 1 让新连接复用 TIME_WAIT 的端口(只对客户端 outbound 有效),配合 net.ipv4.tcp_fin_timeout 调小。千万别碰 tcp_tw_recycle,那个在 NAT 环境是著名的坑,早被社区删了。
端口耗尽又是另一个故事。一个服务能用的临时端口就 net.ipv4.ip_local_port_range 那一段,默认才几万。压测一上来端口不够,connect 直接报 Cannot assign requested address。这种问题 top 看一百年也看不出来,只能 ss -tan | wc -l 或者 cat /proc/net/sockstat 看 tcp 的 inuse 数。
内核参数这块,我建议每个运维都维护一份自己的 sysctl 调优模板,按业务类型分(Web 反向代理一套、数据库一套、消息队列一套),上线前 diff 一遍。别每次都临时 Google 然后忘了。
三、容器不是黑盒,cgroup 才是真相
很多人用 Docker 就是 docker run -d,OOM 了就加内存,CPU 飙了就加核。这跟当年只会重启没两样。
容器本质是 Namespace 做隔离、cgroup 做限制。一个容器被 OOM Kill,先别急着加内存。去看 dmesg | grep -i oom 或者 journalctl -u kubelet 里的 oomkill 记录,确认到底是哪个 cgroup 超了。cgroup v1 和 v2 的路径和统计方式不一样,现在主流发行版都在往 v2 迁,memory.events 里的 oom_kill 计数才是权威来源。
我见过最离谱的故障,是宿主机本身没满,但某个 cgroup 的 memory.max 设得太小,进程申请一点内存就被 cgroup 杀掉,表现为「服务莫名其妙重启」。这种问题在宿主机的 top 里干干净净,只有进到 cgroup 的视角才看得清。
CPU 限额也是个深坑。cpu.cfs_quota_us / cpu.cfs_period_us 算出来的限额,如果设成小于一个核,进程在调度器眼里就是「半个核」,多线程程序会大量等待。很多「CPU 明明没满却很慢」的谜题,答案都在这里。
四、可观测性:从「看见」到「看穿」
日志、指标、链路追踪,这三根柱子大家都背得出。但真正落地的时候,大多数人停留在「装了 Prometheus 和 Grafana 就以为完了」的阶段。
指标要分层次。RED(请求数、错误率、时延)适合业务接口,USE(利用率、饱和度、错误)适合资源。别把两个搞混,否则告警会又多又吵。我带团队时定过一条铁律:每个核心接口必须有 RED 三件套,每个核心资源必须有 USE 三件套,少一个不许上线。
链路追踪这块,eBPF 是近几年真正改变游戏规则的东西。传统 APM 要在代码里埋点,eBPF 直接从内核挂探针,不改动一行业务代码就能拿到系统调用、网络、文件 IO 的全景。像 Pixie、Cilium Hubble 这种基于 eBPF 的工具,能在你完全不知道问题在哪的时候,直接把慢请求对应的内核路径画出来。
说句实话,eBPF 入门门槛不低,要懂一点内核和 C。但它值得。当你能在不需要重启、不需要改代码的情况下,看清一个请求从网卡到应用再到磁盘的完整旅程,那种掌控感,是背一百条命令都换不来的。
五、存储与 IO:你以为的慢,往往是缓存的锅
「磁盘 IO 很高」是我听得最多的误判。新手看到 iostat 里 %util 100% 就慌,老手会先看 await 和 aqu-sz(队列长度)。%util 100% 只说明设备在忙,不代表它跟不上。NVMe 盘并发能力极强,%util 常年 100% 但 await 只有零点几毫秒,那是正常干活,不是故障。
真正的 IO 瓶颈信号是 await 持续升高、aqu-sz 堆积。这时候再用 iotop 看是哪个进程在狂写,blktrace + fio 做真实压测,别信嘴上的「应该不慢」。
文件系统选型也有讲究。数据库这种随机小 IO 密集的场景,XFS 通常比 ext4 稳,尤其是大文件和高并发删除。而 EXT4 在小文件、兼容性上更省心。挂载参数里 noatime 必加,能砍掉大量无谓的元数据写。IO 调度器,机械盘用 mq-deadline,SSD/NVMe 用 none(也就是 noop),这是常识但太多人没改过。
还有一个被严重低估的认知:大部分「慢」,是 page cache 没命中导致的读放大,或者 dirty page 回写策略把 IO 打满。/proc/sys/vm/dirty_ratio 和 dirty_background_ratio 这两个值,调好了能救活一个卡顿的数据库。
六、故障自愈与灰度:让系统自己站起来
高级运维的标志之一,是不再靠人肉半夜爬起来按按钮。systemd 的单元文件里,Restart=always 加上 StartLimitInterval 和 StartLimitBurst,能让一个崩溃的服务自动拉起并限制重启风暴。再配个 watchdog,进程假死也能被看护进程干掉重启。
但自愈的更高形态是「不把故障当故障」。滚动发布、蓝绿部署、金丝雀发布,目的是让一次有问题的上线,只影响 5% 的流量,而不是 100%。Kubernetes 的 readinessProbe 和 livenessProbe 就是干这个的,但 probe 配错了比不配还糟,比如 liveness 阈值太短,服务一 GC 就被杀,雪崩从此开始。
我个人的经验,自愈系统的第一原则是可观测。你都没法度量一个服务健不健康,谈什么自动恢复。先有指标,后有探针,最后才谈自愈策略。顺序反了,就是给自己埋雷。
七、安全加固:别等被拖库才想起最小权限
安全这事,平时没人夸你,一出事全公司追着你。我见过太多机器,默认 run 着 root 的 ssh,密码是 123456 或者干脆密钥没设密码。
底线几条,没商量:SSH 关掉密码登录改密钥、改掉默认 22 端口、root 直接登录禁止、fail2ban 挡暴力破解。更上一层,SELinux 别图省事设成 disabled,至少设成 permissive 观察一段时间再转 enforcing。很多运维怕 SELinux 是因为不熟悉它的报错,但 ausearch -m avc -ts recent 加 sealert 其实能把每个拒绝都讲清楚。
审计也重要。auditd 能把「谁、在什么时间、改了哪个文件」记下来。出了内鬼或者误删,这是唯一的真相来源。我建议至少审计 /etc、/usr/bin、/bin 这些关键路径的写操作。
入侵痕迹这种事,要学会看反常:crontab 里多了陌生任务、/tmp 下有不认识的二进制、网络连接连着陌生国家 IP、账号文件被改。这些不是等告警,是要主动巡检的。
八、内核崩溃不慌:kdump 和 vmcore
生产机器最吓人的不是慢,是「直接死了」,连日志都没留下。这时候 kdump 就是你的黑匣子。它用 kexec 在崩溃时切到一个预留内存里的小内核,把当前的崩溃现场(vmcore)dump 到磁盘。
kdump 要提前配。预留内存 crashkernel=xxxM,装好 kexec-tools,服务 enable 起来。等真的 panic 了,你拿到 vmcore,用 crash 工具加载对应的 vmlinux(注意调试符号版本要对应),bt 看崩溃时的调用栈,ps 看进程状态,files 看打开了什么。很多时候崩溃就因为某个内核模块有 bug,或者硬件内存出错,vmcore 一分析就知道。
[ 配图8 · kdump 流程图 ]
1080×720|正常内核 → panic → kexec 切换捕获内核 → dump vmcore 到磁盘 → 用 crash 工具分析,标出每步关键产出
应用层也一样。coredump 用 ulimit -c 或 systemd 的 LimitCORE 打开,程序崩了留个 core 文件,用 gdb bt 一下,段错误在哪个函数的哪一行,一目了然。我见过太多人程序崩了只甩一句「它挂了」,连 core 都没留,这种排查等于蒙眼走路。
九、自动化与 IaC:把经验写进代码
最后这条,是区分「运维」和「高级运维」的分水岭。你辛辛苦苦调好的 sysctl、写好的排查 SOP,如果只存在你脑子里,那公司就你一个能搞定。一旦你请假,系统就裸奔。
Ansible 适合批量配置和标准化,Playbook 把「这台机器该长什么样」写成声明。Terraform 适合管基础设施,云上的网络、机器、负载均衡,全用代码描述,版本库一提交,谁都能重建一套一模一样的环境。这叫基础设施即代码(IaC),核心价值不是方便,是可复现、可审计、可回滚。
我现在的习惯,所有环境变更走 PR。改个内核参数也要提代码、过 review、自动 apply。刚推行时被吐槽「效率慢」,半年后没人愿意回到那种「谁改了啥全靠记忆」的日子。
收尾:运维的尽头是方法论
写了这么多,其实我最想说的是最后一句。上面 9 个必杀技,单独拎出来都不难学。难的是把它们串成一套你自己的排查方法论。
我的方法论很简单:先分清是算力问题、IO 问题、网络问题还是逻辑问题,再决定往哪条线深挖,每深入一层都先度量再下结论,永远用数据说话而不是凭感觉。这套东西长得不性感,但它在凌晨三点的机房里,能让你比同事早半小时睡下。
技术会过时。perf 会出新版,eBPF 会有新玩法,K8s 年年变。但「系统地看问题、用证据下判断」这件事,是你穿在身上的铠甲,谁都拿不走。
聊到这儿,那 9 把刀我算是磨完了。剩下的,就看你愿不愿意在下次故障里,亲自把它们用一遍。