服务器连接数过多怎么办?TCP连接状态分析与优化技巧

告警弹出"TCP 连接数 3 万",伴随而来的是新建连接超时、Nginx 出现 502、数据库报 Too many connections。这时候最危险的做法是直接把 ulimit 和内核参数拉到最大——连接数只是结果,不是原因。真正要回答的是三个问题:这些连接处于什么状态、是谁建起来的、为什么没被释放。本文按 TCP 状态逐个拆解,给出每一类"连接过多"的判定阈值与对应处置手段。

一、先把连接画像画出来

用 ss 而不是 netstat(后者属于 net-tools,在新发行版上默认不再安装),第一条命令先看总量与状态分布。

# 总量与状态摘要(最关键的一条)
ss -s

# 各 TCP 状态的计数排行
ss -ant | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn

# 只看某一种状态
ss -ant state time-wait | wc -l
ss -ant state established '( dport = :443 or sport = :443 )' | wc -l
ss -ant state close-wait | wc -l
ss -ant state syn-recv | wc -l

# 按远端 IP 聚合,快速识别来源
ss -ant state established | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20

# 按进程聚合,找出连接是哪个进程持有的
ss -antp | awk '{print $NF}' | sort | uniq -c | sort -rn | head -15

ss -s 的输出形如 TCP: 32000 (estab 1200, closed 0, orphaned 30, synrecv 0, timewait 30500/0)。这里要特别注意 orphaned(孤儿连接):它指的是已经不属于任何进程但内核还未回收的 socket,数量突然升高通常意味着有应用在崩溃或被频繁重启。

二、TCP 状态逐个解读

状态含义大量出现的根因判定阈值与处置
ESTABLISHED正常通信中真实并发高;长连接未设超时;连接池配置过大与服务设计容量对比。超过预期 2 倍需限流或扩容;长连接务必设置空闲超时
TIME_WAIT主动关闭方等待 2MSL短连接频繁(HTTP 未开 keepalive、数据库连接未复用)见第三节:是否成问题取决于新建连接速率,不是绝对数量
CLOSE_WAIT对端已关闭,本端未调 close()应用程序 bug:异常分支未释放连接、库版本问题持续大于几十且不断上涨,就是代码问题,调内核参数无效
SYN_RECV收到 SYN 未回 ACK,半连接队列SYN flood 攻击;或 listen backlog 太小结合 nstat 的 ListenOverflows 判断;先调 backlog,再考虑防护
FIN_WAIT_1 / FIN_WAIT_2本端已发 FIN,等待对端对端不响应 FIN;链路中间设备丢包FIN_WAIT_2 堆积可查 tcp_fin_timeout,但根治要看对端
SYN_SENT已发 SYN 未收到响应目标不可达、防火墙丢包、端口未监听检查目标服务与安全组,不是连接数问题

最常被误判的是 TIME_WAIT 和 CLOSE_WAIT,二者只差一个字母,但成因完全相反:TIME_WAIT 多通常说明你的系统是"主动关闭方",属于正常协议行为;CLOSE_WAIT 多则是应用漏了 close(),属于缺陷。

三、TIME_WAIT:到底要不要处理?

先给结论:只有在"本机作为客户端对外发起大量短连接"的场景下,TIME_WAIT 才是实际问题(例如 Nginx/HAProxy 反代到后端、爬虫、调用第三方 API)。服务端被动接受连接时,TIME_WAIT 落在客户端那边。

3.1 判断的量化口径

Linux 上 TIME_WAIT 的存活时长由内核常量 TCP_TIMEWAIT_LEN 决定,固定为 60 秒,并不是由 net.ipv4.tcp_fin_timeout 控制——后者控制的是 FIN_WAIT_2 状态的超时(默认同样为 60 秒),这一点很多老教程是错的。

可用端口数决定上限:

# 查看本地临时端口范围(常见默认 32768 60999,约 2.8 万个)
cat /proc/sys/net/ipv4/ip_local_port_range

# 当前 TIME_WAIT 数量
ss -ant state time-wait | wc -l

# 每秒新建连接速率(1 秒采样,看 PassiveOpens 的变化量)
nstat -az | grep -E 'TcpActiveOpens|TcpPassiveOpens'

算一笔账:端口范围约 28231 个,TIME_WAIT 存活 60 秒,那么向同一个"目标 IP + 目标端口"发起短连接的最大可持续速率约为 28231 / 60 ≈ 470 次/秒。超过这个速率就会出现 Cannot assign requested address(EADDRNOTAVAIL)。这是判断 TIME_WAIT 是否"过多"的硬指标,比看绝对数量有意义得多。

3.2 正确的优化顺序

  1. 第一优先:改成长连接。HTTP 场景开启 keepalive,反代到 upstream 时配置连接池,数据库/Redis 使用连接池。这是唯一能从根本上消除 TIME_WAIT 的办法。
  2. 第二:扩大端口范围。改为 1024 65535,可把上限从约 2.8 万提升到约 6.4 万。
  3. 第三:开启 tcp_tw_reuse。允许在安全条件下复用 TIME_WAIT socket 用于新发起的出站连接,需要两端都开启时间戳。
  4. 不推荐:调 tcp_max_tw_buckets 强行销毁。它会让内核在超出时直接丢弃 TIME_WAIT socket 并打警告日志,属于掩盖问题。
sudo tee /etc/sysctl.d/90-tcp-tuning.conf <<'EOF'
# 扩大临时端口范围,提高短连接并发上限
net.ipv4.ip_local_port_range = 1024 65535

# 允许复用 TIME_WAIT 套接字(仅对出站连接生效,需对端启用时间戳)
net.ipv4.tcp_tw_reuse = 1

# 时间戳必须开启,tcp_tw_reuse 才有效
net.ipv4.tcp_timestamps = 1

# FIN_WAIT_2 超时(默认 60 秒),按需下调
net.ipv4.tcp_fin_timeout = 30

# SYN 队列与 accept 队列
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000

# 孤儿连接上限(被调得过小会触发 "Out of socket memory")
net.ipv4.tcp_max_orphans = 262144
EOF

sudo sysctl --system
sudo sysctl -a --pattern 'ip_local_port_range|tcp_tw_reuse|somaxconn'

必须澄清的一个历史遗留错误:net.ipv4.tcp_tw_recycle 在 Linux 4.12 中已被彻底移除,且它在此前的版本里对 NAT 后的客户端有严重副作用(会导致连接被无理由拒绝)。任何还在推荐设置 tcp_tw_recycle=1 的文档都已过时,切勿照抄。

四、CLOSE_WAIT:这是应用的问题,不是内核的

CLOSE_WAIT 表示"对方已经发来 FIN,我这边也回了 ACK,但我的应用程序还没调用 close()"。只要它持续堆积,就说明代码里有连接没有正确释放——常见于异常分支、超时未处理、或 HTTP 客户端库使用不当(读取响应体后才释放)。

# 找出持有哪些 CLOSE_WAIT 的是哪些进程
ss -antp state close-wait | head -30

# 按进程名聚合
ss -antp state close-wait | awk '{print $NF}' | sort | uniq -c | sort -rn

# 看某个进程打开了多少 fd、上限是多少
PID=1234
ls /proc/$PID/fd | wc -l
grep 'open files' /proc/$PID/limits
lsof -p $PID | awk '{print $4}' | sort | uniq -c | sort -rn | head

# Java 进程抓线程栈看连接持有位置
jstack $PID | grep -A5 -i 'socket\|http' | head -40

处置动作只有两个方向:修代码(确保 finally 块或 try-with-resources 里关闭连接、为 HTTP 客户端设置合理的 connectionRequestTimeout / socketTimeout),或者重启该进程临时缓解(这只能争取时间,不代表修复)。

五、句柄与 accept 队列:连接数上不去 vs 连接被丢弃

两类不同的"连接数问题"要分开处理:

  • 连接数上不去:通常是文件描述符(fd)耗尽,报 Too many open files。
  • 连接建了但被丢弃:是 listen/accept 队列溢出,表现为客户端超时但服务端日志里没有请求。

5.1 文件描述符

# 系统级上限
cat /proc/sys/fs/file-max
cat /proc/sys/fs/file-nr          # 三个数:已分配 / 空闲 / 上限

# 当前 shell 的限制(很多发行版 soft 为 1024,这是最常见的天花板)
ulimit -n
ulimit -Hn

# systemd 服务的限制(PAM 的 limits.conf 对服务无效,必须在这里改)
systemctl show nginx.service -p LimitNOFILE
sudo systemctl edit nginx.service
# [Service]
# LimitNOFILE=65535
sudo systemctl daemon-reload && sudo systemctl restart nginx

判断阈值:当某个进程的 fd 使用数达到其 LimitNOFILE 的 80% 时就要处理,别等到报错。

5.2 accept 队列溢出

# 这两个计数持续增长,就是 accept 队列溢出
nstat -az | grep -iE 'TcpExtListenOverflows|TcpExtListenDrops'

# 确认当前 backlog 上限
cat /proc/sys/net/core/somaxconn
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
cat /proc/sys/net/ipv4/tcp_abort_on_overflow

somaxconn 的默认值随内核版本变化明显:Linux 5.4 之前为 128,5.4 起调整为 4096。如果你的系统是较旧内核,这个值往往就是瓶颈。注意调大 somaxconn 的同时,应用自身的 listen backlog 参数也要一起调——Nginx 是 listen ... backlog=,Java 的 ServerSocket 构造参数、Tomcat 的 acceptCount 同理,否则内核参数调大也没用。

tcp_abort_on_overflow 默认为 0,即队列满时内核丢弃 SYN;置为 1 会改为直接回 RST,让客户端立即失败而非超时。它有助于快速失败,但会让客户端来不及重试,一般保持默认。

六、conntrack 表满:NAT 与防火墙场景专属坑

只要机器启用了 iptables/nftables 的状态跟踪(做 NAT、Docker 端口映射、网关),连接都会占用 conntrack 表项。表满时内核日志会出现 nf_conntrack: table full, dropping packet,表现为部分连接随机超时,非常难查。

# 当前用量与上限
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_buckets

# 内核日志确认
dmesg -T | grep -i conntrack

# 详细统计(需安装 conntrack-tools)
sudo apt install -y conntrack
sudo conntrack -L | wc -l
sudo conntrack -S

处理能力估算:nf_conntrack_max 的常见默认值因发行版和内存而异(数万到数十万不等),以本机输出为准。用量达到上限的 70% 就应该扩容或排查异常连接,因为表项回收有延迟,等到 100% 时丢包已经发生。调整时要同时增大 nf_conntrack_buckets(哈希桶数),否则哈希链过长会让查找变慢。

七、应用侧的正确做法

7.1 Nginx 相关配置

# 主配置:worker 数与连接数上限
worker_processes auto;
worker_rlimit_nofile 65535;
events {
    worker_connections 16384;
    multi_accept on;
}

# 客户端长连接:超时不宜过长,否则空闲连接白占资源
http {
    keepalive_timeout 15s;
    keepalive_requests 1000;      # 较老版本默认 100,1.19.10 起改为 1000

    upstream backend {
        server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
        keepalive 64;             # 到后端的长连接池,消除 TIME_WAIT
    }
    server {
        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;              # 必须 1.1 才能复用
            proxy_set_header Connection "";      # 清掉 close,否则长连接失效
            proxy_connect_timeout 3s;
            proxy_read_timeout 30s;
        }
    }
}

# 语法自检与生效
sudo nginx -t && sudo systemctl reload nginx

这里最值得记住的是 upstream 里的 keepalive 配合 proxy_http_version 1.1 与 Connection ""。缺了后两行,keepalive 不会生效,你会发现 Nginx 与后端之间产生海量 TIME_WAIT。

7.2 数据库连接池

MySQL 的 max_connections 默认为 151,很多应用为每个请求开一条连接,几百并发就能打满。正确做法是连接池化,并把池大小控制在合理范围——池子不是越大越好,过多连接会导致上下文切换与锁竞争反而变慢。

# 观察数据库侧连接来源
mysql -e "SHOW STATUS LIKE 'Threads_%'; SHOW VARIABLES LIKE 'max_connections';"
mysql -e "SELECT user,host,count(*) FROM information_schema.processlist GROUP BY user,host;"

八、一份可复制的排查清单

  1. ss -s 看总量与 orphaned,先判断是"连接没释放"还是"请求量真的涨了"。
  2. ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn 看状态分布,把问题归到 ESTABLISHED / TIME_WAIT / CLOSE_WAIT / SYN_RECV 中的一类。
  3. ss -antp | awk '{print $NF}' | sort | uniq -c 定位到具体进程。
  4. TIME_WAIT 多 → 算新建连接速率是否接近 端口数/60 这个上限;优先改长连接。
  5. CLOSE_WAIT 多 → 直接查应用代码与库版本,不要调内核。
  6. SYN_RECV 多 → 查 nstat 的 ListenOverflows,判断是 backlog 不足还是攻击。
  7. 报 Too many open files → 查 ulimit -n 与 systemd 的 LimitNOFILE。
  8. 随机超时且做 NAT/Docker 映射 → 查 nf_conntrack_count。
  9. 改完内核参数后,用压测或观察一个完整业务周期验证,并记录改动前后对比值。

小结

连接数过多从来不是"把参数调大"能解决的问题。核心判断顺序是:状态分类 → 归属进程 → 判断是协议正常行为还是程序缺陷 → 再决定调内核还是改代码。其中 CLOSE_WAIT 是缺陷、TIME_WAIT 多为正常、SYN_RECV 要区分攻击与 backlog 不足。

避坑建议:

  • 不要用 tcp_tw_recycle——Linux 4.12 起该参数已不存在,此前使用它还会破坏 NAT 后的客户端连接。
  • 不要以为 TIME_WAIT 的存活时长由 tcp_fin_timeout 控制,实际是内核常量的 60 秒,tcp_fin_timeout 管的是 FIN_WAIT_2。
  • tcp_tw_reuse 只对出站连接生效,对服务端被动接收的连接没有任何作用。
  • 别盲目把 tcp_max_tw_buckets 调小,那只是让内核悄悄丢弃 TIME_WAIT 并打日志,问题照旧。
  • 调 somaxconn 的同时必须调应用自己的 listen backlog,两处都要改才生效。
  • systemd 服务不受 /etc/security/limits.conf 约束,fd 限制要用 systemctl edit 设 LimitNOFILE。
  • 长连接一定要配空闲超时。没有超时的长连接在异常网络下会长期占用 fd 和内存,比短连接更危险。
  • 任何内核参数改动都要写进 /etc/sysctl.d/*.conf 并执行 sysctl --system,直接 sysctl -w 重启即失效。

先把 ss -s 和状态分布看清楚,再动手改任何参数。这一步能省掉八成的无效调参。