网络基础——10 万长连接是如何拖垮服务器的 / Network Fundamentals and the Cost of One Hundred Thousand Connections
📅 创建时间:2026-05-08 🏷️ 标签:#TCP #HTTP #三次握手 #四次挥手 #time_wait #滑动窗口 📚 前置知识:[[00-backend-overview]] [[05-middleware]](网关的连接问题) 📚 相关知识:[[08-concurrency]](线程池与连接数)
场景:你的服务器有 65535 个连接,但它已经撑不住了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 运维监控: │
│ CPU: 5%(几乎空闲) │
│ 内存: 30%(正常) │
│ 网络: 100Mbps(满了) │
│ 连接数: 40000(很多) │
│ │
│ 但用户投诉:页面加载慢,请求超时。 │
│ │
│ 为什么 CPU 空闲,但系统却很慢? │
│ │
│ 问题出在 TCP 连接的底层机制上。 │
│ │
└─────────────────────────────────────────────────────────────┘第1节:TCP 三次握手——为什么连接这么慢
场景:两个人打电话
小李(客户端):喂,我想和你建立通话,可以吗?
小明(服务端):可以!请问你听得到吗?
小李(客户端):听得到!
→ 通话建立!TCP 三次握手图解
┌─────────────────────────────────────────────────────────────┐
│ TCP 三次握手 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务端 │
│ │ │ │
│ │ ─────── SYN=1, seq=x ──────────────────────▶ │ │
│ │ 第一次握手:客户端想建立连接,发送 SYN │ │
│ │ │ │
│ │ ◀───────────────── SYN=1, seq=y, ACK=x+1 ─── │ │
│ │ 第二次握手:服务端同意,返回 SYN + ACK │ │
│ │ │ │
│ │ ─────── ACK=y+1 ─────────────────────────────▶ │ │
│ │ 第三次握手:客户端确认收到 │ │
│ │ │ │
│ │ 建立连接! │ │
│ │ │ │
│ │
│ 为什么不两次握手? │
│ 客户端发送 SYN 后崩溃了 → 服务端收到 SYN → 等待 │
│ → 服务端资源被浪费(半连接队列) │
│ │
│ 为什么不四次握手? │
│ 服务端的 SYN + ACK 可以一起发(三次够了) │
│ │
└─────────────────────────────────────────────────────────────┘HTTP 请求的完整过程
┌─────────────────────────────────────────────────────────────┐
│ HTTP 请求完整过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ DNS 解析(第一次通信) │
│ 浏览器 ──▶ DNS 服务器 ──▶ 获得 IP │
│ │
│ TCP 连接(三次握手) │
│ 浏览器 ──▶ 服务器(SYN)─── SYN+ACK ──▶ 浏览器 │
│ 浏览器 ──▶ 服务器(ACK) │
│ │
│ TLS 握手(HTTPS) │
│ 浏览器 ──▶ 服务器(ClientHello) │
│ 浏览器 ◀── 服务器(ServerHello + 证书) │
│ 浏览器 ──▶ 服务器(密钥交换) │
│ 浏览器 ◀── 服务器(握手完成) │
│ │
│ HTTP 请求/响应 │
│ 浏览器 ──▶ 服务器(GET /api/order HTTP/1.1) │
│ 浏览器 ◀── 服务器(200 OK + JSON) │
│ │
│ TCP 断开(四次挥手) │
│ 浏览器 ──▶ 服务器(FIN)─── ACK ──▶ 浏览器 │
│ 浏览器 ◀── 服务器(FIN) │
│ 浏览器 ──▶ 服务器(ACK) │
│ │
│ 总耗时: │
│ HTTP/1.1 + TLS 1.3:约 3 个 RTT(往返时延) │
│ 假设 RTT = 50ms:建立连接需要 150ms │
│ │
└─────────────────────────────────────────────────────────────┘第2节:四次挥手——为什么连接总是关不掉
场景:两个人通话结束
小李:我说完了,再见!
小明:好的收到!
...(等待小明确认说完)...
小明:我也说完了,再见!
小李:好的!TCP 四次挥手图解
┌─────────────────────────────────────────────────────────────┐
│ TCP 四次挥手 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务端 │
│ │ │ │
│ │ ─────── FIN=1, seq=u ────────────────────────▶ │ │
│ │ 第一次挥手:客户端发送 FIN,请求断开 │ │
│ │ │ │
│ │ ◀────────────────── ACK=u+1 ─────────────────── │ │
│ │ 第二次挥手:服务端确认(但可能还有数据没发完)│ │
│ │ │ │
│ │ ... 处理剩余数据 ... │ │
│ │ │ │
│ │ ◀────────────────── FIN=1, seq=w ─────────────── │ │
│ │ 第三次挥手:服务端发完数据,发送 FIN │ │
│ │ │ │
│ │ ─────── ACK=w+1 ──────────────────────────────▶ │ │
│ │ 第四次挥手:客户端确认 │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ TIME_WAIT (客户端) │ │ │
│ │ │ 等待 2MSL(分) │ │ │
│ │ │ 确保服务端收到 ACK │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ │
│ 为什么不三次挥手? │
│ 服务端发完数据才能发 FIN,所以 ACK 和 FIN 分开发送 │
│ │
└─────────────────────────────────────────────────────────────┘TIME_WAIT:为什么端口用着用着就没了
┌─────────────────────────────────────────────────────────────┐
│ TIME_WAIT 问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ TIME_WAIT = 2MSL(Maximum Segment Lifetime) │
│ MSL = 60 秒(Linux 默认) │
│ TIME_WAIT = 2 分钟 │
│ │
│ 作用: │
│ 1. 确保最后的 ACK 能到达服务端 │
│ 2. 让旧连接的重复数据包在网络中消散 │
│ │
│ 问题: │
│ 连接关闭后,客户端端口处于 TIME_WAIT 状态 2 分钟 │
│ 每个连接占用一个端口 │
│ 系统端口数:65535 │
│ 高并发短连接:2分钟内用完所有端口 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 查看:netstat -an | grep TIME_WAIT │ │
│ │ │ │
│ │ 解决: │ │
│ │ 1. 客户端使用连接池(复用连接) │ │
│ │ 2. 服务端设置 SO_LINGER = 0(强制关闭连接) │ │
│ │ 3. 调整 MSL:sysctl -w net.ipv4.tcp_fin_timeout=30 │ │
│ │ 4. 开启 tcp_tw_reuse(复用 TIME_WAIT 端口) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘CLOSE_WAIT:为什么连接关不掉
┌─────────────────────────────────────────────────────────────┐
│ CLOSE_WAIT 问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ CLOSE_WAIT = 服务端收到了 FIN,但没调用 close() │
│ │
│ 常见原因: │
│ 代码 bug:读取数据后没调用 socket.close() │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 服务端代码 bug: │ │
│ │ │ │
│ │ while True: │ │
│ │ data = socket.recv(1024) │ │
│ │ if not data: │ │
│ │ break # ← 这里没有调用 close()! │ │
│ │ process(data) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 后果:CLOSE_WAIT 越来越多 → 端口耗尽 │
│ │
│ 排查命令: │
│ netstat -an | grep CLOSE_WAIT | wc -l │
│ netstat -an | grep CLOSE_WAIT │
│ │
└─────────────────────────────────────────────────────────────┘第3节:HTTP 各版本对比
┌─────────────────────────────────────────────────────────────┐
│ HTTP/1.0 vs 1.1 vs 2.0 vs 3.0 │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTP/1.0(1996): │
│ • 每次请求建立新连接,用完关闭 │
│ • 无 Keep-Alive,每个资源都要一次握手 │
│ • 连接数限制:浏览器最多 4-6 个并发连接 │
│ │
│ HTTP/1.1(1999,推荐): │
│ • 默认开启 Keep-Alive(连接复用) │
│ • 管道化(Pipelining):不等待响应可发下一个请求 │
│ • 问题:队头阻塞(HOL Blocking) │
│ → 请求 1 响应慢,请求 2/3 也被阻塞 │
│ │
│ HTTP/2.0(2015): │
│ • 多路复用:一个 TCP 连接并行传输多个请求/响应 │
│ → 解决 HTTP/1.1 的队头阻塞 │
│ • Header 压缩(HPACK) │
│ • Server Push:服务端主动推送资源 │
│ • 问题:TCP 层面的队头阻塞(一个包丢了,全部等) │
│ │
│ HTTP/3.0(QUIC,2022): │
│ • 基于 UDP,不存在 TCP 队头阻塞 │
│ • 0-RTT(快速恢复,上下文复用) │
│ • 连接迁移(切换网络 IP 不掉线) │
│ • 目前在逐步采用中 │
│ │
│ 实际影响: │
│ • 静态资源多 → HTTP/2 多路复用效果明显 │
│ • 高丢包网络 → HTTP/3 优势明显 │
│ │
└─────────────────────────────────────────────────────────────┘第4节:Keep-Alive vs 长连接
场景:10 万 QPS 要建立 10 万次 TCP 连接吗?
HTTP/1.0,无 Keep-Alive:
10 万请求 = 10 万次 TCP 连接 = 10 万次握手
→ 握手耗时 150ms × 10 万 = 15000 秒 = 4 小时!(不可能)
HTTP/1.1,Keep-Alive:
→ 建立一个 TCP 连接,复用处理 10 万请求
→ 10 万次 HTTP 请求,复用少量 TCP 连接
→ 性能提升 10-100 倍连接池:最优实践
python
# Python requests 的连接池(默认开启)
import requests
# 单个 Session 对象维护一个连接池
session = requests.Session()
for i in range(10000):
session.get(f"http://api.example.com/user/{i}")
# 实际只建立了几个 TCP 连接,复用处理 1 万请求
# HTTP/1.1 Keep-Alive 超时(服务端默认 15 秒)
# 超过这个时间没活动,连接关闭第5节:滑动窗口——TCP 怎么控制流量
┌─────────────────────────────────────────────────────────────┐
│ TCP 滑动窗口 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 问题:发送方发得太快,接收方处理不过来怎么办? │
│ │
│ 滑动窗口 = 接收方告诉发送方:"你最多发这么多" │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 接收方窗口大小:4 个包 │ │
│ │ │ │
│ │ 已确认 已发未确认 可发送 不能发 │ │
│ │ [1,2] [3,4] [5,6,7] [8,9,10] │ │
│ │ ↑ │ │
│ │ 发送指针 │ │
│ │ │ │
│ │ ACK=3 到达 → 滑动 → [1,2]已确认,可发 [8,9] │ │
│ │ │ │
│ │ 如果接收方处理慢(窗口=0): │ │
│ │ 发送方停止发送 → 零窗口探测(探测窗口何时打开) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 拥塞控制(防止网络本身过载): │
│ • 慢启动:连接建立时,从小到大试探窗口大小 │
│ • 拥塞避免:达到阈值后,缓慢增长 │
│ • 快重传:收到 3 个重复 ACK,立即重传丢失的包 │
│ • 快恢复:重传后,窗口减半而不是回到起点 │
│ │
└─────────────────────────────────────────────────────────────┘升华:网络问题排查思路
┌─────────────────────────────────────────────────────────────┐
│ 网络问题排查三板斧 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一板斧:netstat / ss 查看连接状态 │
│ ss -s # 统计各类连接数量 │
│ ss -tan state established | wc -l # 并发连接数 │
│ │
│ 第二板斧:TIME_WAIT 太多? │
│ → 开启 tcp_tw_reuse │
│ → 减少 MSL 时间 │
│ → 服务端主动关闭连接 │
│ │
│ 第三板斧:CLOSE_WAIT 太多? │
│ → 一定是代码 bug:调用了 close() 的地方没调用 │
│ → 搜索代码中的 close() 调用 │
│ │
│ 一句话总结: │
│ TIME_WAIT = 协议行为(调参数),CLOSE_WAIT = 代码 bug。 │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ netstat / ss / tcpdump 的具体命令参数
✅ /proc/sys/net/ 下各种 TCP 参数
✅ HTTP/2 HPACK 压缩算法细节
必须理解:
🔴 三次握手 vs 四次挥手(为什么一个是三,一个是四)
🔴 TIME_WAIT 和 CLOSE_WAIT 的本质区别
🔴 TIME_WAIT 太多的后果和解决方案
🔴 HTTP/1.1 vs 2.0 多路复用的区别
🔴 队头阻塞在 HTTP/1.1 和 HTTP/2 的不同表现学习状态:🟡 开始学习