@@ -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[边缘节点<br/>缓存1]
|
||||
E --> R[区域节点<br/>缓存2]
|
||||
R --> S[源站<br/>原始文件]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实际配置
|
||||
|
||||
### 阿里云 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,所以内容一定是新的。
|
||||
@@ -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` 本身就是连接池实现。
|
||||
@@ -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 访问受保护资源。
|
||||
@@ -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
|
||||
<!-- 以前 -->
|
||||
<img src="https://www.example.com/images/logo.png">
|
||||
|
||||
<!-- 分片后 -->
|
||||
<img src="https://img1.example.com/images/logo.png">
|
||||
```
|
||||
|
||||
服务器端不用改动,所有子域名 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<br/>每个请求一个连接] --> B[HTTP/1.1 + Keep-Alive<br/>一个连接串行复用]
|
||||
B --> C[域名分片<br/>多域名骗浏览器多开连接]
|
||||
C --> D[HTTP/2 多路复用<br/>一个连接真正并行]
|
||||
D --> E[HTTP/3 QUIC<br/>连 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 时代已经过时,但浏览器仍然保持兼容。
|
||||
@@ -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`,保留符号表。
|
||||
@@ -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 多路复用等。
|
||||
@@ -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 解决的是"怎么高效通信"。
|
||||
@@ -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<br/>一请求一连接] -->|加 Connection 头| B[HTTP/1.1 默认 Keep-Alive<br/>一个连接串行多个请求]
|
||||
B -->|多开 6 个连接弥补| C[浏览器并行连接<br/>缓解队头阻塞]
|
||||
C -->|彻底重构| D[HTTP/2 多路复用<br/>一个连接上真正并行]
|
||||
D -->|连 TCP 队头阻塞都干掉| E[HTTP/3 QUIC<br/>基于 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 个请求后关闭。
|
||||
@@ -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) | 剥离调试信息、体积优化、生产发布 |
|
||||
@@ -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
|
||||
<Connector keepAliveTimeout="20000"
|
||||
maxKeepAliveRequests="200" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
!!! 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)超大文件传输会独占连接。除此之外,默认开启是正确选择。
|
||||
+11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user