diff --git a/docs/networking/cdn.md b/docs/networking/cdn.md new file mode 100644 index 0000000..e5c558b --- /dev/null +++ b/docs/networking/cdn.md @@ -0,0 +1,200 @@ +# CDN(Content Delivery Network) + +!!! note "把资源复制到全世界一堆服务器上,用户就近获取——互联网上最基础的加速手段" + +--- + +## 核心概念 + +1. **边缘节点** — CDN 在全球部署的缓存服务器,用户就近访问 +2. **回源** — 边缘节点缓存未命中时,去源站拉取文件 +3. **智能 DNS** — 根据用户地理位置将请求导向最近的边缘节点 +4. **静态资源加速** — 图片、JS、CSS、视频等不变的内容最适合上 CDN +5. **DDoS 防护** — 攻击流量被分散到全球边缘节点吸收 + +--- + +## 详解 + +### 没有 CDN 的世界 + +所有用户都直接访问源站服务器: + +```mermaid +graph TD + S[源站服务器 上海] + A[用户A 上海 5ms] --> S + B[用户B 北京 30ms] --> S + C[用户C 广州 50ms] --> S + D[用户D 纽约 200ms] --> S + E[用户E 伦敦 250ms] --> S +``` + +所有流量挤在一台服务器上,远距离用户体验差,服务器扛不住。 + +### 有 CDN 之后 + +```mermaid +graph TD + S[源站 上海] + E1[边缘节点 上海] + E2[边缘节点 北京] + E3[边缘节点 纽约] + E4[边缘节点 伦敦] + S --> E1 + S --> E2 + S --> E3 + S --> E4 + A[用户A] --> E1 + B[用户B] --> E2 + D[用户D] --> E3 + E[用户E] --> E4 +``` + +每个用户都就近访问最近的边缘节点,不直接打源站。 + +### CDN 工作流程 + +```mermaid +sequenceDiagram + participant U as 用户 + participant DNS as 智能 DNS + participant E as CDN 边缘节点 + participant S as 源站 + U->>DNS: 请求 cdn.example.com/logo.png + DNS->>U: 解析到最近的边缘节点 IP + U->>E: GET /logo.png + alt 缓存命中 + E->>U: 200 OK + 图片(直接返回) + else 缓存未命中 + E->>S: GET /logo.png(回源) + S->>E: 200 OK + 图片 + Note over E: 存一份缓存 + E->>U: 200 OK + 图片 + end +``` + +关键点:**第一次请求会慢一点(回源),之后就快了。** + +### CDN 缓存了什么? + +| 适合放 CDN | 不适合放 CDN | +|-----------|-------------| +| 图片(logo、banner、产品图) | 用户个人数据 | +| JS / CSS 文件 | 实时交易数据 | +| 视频、音频 | 登录接口 | +| 字体文件 | 搜索接口 | +| 软件安装包、APK | 任何需要实时性的内容 | + +简单判断标准:**内容不怎么变 + 所有人看到的一样 + 可以缓存 → 适合 CDN。** + +### CDN 核心能力 + +#### 1. 加速 + +``` +没 CDN:用户(伦敦)→ 上海源站 = 250ms +有 CDN:用户(伦敦)→ 伦敦边缘节点 = 8ms +``` + +#### 2. 减轻源站压力 + +``` +100 万用户 → 99 万次命中边缘节点 → 源站只处理 1 万次回源 +``` + +**99% 的流量被边缘节点消化掉了。** + +#### 3. 防 DDoS + +``` +攻击者打 100Gbps 流量 → 分散到全球几百个边缘节点吸收 → 源站几乎无感 +``` + +#### 4. HTTPS 终止 + +边缘节点处理 TLS 握手,到源站可以用 HTTP(内网更快): + +``` +用户 ←── HTTPS ──→ CDN 边缘节点 ←── HTTP ──→ 源站 + 安全 信任内网,可以明文 +``` + +### 缓存策略 + +```nginx +# Nginx 源站告诉 CDN 怎么缓存 + +# 静态资源缓存 1 天 +Cache-Control: max-age=86400 + +# 带版本号的资源缓存 1 年 +Cache-Control: max-age=31536000, immutable +# 例如 /js/app.v3.2.1.js ← 文件名带版本,变了就是新文件 + +# 别缓存 +Cache-Control: no-cache + +# 先检查源站有没有更新 +Cache-Control: max-age=300, must-revalidate +``` + +CDN 缓存分层: + +```mermaid +graph LR + U[用户] --> E[边缘节点
缓存1] + E --> R[区域节点
缓存2] + R --> S[源站
原始文件] +``` + +--- + +## 实际配置 + +### 阿里云 CDN + +```bash +# 1. 控制台添加加速域名 +# 加速域名:cdn.example.com +# 回源地址:origin.example.com + +# 2. DNS 配置 CNAME +# cdn.example.com → cdn.example.com.w.alikunlun.com + +# 3. 缓存规则 +# /static/* → 缓存 30 天 +# /images/* → 缓存 7 天 +# /api/* → 不缓存 +``` + +### Cloudflare + +```bash +# 1. 把域名的 NS 改成 Cloudflare 的 +# 2. 自动开启 CDN +# 3. 拖个滑块选安全级别 +# 完事。 +``` + +--- + +## 常见陷阱 + +!!! warning "缓存一致性问题" + 更新了 logo.png,但 CDN 还缓存旧的,用户看到旧 logo。解决方案:(1)文件名带版本号 `/logo.v2.png`;(2)调用 CDN API 强制清除缓存;(3)设置较短的 `max-age` 等自然过期。 + +!!! warning "CDN 服务商挂了全挂" + 过度依赖单一 CDN 有中心化风险。重要业务考虑多 CDN 冗余或回源降级方案。 + +--- + +## 练习题 + +??? question "CDN 的智能 DNS 是怎么知道用户在哪里的?" + ??? success "答案" + CDN 的 DNS 服务器会根据**请求来源 IP 的地理位置**(通过 IP 地理位置数据库)来决定返回哪个边缘节点的 IP。这个过程对用户完全透明,用户只需解析域名,CDN 的 DNS 自动帮你选最近的节点。 + +??? question "Cache-Control: immutable 是什么意思?" + ??? success "答案" + `immutable` 告诉浏览器:这个资源永远不会改变,不需要向源站发起条件请求(If-Modified-Since/ETag 验证)。适用于文件名带版本号的静态资源(如 `/app.v2.js`),版本变了就是新 URL,所以内容一定是新的。 diff --git a/docs/networking/connection-pooling.md b/docs/networking/connection-pooling.md new file mode 100644 index 0000000..869dc62 --- /dev/null +++ b/docs/networking/connection-pooling.md @@ -0,0 +1,165 @@ +# 连接池复用 + +!!! note "预先把连接建好放着,谁要用谁借,用完别关——高频场景下最简单有效的优化" + +--- + +## 核心概念 + +1. **连接池** — 一批预先建好的连接集合,用时借出,用完归还而非关闭 +2. **HTTP/1.1 Keep-Alive 复用** — 同一 TCP 连接上串行处理多个请求 +3. **HTTP/2 多路复用** — 一个 TCP 连接上并行处理多个请求/响应 +4. **池化参数** — 最大连接数、最小空闲数、超时时间等关键配置 + +--- + +## 详解 + +### 没有连接池的世界 + +每次请求都要:建 TCP 连接 → TLS 握手 → 发数据 → 四次挥手关掉。 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + loop 每次请求 + C->>S: TCP 三次握手 + C->>S: TLS 握手(HTTPS) + C->>S: GET /api/data + S->>C: 200 OK + 数据 + C->>S: TCP 四次挥手 + end +``` + +就像每次出门买菜都要:解锁 → 开门 → 走出去 → 锁门,回来再解锁 → 开门 → 走进去 → 锁门。一天买十次菜,光开关门就累死了。 + +### 有连接池的世界 + +```mermaid +sequenceDiagram + participant P as 连接池 + participant S as 服务器 + Note over P: 启动时建好 50 个连接 + participant C as 请求 + C->>P: 借连接 A + C->>S: GET /api/data(复用连接 A) + S->>C: 200 OK + C->>P: 还连接 A + C->>P: 借连接 B + C->>S: GET /api/user(复用连接 B) + S->>C: 200 OK + C->>P: 还连接 B +``` + +### 两个层次的复用 + +#### HTTP/1.1 连接复用(Keep-Alive) + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + Note over C,S: TCP 连接(建一次) + C->>S: 请求1 + S->>C: 响应1 + C->>S: 请求2(同一个连接) + S->>C: 响应2 + C->>S: 请求3(同一个连接) + S->>C: 响应3 +``` + +**但同一时刻只能有一个请求在跑**(队头阻塞)。浏览器默认开 6 个连接弥补: + +``` +连接1: 请求1 → 响应1 (串行) +连接2: 请求2 → 响应2 (串行) +连接3: 请求3 → 响应3 (串行) +连接4: 请求4 → 响应4 (串行) +连接5: 请求5 → 响应5 (串行) +连接6: 请求6 → 响应6 (串行) + ↑ + 6 个连接之间是并行的 +``` + +#### HTTP/2 多路复用 + +一个 TCP 连接上同时跑多个请求/响应,通过帧编号区分: + +``` +TCP 连接(就一个) +├── 请求1 的第1帧 ──┐ +├── 请求2 的第1帧 ──┼── 混在一起发,到了对端自动分拣 +├── 请求1 的第2帧 ──┤ +├── 请求3 的第1帧 ──┤ +├── 请求2 的第2帧 ──┘ +``` + +不再需要多个 TCP 连接来并行,一根连接全部搞定。 + +### 关键配置参数 + +| 参数 | 说明 | 过大 | 过小 | +|------|------|------|------| +| 最大连接数 | 池子最多同时借出多少个 | 内存和 fd 浪费 | 请求排队等连接 | +| 最小空闲 | 池子里至少保留多少个待命 | 空闲资源浪费 | 高并发时来不及建连 | +| 连接超时 | 借不到连接最多等多久 | 请求长时间挂起 | 高峰期大量失败 | +| 空闲回收 | 超过多久没用就释放 | 占着不用浪费资源 | 频繁重建连接 | + +--- + +## 代码示例 + +### Python requests + +```python +from requests.adapters import HTTPAdapter + +adapter = HTTPAdapter( + pool_connections=10, # 连接到不同目标服务器的池子数 + pool_maxsize=20, # 每个池子最多保持 20 个连接 +) +session.mount('https://', adapter) +``` + +### Go + +```go +client := &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, // 最大空闲连接 + MaxIdleConnsPerHost: 10, // 每个主机最多保留 10 个空闲连接 + IdleConnTimeout: 90 * time.Second, + }, +} +``` + +### Java Apache HttpClient + +```java +PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); +cm.setMaxTotal(200); // 总连接池大小 +cm.setDefaultMaxPerRoute(20); // 每个目标服务器最多 20 个连接 +``` + +--- + +## 常见陷阱 + +!!! warning "连接池打满导致延迟飙升" + 池子大小为 10,但同时来了 50 个请求,40 个在排队等连接。解决方案:合理调大池子,或用 HTTP/2 多路复用减少对连接数的需求。 + +!!! warning "僵尸连接" + 服务器端已经关闭了连接,但客户端不知道,下次复用时发现连接已死 → 报错。解决方案:开启连接健康检查、设置合理的空闲超时、使用 TCP keepalive。 + +--- + +## 练习题 + +??? question "连接池大小设为多少合适?" + ??? success "答案" + 没有万能值,取决于场景。一般经验值:**最大连接数 = 服务端能承受的并发数**,**空闲连接数 = 平均并发请求数的 20-50%**。高 IO 等待的场景(如调外部 API)可以设大一些,CPU 密集的场景设小一些。监控连接池的借出/归还速率来动态调整。 + +??? question "HTTP/2 的多路复用还需要连接池吗?" + ??? success "答案" + 连接池仍然有用。HTTP/2 解决了单个连接上的并行问题,但连接池提供了**容错**(一个连接挂了可以切到另一个)和**负载均衡**(多连接分散压力)的能力。此外,Go 的 `http.Transport` 本身就是连接池实现。 diff --git a/docs/networking/cors.md b/docs/networking/cors.md new file mode 100644 index 0000000..ac9ff1d --- /dev/null +++ b/docs/networking/cors.md @@ -0,0 +1,235 @@ +# 跨域(CORS) + +!!! note "浏览器的同源策略看的是'协议+域名+端口'三元组,不是'同一台机器'——只要有一个不一样就跨域" + +--- + +## 核心概念 + +1. **同源策略** — 浏览器的安全基石,不同源的请求一律先拦住 +2. **Origin 三元组** — 协议 + 域名 + 端口,三者必须完全一致才算同源 +3. **CORS** — 跨源资源共享,服务器通过响应头告诉浏览器"允许谁来访问" +4. **预检请求** — 非简单请求浏览器先发 OPTIONS 探路,确认安全后才发真正请求 + +--- + +## 详解 + +### 同源的定义 + +浏览器判断跨域的标准不是"是不是同一台机器",而是: + +``` +源(Origin)= 协议 + 域名 + 端口 + +https://example.com:443 + ↑ ↑ ↑ +协议 域名 端口 + +三个都一样 → 同源 → 没问题 +任何一个不一样 → 跨域 → 浏览器拦截 +``` + +### 常见的"同一台主机但跨域"情况 + +#### 1. 协议不同 + +``` +页面:http://localhost:3000 (HTTP) +请求:https://localhost:3000 (HTTPS) + +http ≠ https → 跨域! +``` + +#### 2. 端口不同 + +``` +页面:http://localhost:3000 (前端) +请求:http://localhost:8080 (后端 API) + +3000 ≠ 8080 → 跨域! +``` + +前后端分离开发时极其常见。 + +#### 3. 域名写法不同 + +``` +页面:http://example.com +请求:http://www.example.com + +example.com ≠ www.example.com → 跨域! +``` + +#### 4. 子域名不同 + +``` +页面:https://app.example.com (前端) +请求:https://api.example.com (后端 API) + +app.example.com ≠ api.example.com → 跨域! +``` + +### 图示 + +```mermaid +graph TD + subgraph 同源 ✅ + A1["https://example.com:443"] + A2["https://example.com:443"] + A1 --- A2 + end + subgraph 跨域 ❌ + B1["https://app.example.com:443"] + B2["https://api.example.com:443"] + B1 --- B2 + end + subgraph 跨域 ❌ + C1["http://localhost:3000"] + C2["http://localhost:8080"] + C1 --- C2 + end + subgraph 跨域 ❌ + D1["http://example.com:80"] + D2["https://example.com:443"] + D1 --- D2 + end +``` + +### 为什么要拦截? + +```mermaid +sequenceDiagram + participant You as 你 + participant Bank as bank.com + participant Evil as evil.com + Note over You,Bank: 你登录了银行网站,浏览器存了 cookie + You->>Evil: 访问恶意网站 + Evil->>Bank: 请求 bank.com/api/transfer(冒充你转账) + Note over Bank: 浏览器同源策略拦截! + Note over Bank: evil.com 的请求被阻止 😅 +``` + +**同源策略 = 浏览器替你守门,不同源的请求一律先拦住。** + +### 解决方案 + +#### 方案 1:后端设置 CORS 响应头(最常用) + +```python +# Python Flask +from flask import Flask +from flask_cors import CORS + +app = Flask(__name__) +CORS(app) # 允许所有来源 + +# 或精确控制 +CORS(app, origins=["https://app.example.com"]) +``` + +```go +// Go 标准库 +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "https://app.example.com") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} +``` + +```nginx +# Nginx +add_header Access-Control-Allow-Origin "https://app.example.com"; +add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; +add_header Access-Control-Allow-Headers "Content-Type, Authorization"; +``` + +#### 方案 2:前端开发代理 + +```javascript +// Vite 开发服务器配置 +export default { + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + } + } + } +} +``` + +浏览器看到的请求始终在 `localhost:3000`,同源,不触发跨域。 + +#### 方案 3:Nginx 反向代理(生产环境) + +```nginx +server { + listen 80; + server_name app.example.com; + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://localhost:8080; + # 对浏览器:请求的是 app.example.com/api → 同源 ✅ + } +} +``` + +### 简单请求 vs 预检请求 + +| 类型 | 条件 | 行为 | +|------|------|------| +| 简单请求 | GET/POST/HEAD + 简单 Header | 浏览器直接发,看响应头判断 | +| 预检请求 | PUT/DELETE/自定义 Header 等 | 浏览器先发 OPTIONS 探路,确认后再发 | + +预检请求流程: + +```mermaid +sequenceDiagram + participant C as 浏览器 + participant S as 服务器 + C->>S: OPTIONS /api/data(预检) + S->>C: 204 No Content + Note over S: Access-Control-Allow-Origin: https://app.example.com + Note over S: Access-Control-Allow-Methods: PUT, POST, GET + C->>C: 确认允许 + C->>S: PUT /api/data(真正的请求) + S->>C: 200 OK +``` + +网络面板里一个请求出现两次——**第一次是 OPTIONS 预检**,不是 bug。 + +--- + +## 常见陷阱 + +!!! warning "Access-Control-Allow-Origin 不能设为 * 同时带 Cookie" + 如果请求携带 Cookie(`withCredentials: true`),`Access-Control-Allow-Origin` 不能是 `*`,必须指定具体来源。否则浏览器会拒绝。 + +!!! warning "忘记处理 OPTIONS 预检请求" + 很多后端框架默认不处理 OPTIONS 方法,导致预检请求返回 405 Method Not Allowed。需要显式添加 OPTIONS 处理逻辑。 + +--- + +## 练习题 + +??? question "为什么 WebSocket 不受同源策略限制?" + ??? success "答案" + WebSocket 连接一旦建立,就是全双工通信,不受 CORS 限制。但建立连接的**握手阶段**(HTTP Upgrade 请求)仍然受同源策略影响。所以 WebSocket 的安全性依赖服务器端的身份验证,而不是浏览器的同源策略。 + +??? question "如果后端设置了 Access-Control-Allow-Origin: *,前端 withCredentials: true 会怎样?" + ??? success "答案" + 浏览器会拒绝。规范明确要求:当请求携带凭据(Cookie、HTTP Auth)时,`Access-Control-Allow-Origin` 不能是通配符 `*`,必须是具体的源。这是为了防止任意网站都能通过你浏览器里的 Cookie 访问受保护资源。 diff --git a/docs/networking/domain-sharding.md b/docs/networking/domain-sharding.md new file mode 100644 index 0000000..3bd3002 --- /dev/null +++ b/docs/networking/domain-sharding.md @@ -0,0 +1,142 @@ +# 域名分片(Domain Sharding) + +!!! note "HTTP/1.1 时代的聪明 hack——用多个子域名骗浏览器多开连接,绕过同域名 6 连接限制" + +--- + +## 核心概念 + +1. **浏览器连接限制** — 同一域名最多同时开 6 个 TCP 连接 +2. **域名分片** — 用多个子域名指向同一服务器,每个域名都能开 6 个连接 +3. **缓存碎片化** — 不同域名的缓存独立,可能重复下载相同资源 +4. **HTTP/2 淘汰** — 多路复用从根本上解决了这个问题,域名分片已无必要 + +--- + +## 详解 + +### 它要解决什么问题 + +浏览器对**同一域名**最多同时开 6 个 TCP 连接,这是硬限制。一个页面要加载 30 个资源: + +``` +只用一个域名 www.example.com: + +连接1: req1 → req7 → req13 → req19 → req25 +连接2: req2 → req8 → req14 → req20 → req26 +连接3: req3 → req9 → req15 → req21 → req27 +连接4: req4 → req10 → req16 → req22 → req28 +连接5: req5 → req11 → req17 → req23 → req29 +连接6: req6 → req12 → req18 → req24 → req30 + +30 个资源 ÷ 6 个连接 = 每个连接排队 5 轮 +``` + +每个连接上是**串行的**,请求排队等响应。 + +### 域名分片的思路 + +用多个子域名指向同一台服务器: + +```mermaid +graph TD + A[www.example.com] --> D[源站服务器] + B[img1.example.com] --> D + C[img2.example.com] --> D + E[img3.example.com] --> D + + F[每个域名 6 个连接] --> G[3 × 6 = 18 个并行连接] +``` + +``` +3 个子域名 × 6 个连接 = 18 个并行连接 +18 个连接同时下载,30 个资源几轮就完了 +``` + +### 分片前后对比 + +**不分片(1 个域名,6 个连接):** + +``` +连接1: ████ req1 ████ req7 ████ req13 ████ req19 ████ req25 +连接2: ████ req2 ████ req8 ████ req14 ████ req20 ████ req26 +连接3: ████ req3 ████ req9 ████ req15 ████ req21 ████ req27 +连接4: ████ req4 ████ req10 ████ req16 ████ req22 ████ req28 +连接5: ████ req5 ████ req11 ████ req17 ████ req23 ████ req29 +连接6: ████ req6 ████ req12 ████ req18 ████ req24 ████ req30 + ↑ 很长 +``` + +**分片后(3 个域名,18 个连接):** + +``` +img1 连接1-6: ████ 1 ████ 7 ████ 13 ████ 19 ████ 25 +img2 连接1-6: ████ 2 ████ 8 ████ 14 ████ 20 ████ 26 +img3 连接1-6: ████ 3 ████ 9 ████ 15 ████ 21 ████ 27 + ↑ 快多了 +``` + +### 实现方式 + +前端 HTML 里只需把资源 URL 的域名换掉: + +```html + + + + + +``` + +服务器端不用改动,所有子域名 DNS 解析到同一台机器就行。 + +### 域名分片的代价 + +| 代价 | 说明 | +|------|------| +| **DNS 解析开销** | 每个子域名都要单独解析,5-50ms | +| **TLS 握手开销** | 每个新域名要单独做 TLS 握手 | +| **缓存碎片化** | 不同域名的缓存独立,不能共享,可能重复下载 | + +### 为什么现在不需要了 + +| | HTTP/1.1 + 域名分片 | HTTP/2 | +|---|---|---| +| 并行方式 | 多个 TCP 连接 | 一个 TCP 连接内多路复用 | +| 开 30 个连接 | 需要 5 个子域名骗浏览器 | 不需要,一根连接全搞定 | +| 额外开销 | 每个子域名 DNS + TLS | 只有一次 | +| 缓存 | 每个子域名独立缓存 | 统一缓存 | + +--- + +## 演进路线 + +```mermaid +graph TD + A[HTTP/1.0 无 Keep-Alive
每个请求一个连接] --> B[HTTP/1.1 + Keep-Alive
一个连接串行复用] + B --> C[域名分片
多域名骗浏览器多开连接] + C --> D[HTTP/2 多路复用
一个连接真正并行] + D --> E[HTTP/3 QUIC
连 TCP 队头阻塞都干掉] +``` + +--- + +## 常见陷阱 + +!!! warning "不要在 HTTP/2 上使用域名分片" + HTTP/2 的多路复用让域名分片变成了负优化——多了 DNS 和 TLS 开销,还破坏了缓存统一性。如果你的站点已经上了 HTTP/2,应该去掉域名分片。 + +!!! warning "分片数量不是越多越好" + 3-4 个子域名通常就够了。过多子域名意味着更多 DNS 查询和 TLS 握手,反而拖慢加载速度。 + +--- + +## 练习题 + +??? question "如果一个网站同时有 HTTP/1.1 和 HTTP/2 的用户,域名分片还有意义吗?" + ??? success "答案" + 需要权衡。HTTP/1.1 用户能从分片中获益(更多并行连接),但 HTTP/2 用户会受额外 DNS/TLS 开销之苦。建议直接升级到 HTTP/2 全站覆盖,而不是为少数 HTTP/1.1 用户保留分片。 + +??? question "浏览器对同一域名 6 个连接的限制是怎么来的?" + ??? success "答案" + 这是浏览器厂商的**经验值**,不是协议规定。2000 年代初期,6 个连接被认为是在"并行效率"和"服务器负载"之间的平衡点。这个限制在 HTTP/2 时代已经过时,但浏览器仍然保持兼容。 diff --git a/docs/networking/go-build-strip.md b/docs/networking/go-build-strip.md new file mode 100644 index 0000000..e81acf7 --- /dev/null +++ b/docs/networking/go-build-strip.md @@ -0,0 +1,172 @@ +# Go 编译标志 -s + +!!! note "剥离调试信息让二进制缩小 20-30%,生产发布必备,但代价是调试和排查困难" + +--- + +## 核心概念 + +1. **`-s` 标志** — 剥离符号表(函数名、变量名等调试符号) +2. **`-w` 标志** — 剥离 DWARF 调试信息(gdb/lldb 所需的详细信息) +3. **`-s -w` 组合** — 体积最小化,最适合发布部署 +4. **`-ldflags`** — 可在剥离的同时注入编译时变量(版本号、构建时间等) + +--- + +## 详解 + +### 基本用法 + +```bash +# 普通编译 +go build -o myapp main.go + +# 剥离符号表 +go build -s -o myapp main.go + +# 剥离调试信息 +go build -w -o myapp main.go + +# 同时剥离(最常用) +go build -s -w -o myapp main.go +``` + +### -s 和 -w 的区别 + +| 标志 | 剥离内容 | 效果 | +|------|----------|------| +| `-s` | 符号表 | 体积减小,panic 时看不到完整函数路径 | +| `-w` | DWARF 调试信息 | 体积减小,无法用 gdb 调试 | +| `-s -w` | 全部剥离 | 体积最小,最适合发布 | + +### 实际体积对比 + +```bash +go build -o app_normal main.go +go build -s -o app_stripped main.go + +ls -lh app_normal app_stripped +# -rwxr-xr-x 1 user staff 1.8M app_normal +# -rwxr-xr-x 1 user staff 1.2M app_stripped +# 省了约 30% 的体积 +``` + +实际节省取决于程序复杂度,一般能**减少 20%-30%**。 + +### panic 输出变化 + +**有符号表(正常编译):** + +``` +goroutine 1 [running]: +main.processData(0x10a0c00, 0x3) + /home/user/app/main.go:42 +0x1a5 ← 完整路径 +main.main() + /home/user/app/main.go:15 +0x89 +``` + +**剥离符号表后(-s):** + +``` +goroutine 1 [running]: +main.processData(0x10a0c00, 0x3) + main.go:42 +0x1a5 ← 只有文件名,没有完整路径 +main.main() + main.go:15 +0x89 +``` + +函数名还在,但完整源码路径丢失。出问题时调试稍麻烦。 + +### 对调试工具的影响 + +```bash +# 有 DWARF +gdb ./app_normal +(gdb) break main.processData # ✅ 可以设断点 + +# 剥离 DWARF +gdb ./app_stripped +(gdb) break main.processData # ❌ 找不到符号 +``` + +```bash +# go tool pprof +# 正常二进制 +(pprof) top + flat flat% sum% cum cum% + 1.2s 45.2% 45.2% 1.2s 45.2% runtime.mallocgc ← 有函数名 + +# 剥离后 +(pprof) top + flat flat% sum% cum cum% + 1.2s 45.2% 45.2% 1.2s 45.2% 0x4a3b20 ← 只有地址 +``` + +### 生产环境最佳实践 + +结合 `-ldflags` 在剥离的同时注入构建信息: + +```makefile +# Makefile +VERSION := $(shell git describe --tags --always) +BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') + +# 发布版 +build-release: + go build -ldflags="-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" \ + -o bin/app . +``` + +```go +// main.go +var ( + Version string + BuildTime string +) + +func main() { + fmt.Printf("Version: %s, Built: %s\n", Version, BuildTime) +} +``` + +### 使用场景决策 + +| 场景 | 用 `-s -w`? | 原因 | +|------|-------------|------| +| 生产环境部署 | ✅ | 体积小,不需要调试符号 | +| CI/CD 构建产物 | ✅ | 减少传输和存储开销 | +| 嵌入式 / IoT | ✅ | 存储空间宝贵 | +| 本地开发调试 | ❌ | 需要完整调试信息 | +| 性能分析(pprof) | ❌ | 需要符号表才能看到函数名 | +| 排查线上 panic | ❌ | 需要完整路径定位代码 | + +### 语言对比 + +| 语言 | 类似功能 | +|------|----------| +| C/C++ | `strip` 命令、`-s` 编译选项 | +| Rust | `strip = true` in Cargo.toml | +| Java | ProGuard / R8 混淆和精简 | +| Go | `-s -w` 编译标志 | + +--- + +## 常见陷阱 + +!!! warning "线上出了 panic 但看不到完整堆栈" + 生产二进制用了 `-s`,panic 堆栈只有文件名没有完整路径。解决方案:保留符号表文件(`-w` 但不用 `-s`),或者用 `go tool addr2line` 通过地址反查行号。 + +!!! warning "pprof 数据无法解读" + 剥离符号表后 pprof 只显示地址,无法定位到具体函数。线上排查性能问题时非常痛苦。建议线上至少保留 `-w`(只剥离 DWARF),不要用 `-s`。 + +--- + +## 练习题 + +??? question "为什么 -s 能省 20-30% 的体积?符号表占那么多吗?" + ??? success "答案" + 符号表 + DWARF 调试信息确实可以占很大比例。DWARF 包含了每个函数的行号信息、变量类型、作用域等详细调试数据,对于大型程序来说体积相当可观。Go 的 runtime 本身也有很多符号信息。实际比例取决于程序复杂度和依赖数量。 + +??? question "如果我用了 -s -w 但线上出了问题,怎么排查?" + ??? success "答案" + (1)如果有构建时生成的 debug info 备份,可以用 `go tool addr2line` 反查;(2)可以通过 `go tool pprof` 的 raw 模式看到地址;(3)如果保留了 `-w`(没用 `-s`),panic 堆栈仍然有函数名,只是没有行号;(4)最稳妥的做法是生产环境用 `-w` 但不用 `-s`,保留符号表。 diff --git a/docs/networking/http-connection-cost.md b/docs/networking/http-connection-cost.md new file mode 100644 index 0000000..a216bcf --- /dev/null +++ b/docs/networking/http-connection-cost.md @@ -0,0 +1,106 @@ +# HTTP 连接资源消耗 + +!!! note "理解单个 HTTP 连接的真实资源开销,才能在百万并发场景下做出正确的架构决策" + +--- + +## 核心概念 + +1. **TCP 连接内存** — 内核为每个连接分配发送/接收缓冲区和控制块,约 3-10 KB +2. **线程开销** — 传统模型每连接一个线程(~1 MB),协程模型仅需几 KB +3. **TLS 会话** — HTTPS 连接额外消耗 50-100 KB 内存 +4. **文件描述符** — 每个连接占一个 fd,受系统 ulimit 限制 +5. **临时端口** — 客户端端口上限 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)** | + +### 形象比喻 + +**传统线程模型 — 泳池:** + +``` +每个连接 = 一个人占一条泳道(1 米宽) +100 万人 → 需要 100 万条泳道 → 1000 公里宽的泳池 +物理上不可能 🤯 +``` + +**异步/事件驱动模型 — 叫号系统:** + +``` +人来了不占泳道,坐在池边等叫号 +有数据来了才临时下水 +每个人只占一把椅子(几 KB) +100 万人 → 100 万把椅子 → 大概几百 MB ✅ +``` + +**高速公路收费站:** + +| 模型 | 比喻 | 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 缓冲区大小 | + +--- + +## 常见陷阱 + +!!! warning "误区:连接池越大越好" + 池子里每个连接都占内存和 fd。配得合理比配得大更重要。连接池打满时新请求排队,延迟飙升。 + +!!! warning "别忽视 TLS 开销" + HTTPS 场景下 TLS 会话缓存每个连接 50-100 KB,百万并发下可能吃掉 50 GB 内存。考虑在 CDN 边缘终止 TLS。 + +--- + +## 练习题 + +??? question "Go 的 goroutine 初始栈大小是多少?和 Java 线程相比如何?" + ??? success "答案" + Go goroutine 初始栈大小为 **2 KB**(可动态增长到 1 GB),Java 线程默认栈大小为 **1 MB**。这意味着在相同内存下,Go 能创建的并发单元数量是 Java 的约 500 倍。 + +??? question "为什么操作系统对客户端临时端口有限制?" + ??? success "答案" + 客户端端口是 16 位整数,理论上限 65535。每个 TCP 连接需要一个唯一的四元组(源IP:源端口:目标IP:目标端口),如果一个客户端对同一服务器发起大量连接,端口很快耗尽。解决方案包括使用多 IP、连接池复用、HTTP/2 多路复用等。 diff --git a/docs/networking/http-handshake.md b/docs/networking/http-handshake.md new file mode 100644 index 0000000..f4bac61 --- /dev/null +++ b/docs/networking/http-handshake.md @@ -0,0 +1,133 @@ +# HTTP 握手 + +!!! note "HTTP 本身没有应用层握手,但底层的 TCP 和 TLS 各自有独立的握手流程" + +--- + +## 核心概念 + +1. **TCP 三次握手** — 所有 HTTP 连接建立前的基础,客户端与服务器确认彼此收发能力 +2. **TLS 握手** — HTTPS 场景下在 TCP 之上完成密钥协商和证书验证 +3. **HTTP/2 SETTINGS 交换** — 应用层的"半个握手",双方协商连接参数 +4. **QUIC 握手** — HTTP/3 将传输层和加密层合二为一,一次握手同时完成建连和加密 + +--- + +## 详解 + +### TCP 三次握手 + +每次 HTTP 通信之前,底层都要先完成 TCP 三次握手: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + C->>S: SYN(我要连接) + S->>C: SYN-ACK(收到,我也准备好了) + C->>S: ACK(确认,开始传数据) +``` + +| 阶段 | 方向 | 标志位 | 作用 | +|------|------|--------|------| +| 第一次 | 客户端 → 服务器 | SYN | 客户端发起连接请求 | +| 第二次 | 服务器 → 客户端 | SYN-ACK | 服务器确认并回应 | +| 第三次 | 客户端 → 服务器 | ACK | 客户端确认,连接建立 | + +三次握手的本质是**双方各确认一次收发能力**,确保连接可靠。 + +### TLS 握手(HTTPS) + +在 TCP 握手之后、HTTP 请求之前完成: + +| TLS 版本 | 握手耗时 | 说明 | +|----------|----------|------| +| TLS 1.2 | 2-RTT(两个来回) | 需要两次往返完成密钥协商 | +| TLS 1.3 | 1-RTT(一个来回) | 优化了握手流程 | +| TLS 1.3 0-RTT | 0-RTT | 恢复会话时无需额外往返 | + +TLS 握手完成的事: + +- **证书验证**:确认服务器身份,防止中间人攻击 +- **密钥协商**:生成对称加密密钥 +- **加密套件确定**:双方商定使用的加密算法 + +### HTTP/1.0 和 HTTP/1.1 + +这两个版本**没有应用层握手**——TCP 连上、TLS 握完,直接发 HTTP 请求。 + +```http +# TCP 连接建立后,直接发请求 +GET /index.html HTTP/1.1 +Host: example.com +``` + +### HTTP/2 的 SETTINGS 交换 + +HTTP/2 在连接建立后有一个应用层初始化过程: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + Note over C,S: TCP + TLS 握手完成 + C->>S: SETTINGS 帧(我的能力参数) + S->>C: SETTINGS 帧(我的能力参数) + C->>S: SETTINGS ACK + S->>C: SETTINGS ACK + Note over C,S: 可以开始发送请求了 +``` + +双方通过 SETTINGS 帧交换参数,如最大并发流数、窗口大小等。这不算严格的"握手",但起到类似的协商作用。 + +### HTTP/3 (QUIC) + +HTTP/3 基于 QUIC 协议,QUIC 运行在 UDP 之上: + +```mermaid +graph LR + A[QUIC] --> B[内置 TLS 1.3] + A --> C[内置可靠传输] + A --> D[多路复用] + A --> E[0-RTT 恢复] +``` + +- QUIC 把传输层握手和 TLS 1.3 握手**合二为一** +- 首次连接只需 **1-RTT** +- 恢复连接可达 **0-RTT** +- 解决了 TCP 的队头阻塞问题(单个流丢包不影响其他流) + +--- + +## 各层握手对比 + +| 层级 | 协议 | 有握手? | 耗时 | +|------|------|----------|------| +| 传输层 | TCP | ✅ 三次握手 | 1-RTT | +| 安全层 | TLS 1.2 | ✅ 密钥协商 | 2-RTT | +| 安全层 | TLS 1.3 | ✅ 密钥协商 | 1-RTT | +| 应用层 | HTTP/1.x | ❌ 无 | — | +| 应用层 | HTTP/2 | 半个(SETTINGS) | ~0.5-1 RTT | +| 传输+安全 | HTTP/3 (QUIC) | ✅ 内置 TLS 1.3 | 1-RTT / 0-RTT | + +--- + +## 常见陷阱 + +!!! warning "误区:认为 HTTPS 只多了一次握手" + HTTPS 的额外开销不仅是 TLS 握手本身。如果 CDN、代理层多,TLS 握手可能要打好几次。理解每一层的握手才能优化延迟。 + +!!! warning "0-RTT 并非免费" + TLS 1.3 的 0-RTT 恢复虽然快,但存在**重放攻击**风险。0-RTT 发送的数据不能保证幂等性,因此不适合做敏感操作(如支付)。 + +--- + +## 练习题 + +??? question "为什么 TCP 握手是三次而不是两次?" + ??? success "答案" + 两次握手无法防止**已失效的连接请求**到达服务器。如果客户端的旧 SYN 延迟到达,服务器会误以为是新请求并建立连接,白白浪费资源。三次握手让客户端有最后一次确认的机会,丢弃过期请求。 + +??? question "HTTP/2 的 SETTINGS 交换和 TCP 三次握手有什么区别?" + ??? success "答案" + TCP 三次握手发生在传输层,目的是建立可靠的字节流通道。HTTP/2 的 SETTINGS 交换发生在应用层,目的是协商 HTTP 协议参数(如最大并发流、窗口大小等)。TCP 握手解决的是"能不能通信",SETTINGS 解决的是"怎么高效通信"。 diff --git a/docs/networking/http-keepalive.md b/docs/networking/http-keepalive.md new file mode 100644 index 0000000..6c0ad8b --- /dev/null +++ b/docs/networking/http-keepalive.md @@ -0,0 +1,137 @@ +# HTTP Keep-Alive + +!!! note "建一次连接,连续发多个请求,不用每次重新握手——HTTP/1.1 最重要的默认优化" + +--- + +## 核心概念 + +1. **Keep-Alive** — TCP 连接建立后不立即关闭,允许在同一连接上发送多个 HTTP 请求 +2. **Connection 头** — 通过 `Connection: Keep-Alive` 或 `Connection: close` 控制连接生命周期 +3. **队头阻塞** — Keep-Alive 连接上的请求严格串行,后一个请求必须等前一个响应返回 +4. **演进路线** — 从无 Keep-Alive → Keep-Alive → HTTP/2 多路复用 → HTTP/3 QUIC + +--- + +## 详解 + +### 没有 Keep-Alive(HTTP/1.0) + +一个请求一个连接,用完即弃: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + loop 每个请求 + C->>S: TCP 三次握手 + C->>S: TLS 握手(HTTPS) + C->>S: GET /resource + S->>C: 200 OK + C->>S: TCP 四次挥手断开 + end +``` + +一个网页通常有几十个资源(HTML、CSS、JS、图片……),每个都走一遍握手流程。**光握手就比传数据花的时间还多。** + +### 有 Keep-Alive(HTTP/1.1 默认) + +建一次连接,连续发多个请求: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + Note over C,S: TCP 连接(就这一次) + C->>S: GET /index.html + S->>C: 200 OK + HTML + C->>S: GET /style.css(同一个连接) + S->>C: 200 OK + CSS + C->>S: GET /app.js(还是同一个) + S->>C: 200 OK + JS + C->>S: GET /logo.png + S->>C: 200 OK + PNG + Note over C,S: 全部搞完,最后才断开 +``` + +### 协议层面的协商 + +服务器通过响应头声明 Keep-Alive 参数: + +```http +HTTP/1.1 200 OK +Connection: Keep-Alive +Keep-Alive: timeout=5, max=100 +``` + +| 字段 | 含义 | +|------|------| +| `Connection: Keep-Alive` | 告诉客户端:这个连接可以复用 | +| `timeout=5` | 空闲超过 5 秒没新请求,关闭连接 | +| `max=100` | 单连接最多处理 100 个请求,之后强制关闭 | + +客户端也可以主动声明: + +```http +GET /page.html HTTP/1.1 +Host: example.com +Connection: Keep-Keep-Alive +``` + +不想复用时: + +```http +HTTP/1.1 200 OK +Connection: close +``` + +### 队头阻塞问题 + +Keep-Alive 解决了频繁握手,但引入了队头阻塞: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + C->>S: 请求1 + Note over C,S: 必须等响应1完全返回 + S->>C: 响应1 + C->>S: 请求2(才能开始) + S->>C: 响应2 + C->>S: 请求3 + S->>C: 响应3 +``` + +请求 2 必须等请求 1 的响应**完全返回**后才能发出。浏览器用**多开 6 个连接**来弥补——一条收银通道排队太慢,所以超市开了 6 个收银台,但每个收银台内部还是串行的。 + +### 演进路线 + +```mermaid +graph TD + A[HTTP/1.0 无 Keep-Alive
一请求一连接] -->|加 Connection 头| B[HTTP/1.1 默认 Keep-Alive
一个连接串行多个请求] + B -->|多开 6 个连接弥补| C[浏览器并行连接
缓解队头阻塞] + C -->|彻底重构| D[HTTP/2 多路复用
一个连接上真正并行] + D -->|连 TCP 队头阻塞都干掉| E[HTTP/3 QUIC
基于 UDP 的多路复用] +``` + +--- + +## 常见陷阱 + +!!! warning "空闲连接占用资源" + Keep-Alive 连接在空闲时仍然占内存和 fd。如果客户端长时间不发新请求,服务器应该主动关闭。合理设置 `timeout` 参数。 + +!!! warning "服务器已关连接但客户端不知道" + 服务器因超时关闭了连接,但客户端下次复用时才发现连接已死 → 报错。需要开启连接健康检查或设置合理的超时。 + +--- + +## 练习题 + +??? question "HTTP/1.1 默认 Keep-Alive,为什么浏览器还要开 6 个连接?" + ??? success "答案" + 因为 HTTP/1.1 的 Keep-Alive 存在队头阻塞——同一连接上的请求严格串行。开 6 个连接可以并行处理 6 个请求,大幅提升页面加载速度。HTTP/2 的多路复用解决了这个问题,一个连接就够了。 + +??? question "Keep-Alive 的 timeout 和 max 有什么区别?" + ??? success "答案" + `timeout` 控制的是**时间**——空闲多久后关闭连接。`max` 控制的是**请求数**——单连接处理多少个请求后关闭。两者取先触发的那个。例如 `timeout=5, max=100` 表示:空闲 5 秒或处理 100 个请求后关闭。 diff --git a/docs/networking/index.md b/docs/networking/index.md new file mode 100644 index 0000000..32757e6 --- /dev/null +++ b/docs/networking/index.md @@ -0,0 +1,19 @@ +# 计算机网络 + +!!! note "从 HTTP 协议到底层连接管理,覆盖面试和实战中的核心网络知识" + +--- + +## 文章目录 + +| 文章 | 核心内容 | +|------|----------| +| [HTTP 握手](http-handshake.md) | TCP 三次握手、TLS 握手、HTTP/2 与 HTTP/3 的连接建立 | +| [HTTP 连接资源消耗](http-connection-cost.md) | 单连接资源开销、百万并发的真实数字与应对策略 | +| [连接池复用](connection-pooling.md) | 预建连接、借还机制、HTTP/1.1 与 HTTP/2 的复用差异 | +| [HTTP Keep-Alive](http-keepalive.md) | Keep-Alive 原理、队头阻塞、演进路线 | +| [Keep-Alive 使用场景](keepalive-scenarios.md) | 何时启用、何时关闭、生产环境最佳实践 | +| [域名分片](domain-sharding.md) | HTTP/1.1 时代的 hack,HTTP/2 后的淘汰 | +| [CDN](cdn.md) | 边缘节点、缓存策略、加速原理与配置 | +| [跨域 CORS](cors.md) | 同源三元组、CORS 响应头、预检请求 | +| [Go 编译标志 -s](go-build-strip.md) | 剥离调试信息、体积优化、生产发布 | diff --git a/docs/networking/keepalive-scenarios.md b/docs/networking/keepalive-scenarios.md new file mode 100644 index 0000000..cb41e3e --- /dev/null +++ b/docs/networking/keepalive-scenarios.md @@ -0,0 +1,161 @@ +# Keep-Alive 使用场景 + +!!! note "99% 的场景都该用 Keep-Alive,关键是配好超时时间和最大请求数" + +--- + +## 核心概念 + +1. **高频请求** — 同一客户端短时间内多次请求同一服务器,Keep-Alive 省掉大量握手开销 +2. **高延迟网络** — 移动端/跨地域场景下,每次握手的延迟代价更高 +3. **资源受限** — 空闲连接太多时反而有害,需要缩短超时或关闭 Keep-Alive +4. **安全隔离** — 某些敏感场景需要每次请求走独立连接 + +--- + +## 详解 + +### 建议启用 Keep-Alive 的场景 + +#### 1. 高频请求同一服务器 + +```mermaid +graph LR + A[前端页面] -->|请求 30 个资源| B[CDN/源站] + C[微服务 A] -->|链式调用| D[微服务 B] + D -->|继续调用| E[微服务 C] +``` + +最常见的场景。前端页面加载、微服务间调用、API 连续请求等,几乎没有任何理由关掉 Keep-Alive。 + +#### 2. 移动端 / 高延迟网络 + +移动端网络延迟高,每次 TLS 握手可能要 100-300ms: + +| 场景 | 无 Keep-Alive | 有 Keep-Alive | +|------|--------------|---------------| +| 30 次请求 × 150ms 握手 | **4.5 秒**光握手 | **150ms**只握手一次 | + +**省了 4 秒多,体感天差地别。** + +#### 3. 短间隔批量请求 + +爬虫批量抓取、定时轮询、WebSocket 建立前的 HTTP 握手等,1000 个请求: + +| 方案 | 每个请求耗时 | 总耗时 | +|------|-------------|--------| +| 无 Keep-Alive | 60ms(50ms 握手 + 10ms 传输) | 60 秒 | +| 有 Keep-Alive | 10ms(0ms 握手 + 10ms 传输) | 10 秒 | + +#### 4. 内部服务间通信 + +微服务之间的 RPC 调用、数据库连接、Redis 连接等,连接池 + Keep-Alive 是标配。 + +### 不建议使用 Keep-Alive 的场景 + +#### 1. 一次性 / 低频请求 + +偶尔请求一次,几分钟甚至几小时才来一次: + +``` +用户一天登录一次,查一次个人信息就不用了 +每天凌晨跑一次定时任务,拉一次数据 +``` + +Keep-Alive 连接空等着白白占内存。不过现代服务器默认超时 5-15 秒会自动释放,实际影响不大。 + +#### 2. 连接数是瓶颈时 + +极端场景下,空闲连接太多反而有害: + +``` +10000 个空闲 Keep-Alive 连接 × 10KB = 100MB 内存 +文件描述符吃掉 10000 个 +新请求反而因为资源不够被拒绝 +``` + +这时候应该**缩短超时时间**,而不是关掉 Keep-Alive: + +```nginx +keepalive_timeout 5; # 空闲 5 秒就释放 +keepalive_requests 1000; # 单连接最多处理 1000 个请求就回收 +``` + +#### 3. 安全隔离 + +银行转账等敏感操作,每次操作用全新连接,确保上一次的上下文不会残留。多租户 SaaS 也要隔离,A 租户的连接不能被 B 租户复用。 + +#### 4. 大文件传输 + +下载 2GB 文件,一个连接独占十几分钟。如果连接池只有 10 个,其他请求全部排队等这个下载完。 + +```python +# 正确做法:下载用独立连接,业务用连接池 +download_session = requests.Session() +download_session.headers['Connection'] = 'close' # 下载完就关 + +api_session = requests.Session() # 默认 Keep-Alive +``` + +--- + +## 决策参考 + +| 条件 | Keep-Alive | Connection: close | +|------|-----------|-------------------| +| 同一客户端高频请求 | ✅ | | +| 请求间隔 < Keep-Alive 超时 | ✅ | | +| 移动端 / 高延迟网络 | ✅ | | +| 微服务内部通信 | ✅ | | +| 偶尔请求一次 | 无所谓(让服务器默认超时就好) | | +| 单次大文件下载 | | ✅ | +| 连接数已达上限 | 缩短超时 | | +| 安全隔离要求 | | ✅ | +| 公共 API 防滥用 | 缩短超时 + 限制次数 | | + +--- + +## 生产环境配置建议 + +### Nginx + +```nginx +# 后端代理(微服务间)—— 激进复用 +proxy_http_version 1.1; +proxy_set_header Connection ""; +keepalive_timeout 60s; +keepalive_requests 1000; + +# 面向公网 —— 适度保守 +keepalive_timeout 10s; +keepalive_requests 100; +``` + +### Java Tomcat + +```xml + +``` + +--- + +## 常见陷阱 + +!!! warning "公共 API 忘记限制连接数" + 不限制 Keep-Alive 超时和最大请求数,一个恶意客户端可以长期霸占连接。应该设置 `keepalive_timeout` 和 `keepalive_requests` 上限。 + +!!! warning "大文件和小请求混用同一连接池" + 大文件传输独占连接时间过长,导致小请求排队。应该分离:大文件用独立连接(`Connection: close`),小请求用连接池复用。 + +--- + +## 练习题 + +??? question "如果你的 API 网关同时服务移动端和 Web 端,Keep-Alive 配置应该怎么考虑?" + ??? success "答案" + 移动端延迟高,Keep-Alive 收益大,超时可以设长一些(如 30 秒)。Web 端延迟低但并发高,超时可以短一些(如 10 秒)。可以按来源区分配置,或者取一个折中值(10-15 秒),配合 `keepalive_requests` 限制单连接最大请求数。 + +??? question "为什么说 Keep-Alive 的 '99% 场景都该用'?那 1% 是什么?" + ??? success "答案" + 那 1% 主要是:(1)安全隔离要求极高的场景(如金融交易),每次连接必须独立;(2)连接数是硬瓶颈的极端高并发场景;(3)超大文件传输会独占连接。除此之外,默认开启是正确选择。 diff --git a/mkdocs.yml b/mkdocs.yml index eb6c285..4b83bc9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,6 +127,17 @@ nav: - 部署与 CI/CD: qiniu-cloud/deployment-cicd.md - 安全·认证·数据: qiniu-cloud/security-auth-data.md - 简历技术要点: qiniu-cloud/resume-tech-points.md + - 计算机网络: + - networking/index.md + - HTTP 握手: networking/http-handshake.md + - HTTP 连接资源消耗: networking/http-connection-cost.md + - 连接池复用: networking/connection-pooling.md + - HTTP Keep-Alive: networking/http-keepalive.md + - Keep-Alive 使用场景: networking/keepalive-scenarios.md + - 域名分片: networking/domain-sharding.md + - CDN: networking/cdn.md + - 跨域 CORS: networking/cors.md + - Go 编译标志 -s: networking/go-build-strip.md - AI: - ai/index.md - Skill 编写最佳实践: ai/skill-best-practices.md