你的服务器做过体检么?一份真实的 CentOS 高并发服务器体检与优化实录
服务器跑起来之后,你多久会去看一次它?
很多人的答案可能是:只要服务没挂,我就不看。
我以前也差不多。CPU 没爆、内存没 OOM、磁盘还有空间、接口能正常访问,就觉得服务器状态应该没什么问题。
直到最近对几台线上 CentOS 服务器做了一次比较完整的"体检",我才发现一个很容易被忽略的问题:
服务器现在正常,不代表它在业务高峰期也正常。 
有一台服务器检查时 CPU Load 只有 0.01 左右,TCP 状态看起来也很平稳,但继续往底层翻统计数据,却发现系统历史上已经出现了接近 30 万次 SYN 丢弃,内核甚至明确记录过:
text
TCP: request_sock_TCP: Possible SYN flooding on port 80.
Sending cookies. Check SNMP counters.
也就是说,服务器表面风平浪静,底层其实早就"咳嗽"过很多次了。
这篇文章就拿这次真实排查过程,聊聊一台 Linux 服务器到底应该怎么体检,以及发现问题之后哪些参数值得改,哪些参数不要跟着网上所谓的"百万并发模板"乱调。
一、服务器体检,到底应该检查什么?
平时大家登录服务器,最常执行的大概就是:
bash
top
free -h
df -h
这三个命令当然重要,但如果服务器承担 Web API、Nginx、Java、PHP、Redis、MySQL 等服务,仅看这些远远不够。
我这次把检查内容大致分成了六类:
- 文件句柄和进程线程限制
- TCP 内核参数
- 当前 TCP 连接状态
- 网卡和网络流量
- CPU、内存、进程资源
- TCP 丢包、Overflow、Reset、OOM 等历史异常
真正有意思的往往是最后一项。
因为 CPU 和内存告诉你的更多是:
服务器现在怎么样。
而 netstat -s、dmesg 这些数据告诉你的则可能是:
服务器过去到底发生过什么。
这两件事差别很大。
二、第一眼看,这台服务器甚至非常"健康"
这台机器配置不算特别高:
text
CentOS 7
Kernel 3.10
内存:7.4G
Swap:0
检查时 CPU Load:
text
load average: 0.01, 0.04, 0.08
这是什么概念?
基本没什么 CPU 压力。
内存情况:
text
total 7.4G
used 5.5G
available 1.6G
虽然内存余量不算特别宽裕,但也没有发生 OOM。
文件句柄:
text
ulimit -n = 65535
fs.file-max = 759020
file-nr = 3232
系统最多允许 75 万左右文件句柄,实际才用了三千多个,这里同样没有瓶颈。
如果检查到这里就结束,很容易得出一个结论:
服务器挺健康,不需要优化。
但继续往 TCP 层看,情况就完全不一样了。
三、真正的问题藏在 TCP 历史统计里
当时这台服务器实时 TCP 状态大概是:
text
TCP: 1076
ESTABLISHED: 1046
TIME_WAIT: 8
CLOSE_WAIT: 7
一千多个 ESTABLISHED 对线上服务器并不是什么特别离谱的数据。
TIME_WAIT 只有个位数,也完全正常。
但继续执行:
bash
netstat -s
然后筛选 TCP 异常:
bash
netstat -s | grep -iE "overflow|drop|reset|listen"
结果就比较有意思了:
text
743705 connection resets received
4984066 resets sent
367106 resets received for embryonic SYN_RECV sockets
299729 SYNs to LISTEN sockets dropped
709862 connections reset due to early user close
TCPBacklogDrop: 2
其中最值得关注的是:
text
299729 SYNs to LISTEN sockets dropped
接近 30 万次。
再看 dmesg,直接找到了:
text
TCP: request_sock_TCP:
Possible SYN flooding on port 80.
Sending cookies.
Check SNMP counters.
这下问题就很明确了。
这台服务器虽然检查的时候很闲,但历史业务高峰期间,80 端口的新 TCP 连接曾经给服务器造成过明显压力。
这就是为什么我现在越来越觉得:
服务器体检不能只看实时 CPU 和内存。
很多问题都是瞬时发生的。你晚上八点检查服务器可能一切正常,但下午两点业务高峰的时候,TCP 队列可能已经爆过一次了。
四、继续往下查,问题就找到根了
检查服务器 TCP 参数:
bash
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.netdev_max_backlog
结果:
text
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 1024
net.core.netdev_max_backlog = 1000
这些参数简单理解:
tcp_max_syn_backlog
客户端访问:
text
客户端
↓ SYN
服务器
↓
SYN_RECV
↓ ACK
ESTABLISHED
三次握手还没有完成的时候,连接需要暂存在 SYN 队列。
这个队列太小,而瞬间又来了大量新连接,就可能出现:
text
SYNs to LISTEN sockets dropped
这次服务器正好已经留下了大量历史记录。
somaxconn
somaxconn 控制的是应用 listen() 队列能够使用的系统级上限。
服务器原来:
text
somaxconn = 1024
对普通小网站其实不一定有问题。
但这台服务器同时运行:
text
Nginx
PHP-FPM
Java
Redis
MySQL
并且历史上已经明确发生过 SYN drop,那么继续保持 1024 就没有什么必要了。
最终我把这几个参数调整为:
text
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.core.netdev_max_backlog = 8192
这里我没有照着网上一些文章直接搞:
text
65535
262144
500000
原因很简单:
系统优化不是数字越大越好。
从 1024 调整到 8192,是因为已经有真实数据证明队列曾经承受过压力。
如果服务器一天只有几百个访问,把参数调到几十万并不会让接口突然快十倍。
五、TIME_WAIT 参数也发现了一个坑
服务器原来的配置:
text
tcp_max_tw_buckets = 5000
tcp_fin_timeout = 60
tcp_tw_reuse = 0
tcp_max_tw_buckets=5000 对这种线上混合服务器明显偏保守。
于是调整为:
text
net.ipv4.tcp_max_tw_buckets = 65536
net.ipv4.tcp_fin_timeout = 30
但是有一个参数我没有动:
text
tcp_tw_reuse = 0
原因是这台服务器运行的是:
text
CentOS 7
Linux Kernel 3.10
而另外一台新服务器运行的是 Linux 5.14,tcp_tw_reuse 在不同年代内核中的行为和可用值不能简单照搬。
尤其是网上经常能看到这种"祖传 Linux 优化配置":
text
tcp_tw_reuse = 1
tcp_tw_recycle = 1
看到这种配置最好先停一下。
特别是 tcp_tw_recycle,在 NAT、负载均衡等环境下曾经带来过很多莫名其妙的连接问题,后来的 Linux 内核也已经将其移除。
所以我的原则一直是:
不知道一个参数为什么要改,就先不要改。
六、临时端口范围也值得注意
服务器原来的:
text
ip_local_port_range = 32768 60999
大约只有 2.8 万个临时端口。
很多人认为 Web 服务器只负责"接收连接",其实现在的业务服务器往往同时也是客户端。
例如:
text
用户
↓
Nginx
↓
Java
↓
Redis / MySQL / 其他微服务 / 第三方API
Java 调第三方 HTTP API、PHP 请求内部服务、Nginx 反向代理后端,本质上都可能产生主动 TCP 连接。
因此最终调整:
text
net.ipv4.ip_local_port_range = 10000 65535
把可使用范围扩大到 5 万多个。
当然,真正解决大量短连接问题,优先级更高的依然应该是:
text
HTTP KeepAlive
数据库连接池
Redis连接池
RPC长连接
连接复用
而不是只想着靠 Linux 参数兜底。
七、KeepAlive 默认 7200 秒,也有点太佛系了
原来的参数:
text
tcp_keepalive_time = 7200
tcp_keepalive_intvl = 75
tcp_keepalive_probes = 9
简单理解就是:
一个 TCP 连接长时间没有数据,Linux 默认要等很久才开始主动探测它是不是已经失效。
这台服务器长期存在一千多个 ESTABLISHED,所以我最终调整为:
text
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
也就是更加积极地清理真正失效的长连接。
但这里同样要提醒:
Linux KeepAlive 不是万能药。
如果你发现:
text
CLOSE_WAIT
持续几百、几千地上涨,那么更应该检查应用程序有没有正确关闭 socket,而不是继续修改 sysctl。
这次服务器 CLOSE_WAIT 只有 7 个,所以暂时没有这个问题。
八、Socket Buffer 也顺手调整了
服务器默认:
text
net.core.rmem_max = 212992
net.core.wmem_max = 212992
也就是大约 208KB。
最终调整:
text
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 16384 16777216
最大提高到 16MB。
这里很容易产生一个误区:
1000 个 TCP × 16MB,那不是直接吃掉 16GB 内存?
并不是。
这里更多是给 TCP 自动调节提供一个更高的上限,不意味着每建立一个连接就立即分配 16MB。
九、最有意思的问题:参数明明改了,为什么还是 1024?
配置写完后,我执行:
bash
sysctl --system
明明看到:
text
net.core.somaxconn = 8192
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_max_tw_buckets = 65536
但重新检查:
bash
sysctl net.core.somaxconn
居然还是:
text
1024
一开始很容易怀疑:
是不是 CentOS 7 内核不支持?
是不是参数超过上限?
是不是需要重启?
后来仔细看 sysctl --system 的输出才发现,真正的问题非常简单:
配置被后面的文件覆盖了。
执行顺序里先加载:
text
/etc/sysctl.d/99-high-concurrency.conf
设置:
text
somaxconn = 8192
随后又加载:
text
/etc/sysctl.d/99-sysctl.conf
重新变成:
text
somaxconn = 1024
最后又加载:
text
/etc/sysctl.conf
于是最终结果还是 1024。
继续检查:
bash
ls -l /etc/sysctl.d/99-sysctl.conf
发现:
text
/etc/sysctl.d/99-sysctl.conf -> ../sysctl.conf
原来 99-sysctl.conf 本身就是指向 /etc/sysctl.conf 的软链接。
再查:
bash
grep -nE 'somaxconn|tcp_max_syn_backlog|tcp_max_tw_buckets|swappiness' \
/etc/sysctl.conf \
/etc/sysctl.d/*.conf
马上看出来 /etc/sysctl.conf 里面同时存在:
text
somaxconn = 1024
和:
text
somaxconn = 8192
两套参数。
最终把旧配置清理掉,只保留新的值,再重新加载:
bash
sysctl -p
问题解决。
这个小插曲其实特别有代表性。
Linux 调优最怕的不是不会改,而是你以为自己改了。
任何 sysctl 修改完成之后,都应该重新执行:
bash
sysctl net.core.somaxconn
确认当前内核真正使用的值。
不要只看到配置文件写成功就认为结束了。
十、最终优化后的服务器是什么状态?
再次运行体检脚本,核心参数已经变成:
text
net.core.somaxconn = 8192
tcp_max_syn_backlog = 8192
net.core.netdev_max_backlog = 8192
tcp_fin_timeout = 30
tcp_max_tw_buckets = 65536
tcp_keepalive_time = 600
tcp_keepalive_intvl = 30
tcp_keepalive_probes = 5
rmem_max = 16777216
wmem_max = 16777216
ip_local_port_range = 10000 65535
kernel.pid_max = 262144
更值得关注的是优化前:
text
299729 SYNs to LISTEN sockets dropped
优化之后再次检查:
text
299729 SYNs to LISTEN sockets dropped
没有继续增长。
当然,这并不能仅凭十几分钟就宣布问题彻底解决,因为 netstat -s 统计的是系统启动以来的累计数据。
正确观察方法应该是:
bash
date
netstat -s | grep -iE "SYNs to LISTEN|listen queue|TCPBacklogDrop|reset"
记录下来。
等业务高峰过去,再执行一次。
如果:
text
299729
↓
299729
说明这段时间没有新增。
如果:
text
299729
↓
310000
那说明仍然存在问题。
这时候就不能继续无脑:
text
8192 → 16384 → 65535
而应该继续向应用层排查。
十一、Linux 调完以后,别忘了 Nginx
这次检查还有一个很典型的问题。
Linux 已经:
text
somaxconn = 8192
但:
bash
ss -lntp
看到 Nginx:
text
LISTEN 0 511 *:80
LISTEN 0 511 *:443
也就是说操作系统允许 8192,但 Nginx 实际监听 backlog 还是 511。
继续检查:
bash
nginx -T | grep -E "worker_processes|worker_connections|multi_accept"
发现:
text
worker_processes auto;
worker_connections 51200;
multi_accept on;
Nginx worker 本身其实已经配置得不错。
下一步真正值得关注的是:
nginx
listen 80 backlog=4096;
listen 443 ssl backlog=4096;
但这类配置不要直接用 sed 批量替换。
因为一台服务器往往存在多个虚拟主机,同时还有:
text
listen 80;
listen [::]:80;
listen 443 ssl;
多个监听配置。
应该先:
bash
nginx -T
找到真正的配置文件,再针对性调整。
否则性能没优化多少,反而先把 Nginx 配挂了。
十二、Java、PHP、Redis、MySQL同样存在自己的"队列"
这也是这次体检之后最大的一个感受:
高并发优化从来不是一个 sysctl.conf 就能解决。
比如这台机器的 Java:
text
8080 LISTEN backlog=1000
18080 LISTEN backlog=100
8080 已经比较合理。
18080 如果是一个高流量 Spring Boot 服务,就应该继续检查:
properties
server.tomcat.accept-count=1000
server.tomcat.threads.max=300
server.tomcat.threads.min-spare=20
但是线程也不是越多越好。
假如:
text
Tomcat线程 = 1000
数据库连接池 = 30
大量线程最终还是堵在数据库连接池前面。
真正合理的链路应该是整体匹配:
text
Linux TCP
↓
Nginx
↓
Tomcat
↓
业务线程
↓
数据库连接池
↓
MySQL
其中任何一层特别小,都可能成为整个系统的瓶颈。
十三、这次还顺便发现 Redis 监听所有网卡
体检结果:
text
*:6379
redis-server
这意味着 Redis 监听所有 IPv4 地址。
它不一定真的暴露公网,因为外面可能还有安全组、防火墙。
但至少值得继续检查:
bash
redis-cli CONFIG GET protected-mode
redis-cli CONFIG GET requirepass
如果 Redis 只给本机程序使用,完全可以考虑:
text
bind 127.0.0.1
protected-mode yes
有时候服务器体检的价值就在这里。
本来是来查性能的,顺手把安全隐患也翻出来了。
十四、还有一个我最终没有忽略的问题:Swap
这台服务器:
text
总内存:7.4G
已使用:5.5G
available:1.6G
Swap:0
而机器上同时跑:
text
Nginx
PHP-FPM
Redis
MySQL
Java
宝塔
CPU 很闲,但内存其实没有想象中宽裕。
因此我的建议是增加 2GB 左右 Swap:
bash
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
然后:
bash
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab
同时:
text
vm.swappiness = 10
这里不是让 Java、MySQL 平时靠 Swap 跑。
Swap 在这种服务器上更像一道保险。
平时不用最好,但某个进程突然出现内存尖峰的时候,至少不要让 OOM Killer 第一时间出来随机"枪毙"进程。
十五、最后整理一份服务器体检清单
以后再检查 CentOS/Linux 服务器,我觉得至少可以关注下面这些东西。
文件句柄
bash
ulimit -n
ulimit -Hn
cat /proc/sys/fs/file-nr
sysctl fs.file-max
重点防:
text
Too many open files
TCP 连接
bash
ss -s
ss -ant
重点关注:
text
ESTABLISHED
TIME_WAIT
CLOSE_WAIT
SYN_RECV
TCP 队列
bash
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.netdev_max_backlog
TCP 历史异常
bash
netstat -s | grep -iE "overflow|drop|reset|listen"
这条我认为特别重要。
内核异常
bash
dmesg -T | grep -iE "oom|drop|overflow|tcp|buffer|too many open"
系统资源
bash
free -h
uptime
df -h
应用监听队列
bash
ss -lntp
不要只看 Linux 参数。
重点看看:
text
Nginx
Java
PHP-FPM
MySQL
Redis
真正申请的 backlog 是多少。
写在最后
这次服务器体检最大的收获,并不是把:
text
1024
改成:
text
8192
而是让我重新确认了一件事:
服务器优化应该从证据出发,而不是从网上复制参数出发。
如果没有:
text
SYN drop
TCPBacklogDrop
TIME_WAIT Overflow
OOM
Too many open files
这些真实迹象,那么很多所谓"高并发参数优化"其实没有必要。
但如果像这次一样,服务器已经留下:
text
299729 SYNs to LISTEN sockets dropped
甚至:
text
Possible SYN flooding on port 80
那就说明系统已经明确告诉你:
这里以前扛不住。
这时候再根据连接队列、Nginx backlog、Tomcat accept-count、文件句柄、内存和连接池逐层排查,优化才真正有意义。
所以,如果你的线上服务器已经稳定运行了一两年,我反而建议找个时间登录上去看看。
不要只执行:
bash
top
然后看到 CPU 5% 就退出。
看看:
bash
netstat -s
dmesg
ss -s
ss -lntp
可能会发现一些平时完全注意不到的东西。
服务器和人其实有点像。
平时能跑、能吃、能睡,不代表体检报告一定没有异常。
你的服务器,上一次做"体检"是什么时候?