一、高并发基础:从一次线上事故说起

我记得那是几年前的一个双十一晚上。监控大屏上,某服务的连接数曲线突然从 2 万飙到了 12 万。我盯着 Wireshark 的抓包界面,满屏的 SYN 包、RST 包像瀑布一样往下刷。服务器 CPU 才 30%,但新连接就是建不起来。

那是我第一次真正意识到:高并发不是简单的「请求多」,而是连接模型、操作系统、网络协议栈三者之间的博弈。今天咱们就从最基础的概念开始,把这些底层的逻辑理清楚。

1.1 什么是高并发?

高并发,说白了就是单位时间内系统能同时处理的请求量。但这里有个坑——很多人把「并发」和「吞吐」混为一谈。

核心指标:

  • QPS(每秒查询数):1 秒内能处理多少请求
  • 并发连接数:同一时刻系统保持的连接总量
  • 响应时间:从发请求到收到响应的时间差

举个例子。你开了一家奶茶店。QPS 是「每分钟能做几杯」,并发连接数是「店里同时站了多少人」。如果店里挤了 100 个人,但店员只能 1 分钟做 1 杯——这就是高并发低吞吐,典型的「看起来热闹,实际效率低」。

我在项目中遇到过最典型的场景:某网关 QPS 只有 5000,但并发连接数到了 8 万。为什么?因为每个请求处理时间太长,连接一直不释放。Wireshark 里一看,全是 TIME_WAIT 状态的连接。

1.2 TCP 连接模型:三次握手与四次挥手

要分析高并发,TCP 连接模型是绕不开的基础。你想想看,每次 HTTP 请求背后,都藏着一套完整的 TCP 生命周期。

三次握手(建立连接)

客户端和服务端建立连接时,需要交换三个报文:

客户端                         服务端
  |                              |
  |-------- SYN (seq=x) -------->|  第一次:客户端说「我要连你」
  |                              |
  |<---- SYN+ACK (seq=y,ack=x+1)-|  第二次:服务端说「收到,我也准备好了」
  |                              |
  |-------- ACK (seq=x+1,ack=y+1)->|  第三次:客户端说「好,开始传数据」
  |                              |

嗯,这里要注意:第三次握手是可以携带数据的。很多高性能服务器会利用这个特性做「TCP 快速打开」,减少一次 RTT 的延迟。

四次挥手(关闭连接)

客户端                         服务端
  |                              |
  |-------- FIN (seq=u) -------->|  第一次:客户端说「我发完了」
  |                              |
  |<---- ACK (ack=u+1) ---------|  第二次:服务端说「知道了,但我还有数据」
  |                              |
  |<---- FIN (seq=v) -----------|  第三次:服务端说「我也发完了」
  |                              |
  |-------- ACK (ack=v+1) ------>|  第四次:客户端说「好,关闭」
  |                              |

我曾经在排查一个连接泄漏问题时,发现服务端有大量 CLOSE_WAIT 状态的连接。原因很简单——服务端代码里忘了调用 close()。Wireshark 里看,服务端只回了 ACK,没发 FIN。这种问题在高并发下特别隐蔽,因为连接数一多,你根本看不出是哪个 socket 出了问题。

1.3 并发与并行的区别

这两个词经常被混用,但本质完全不同。我习惯用一个比喻来解释:

概念 比喻 本质
并发 一个人同时做三件事(来回切换) 逻辑上的「同时」
并行 三个人各做一件事(真正同时) 物理上的「同时」

单核 CPU 只能并行执行一个线程,但可以通过时间片轮转实现「并发」。多核 CPU 才能做到真正的「并行」。

实战经验: 我在调优一个 Nginx 网关时,发现即使把 worker_processes 设成 8(8 核),QPS 也只提升了 30%。Wireshark 抓包发现,瓶颈不在 CPU,而在网卡中断亲和性——所有中断都打在一个核上。这就是典型的「并发模型设计不合理」。

1.4 高并发场景下的网络特征

高并发下,网络行为会和低负载时完全不同。我总结了几个最明显的特征:

1. 连接数暴涨,端口耗尽

每个 TCP 连接都需要一个本地端口。默认情况下,Linux 的临时端口范围是 32768-60999,总共也就 2.8 万个。如果客户端发起的连接超过这个数,就会报 Cannot assign requested address。

避坑指南: 我曾经在生产环境遇到过这个问题。当时 Wireshark 里看到大量 SYN 包发出去后没有 SYN+ACK 回来,客户端一直在重试。排查了半天,发现是客户端端口用完了。解决方案:调大 net.ipv4.ip_local_port_range,或者开启 tcp_tw_reuse 复用 TIME_WAIT 状态的端口。

2. 小包风暴,CPU 飙升

高并发下,网络包的平均大小会变小。因为很多连接只发几个字节的请求就关闭了。这些小包的处理开销(中断、上下文切换)远大于大包。Wireshark 里看,如果 平均包长小于 200 字节,基本可以断定存在小包风暴。

3. 丢包与重传的恶性循环

当网络或服务器处理不过来时,开始丢包。TCP 检测到丢包后会启动重传,重传又增加了网络负载,导致更多丢包。Wireshark 的「Expert Info」里,如果看到大量 Retransmission 和 Fast Retransmission,说明系统已经进入拥塞状态。

4. 连接建立延迟(SYN 队列溢出)

服务端维护两个队列:SYN 队列(半连接)和 Accept 队列(全连接)。高并发下,如果 Accept 队列满了,新来的 SYN 包会被直接丢弃。客户端收不到 SYN+ACK,就会不断重试。Wireshark 里表现为:客户端发了 SYN,服务端没回包,几秒后客户端重发 SYN。

高并发场景下 TCP 连接建立流程与瓶颈点 客户端 服务端 SYN 队列 (半连接) Accept 队列 (全连接) ① SYN ② SYN+ACK ③ ACK ⚠ 瓶颈点 SYN 队列满 → 丢 SYN 包 Accept 队列满 → 丢 ACK 包 Wireshark 观察点 大量 SYN 重传 → SYN 队列溢出 大量 ACK 重传 → Accept 队列溢出 核心结论 高并发下 TCP 连接瓶颈往往不在带宽,而在队列溢出和端口耗尽

1.5 高并发下的关键网络参数

最后,我整理了一份高并发场景下最常用的内核参数清单。这些参数我在每次排查问题时都会先检查一遍:

参数 默认值 高并发建议值 说明
net.ipv4.tcp_tw_reuse 0 1 复用 TIME_WAIT 连接
net.ipv4.tcp_tw_recycle 0 0(已废弃) NAT 环境下有坑,别开
net.ipv4.tcp_fin_timeout 60 15-30 减少 FIN_WAIT2 等待时间
net.core.somaxconn 128 1024-65535 Accept 队列最大长度
net.ipv4.tcp_max_syn_backlog 256 1024-65535 SYN 队列最大长度
net.ipv4.ip_local_port_range 32768 60999 1024 65535 扩大临时端口范围

我的习惯: 每次接手一个新系统,我都会先用 Wireshark 抓 5 分钟的流量,然后看三个指标:SYN 重传率、TIME_WAIT 占比、平均包长。这三个数据基本能告诉你系统的「健康状态」。如果 SYN 重传率超过 1%,说明服务端已经扛不住了。