HTTP 连接资源消耗¶
理解单个 HTTP 连接的真实资源开销,才能在百万并发场景下做出正确的架构决策
核心概念¶
- TCP 连接内存 — 内核为每个连接分配发送/接收缓冲区和控制块,约 3-10 KB
- 线程开销 — 传统模型每连接一个线程(~1 MB),协程模型仅需几 KB
- TLS 会话 — HTTPS 连接额外消耗 50-100 KB 内存
- 文件描述符 — 每个连接占一个 fd,受系统 ulimit 限制
- 临时端口 — 客户端端口上限 65535,高并发时需要多端口或多 IP
详解¶
单个 TCP 连接的内核开销¶
操作系统为每个 TCP 连接分配的资源:
| 资源 | 大约消耗 | 说明 |
|---|---|---|
| 内核内存 | 3-10 KB | 发送/接收缓冲区 + TCP 控制块 |
| 文件描述符 | 1 个 fd | 受 ulimit 限制 |
| CPU | — | 三次握手 + 协议栈处理 |
| 临时端口 | 1 个 | 客户端端口,上限 65535 |
服务器侧的连接模型差异¶
| 模型 | 每连接内存 | 比喻 |
|---|---|---|
| 线程模型(Java BIO) | ~1 MB(线程栈) | 每人占一条泳道(1米宽) |
| 协程模型(Go/async) | ~4 KB | 每人只占一把椅子 |
| 异步事件驱动(Nginx) | ~2 KB | 几乎不占位置 |
百万并发的真实数字¶
| 指标 | 单连接 | × 1,000,000 |
|---|---|---|
| 内核内存 | ~10 KB | ~10 GB |
| 线程(传统模型) | ~1 MB | ~1 TB(不可行) |
| 协程(Go/async) | ~4 KB | ~4 GB(可行) |
| TLS 内存(HTTPS) | ~50 KB | ~50 GB |
| 文件描述符 | 1 | 100 万(需调 ulimit) |
形象比喻¶
传统线程模型 — 泳池:
异步/事件驱动模型 — 叫号系统:
高速公路收费站:
| 模型 | 比喻 | 100 万车 |
|---|---|---|
| 线程模型 | 每辆车配一个专属收费员 | 需要 100 万个收费员 |
| 异步模型 | 一个收费员 + 叫号系统 | 一个收费员 + 一块显示屏 + 几 GB 内存 |
撑住百万并发的关键手段¶
| 手段 | 说明 |
|---|---|
| Epoll / Kqueue | 不为每个连接开线程,一个线程监控百万 fd |
| 连接池复用 | HTTP/1.1 Keep-Alive、HTTP/2 多路复用,少建新连接 |
| 轻量协程 | Go goroutine、Rust async、Python asyncio |
| 负载均衡 | L4 LB 分散到几十台机器 |
| 内核调优 | ulimit -n 1048576、调整 TCP 缓冲区大小 |
常见陷阱¶
误区:连接池越大越好
池子里每个连接都占内存和 fd。配得合理比配得大更重要。连接池打满时新请求排队,延迟飙升。
别忽视 TLS 开销
HTTPS 场景下 TLS 会话缓存每个连接 50-100 KB,百万并发下可能吃掉 50 GB 内存。考虑在 CDN 边缘终止 TLS。
练习题¶
Go 的 goroutine 初始栈大小是多少?和 Java 线程相比如何?
答案
Go goroutine 初始栈大小为 2 KB(可动态增长到 1 GB),Java 线程默认栈大小为 1 MB。这意味着在相同内存下,Go 能创建的并发单元数量是 Java 的约 500 倍。
为什么操作系统对客户端临时端口有限制?
答案
客户端端口是 16 位整数,理论上限 65535。每个 TCP 连接需要一个唯一的四元组(源IP:源端口:目标IP:目标端口),如果一个客户端对同一服务器发起大量连接,端口很快耗尽。解决方案包括使用多 IP、连接池复用、HTTP/2 多路复用等。