持久連線與 Keep-Alive:讓 TCP 連線物盡其用
在 HTTP/1.0 時代,每個請求都需要新建 TCP 連線,這是巨大的效能浪費。HTTP/1.1 引入了持久連線(Persistent Connection),徹底改變了這個局面。本篇將深入探討持久連線的運作機制。
一、 為什麼需要持久連線?
1.1 TCP 連線的代價
建立一個 TCP 連線需要經過三向交握,這需要 1 個 RTT(往返時間):
如果是 HTTPS,還需要 TLS 握手,額外 1-2 個 RTT。
1.2 短連線的問題
HTTP/1.0 預設每個請求都開新連線:
問題:
| 問題 | 影響 |
|---|---|
| 重複握手 | 每次都要 1-3 RTT |
| TCP 慢啟動 | 每次都從低速開始 |
| 伺服器負擔 | 頻繁建立和關閉連線 |
| 埠號耗盡 | 高並發時可能耗盡 |
二、 持久連線的運作
2.1 HTTP/1.1 預設持久連線
HTTP/1.1 預設開啟持久連線,不用每次都重建:
2.2 Connection 標頭
控制連線行為的關鍵標頭:
http
# HTTP/1.1 預設保持連線,不需要明確指定
Connection: keep-alive # 通常可省略
# 明確要求關閉連線
Connection: close版本差異:
| 版本 | 預設行為 | 保持連線 | 關閉連線 |
|---|---|---|---|
| HTTP/1.0 | 短連線 | Connection: keep-alive | 不指定 |
| HTTP/1.1 | 持久連線 | 不指定 | Connection: close |
2.3 何時關閉連線?
連線會在以下情況關閉:
- 客戶端發送
Connection: close - 伺服器發送
Connection: close - 超時:閒置超過 Keep-Alive timeout
- 請求數量上限:達到 max requests
- 錯誤:發生不可恢復的錯誤
三、 Keep-Alive 配置
3.1 Keep-Alive 標頭
提供連線保持的參數資訊:
http
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5, max=100| 參數 | 說明 |
|---|---|
timeout | 閒置超時秒數 |
max | 此連線允許的最大請求數 |
3.2 Nginx 配置範例
nginx
http {
# 保持連線超時時間
keepalive_timeout 65;
# 單個連線最大請求數
keepalive_requests 100;
# 客戶端 Body 超時
client_body_timeout 60;
# 客戶端 Header 超時
client_header_timeout 60;
}3.3 Node.js 配置範例
javascript
const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello World");
});
// 設置超時(毫秒)
server.keepAliveTimeout = 5000; // 閒置超時
server.headersTimeout = 60000; // Headers 等待超時
server.timeout = 120000; // 總請求超時
server.listen(3000);四、 管線化(Pipelining)
4.1 什麼是管線化?
在同一個持久連線上,不等待回應就連續發送多個請求:
4.2 隊頭阻塞問題
管線化的致命缺陷:回應必須按順序返回
快的請求被慢的請求「堵住」了!
4.3 為什麼瀏覽器不使用管線化?
| 原因 | 說明 |
|---|---|
| 隊頭阻塞 | 一個慢請求阻塞所有後續 |
| 代理不支援 | 很多中間代理處理不正確 |
| 伺服器相容性 | 不是所有伺服器都支援 |
| 錯誤恢復困難 | 失敗時難以判斷哪個請求出問題 |
NOTE
由於這些問題,主流瀏覽器都停用了 HTTP/1.1 管線化。HTTP/2 用多路複用徹底解決了這個問題。
五、 連線池(Connection Pool)
5.1 瀏覽器的策略
由於管線化不可靠,瀏覽器選擇開多個連線:
瀏覽器連線限制:
| 瀏覽器 | 每個域名最大連線數 |
|---|---|
| Chrome | 6 |
| Firefox | 6 |
| Safari | 6 |
| Edge | 6 |
5.2 域名分片(Domain Sharding)
為了突破連線限制,有時會使用多個域名:
# 將資源分散到多個域名
static1.example.com → 6 個連線
static2.example.com → 6 個連線
static3.example.com → 6 個連線
總共 18 個並行連線!WARNING
這在 HTTP/1.1 時代是有效的優化,但在 HTTP/2 時代反而有害(因為無法使用單一連線的多路複用)。
六、 最佳實踐
6.1 伺服器端
nginx
# Nginx 推薦配置
http {
# 長一點的超時,適合大多數場景
keepalive_timeout 65;
# 足夠的請求數
keepalive_requests 1000;
# 對上游(後端服務)也使用 keepalive
upstream backend {
server 127.0.0.1:8080;
keepalive 32; # 保持 32 個空閒連線
}
}6.2 客戶端
javascript
// Node.js HTTP Agent 配置
const http = require("http");
const agent = new http.Agent({
keepAlive: true, // 啟用持久連線
keepAliveMsecs: 1000, // 閒置時每秒發送 TCP keep-alive
maxSockets: 100, // 每個主機最大連線數
maxFreeSockets: 10, // 最大空閒連線數
timeout: 60000, // 超時
});
http.get(
{
hostname: "example.com",
agent: agent,
},
(res) => {
// ...
}
);6.3 監控指標
應該監控的連線相關指標:
| 指標 | 說明 | 警戒線 |
|---|---|---|
| 活躍連線數 | 正在使用的連線 | 接近上限 |
| 空閒連線數 | 等待復用的連線 | 過高浪費記憶體 |
| 連線建立率 | 每秒新建連線數 | 過高表示 keep-alive 無效 |
| 平均連線復用次數 | 每個連線完成的請求數 | 過低需調整配置 |
七、 TCP Keep-Alive vs HTTP Keep-Alive
這是兩個完全不同的概念!
| 面向 | TCP Keep-Alive | HTTP Keep-Alive |
|---|---|---|
| 層級 | 傳輸層 | 應用層 |
| 用途 | 偵測死連線 | 復用連線 |
| 機制 | 發送探測封包 | Connection 標頭 |
| 預設 | 通常關閉 | HTTP/1.1 預設開啟 |
TCP Keep-Alive
# Linux 預設值
tcp_keepalive_time = 7200 # 閒置 2 小時後開始探測
tcp_keepalive_intvl = 75 # 每 75 秒探測一次
tcp_keepalive_probes = 9 # 失敗 9 次判定斷線HTTP Keep-Alive
http
Connection: keep-alive
Keep-Alive: timeout=5, max=100總結
| 概念 | 說明 |
|---|---|
| 持久連線 | HTTP/1.1 預設開啟,複用 TCP 連線 |
| Keep-Alive 標頭 | 控制連線超時和最大請求數 |
| 管線化 | 理論上可行,實際因隊頭阻塞問題被棄用 |
| 連線池 | 瀏覽器開多個連線繞過限制 |
| 域名分片 | HTTP/1.1 優化技巧,HTTP/2 時代已過時 |
> **核心要點**:持久連線減少了 TCP 握手開銷,但因為隊頭阻塞問題,HTTP/1.1 仍需開多個連線。HTTP/2 的多路複用才是終極解決方案。
進階挑戰
- 使用
netstat或ss命令觀察瀏覽器與網站建立了多少個 TCP 連線。 - 配置一個 Nginx 伺服器,設置不同的
keepalive_timeout值,觀察對效能的影響。 - 思考:為什麼 HTTP/2 只需要一個 TCP 連線,卻能達到比 HTTP/1.1 多個連線更好的效能?