叙事密度:低 | 主语言:Python + Bash | Java/C 差异窗 | Vol 4·计算机网络
元数据卡
| 属性 | 内容 |
|---|---|
| 卷号 | Vol 4 — 计算机网络 |
| 章节 | 第14章:网络实战与性能优化 |
| 前置 | 第1章(分层模型)、第3章(TCP 深度)、第7章(DNS/CDN/负载均衡)、第10章(拥塞控制)、第13章(IPv6) |
| 后置 | —(本章是 Vol 4 最后一章) |
| 理论深度 | (2/5)—— 工具和实操为主,不要求推导 |
| Python 相关度 | ≈40%;重点是 Bash 工具链(curl/iperf/ss/tcpdump) |
| 核心技能 | 定位"慢"的根因、TCP 内核调参、HTTP 性能优化 |
| 代码量 | ~100 行 Python + 大量 Shell 命令 |
你在哪
"你从第1章一路走到了第13章——驿道分层模型、法术信标接口(Socket)、TCP、HTTP、TLS、域名解析法阵(DNS)、法术内容分发驿道(CDN)、魔力数据链路、IP 驿道层、拥塞控制、QUIC、驿道调试、IPv6。现在你将它们全部串联起来——从法师说'慢'到定位根因再到优化方案。这是驿道知识的实战收尾。"
从第1章到第13章,你走过了一张完整的驿道知识地图:
- 驿道分层模型、法术信标接口编程
- TCP 三次法力握手、滑动卷轴、拥塞控制
- HTTP/HTTPS、gRPC、法术信函推送(WebSocket)
- 域名解析法阵/法术内容分发驿道/负载均衡
- 魔力数据链路层(信标标识、驿站交换机、虚拟驿道)
- 驿道层(IPv4 法术信函分片、子网划分、传送门地址转换、驿道路由直觉)
- IPv6——从驿道坐标耗尽到自动地址分配
但这里有一个巨大的鸿沟:知道"TCP 拥塞控制用 CUBIC 三次弹簧术"和"当法师说慢时你能找到根因"之间,差了十条驿道。
这一章就是那条驿道。我们不学任何"新传送咒"。我们学怎么把之前所有知识当成排障工具箱,遇到驿道问题时打出正确的工具。
你的任务
学完本章,你应当:
- 面对"慢"的投诉,系统性地排查全链路:DNS → TCP 连接 → TLS 握手 → TTFB → 内容传输
- 使用 curl -w、httpstat 做请求分阶段计时
- 用 ss -ti 监控实时 TCP 连接拥塞窗口和 RTT
- 在 Linux 上调优 TCP 内核参数(缓冲区、窗口缩放、拥塞算法)
- 配置 HTTP 缓存策略和连接复用
- 用 iperf3 测量带宽
- 解释 CDN + 负载均衡在生产环境中的分工
遭遇战 → 获得技能
第1战:全链路分析——"用户说慢"到底慢在哪
问题: 法师发来魔力截图——"你们的传送阵加载要 8 秒"。你打开法师观测镜的驿道调试面板一看:
Name Status Time Waterfall
api.example.com 200 7.84s ████████████████████████████████7.84 秒。但你不知道这 7.84 秒花在哪里——是 DNS 解析用了 3 秒?TCP 连接用了 2 秒?TTFB 用了 4 秒?还是下载大文件用了 1 秒?
把 "请求时间" 分解成五个阶段:
DNS TCP TLS TTFB Content
──────┬────────┬────────┬────────┬──────────────┬─────→
时间: t1 t2 t3 t4 t5
t1 = DNS 解析(域名 → IP)
t2 = TCP 连接建立(三次握手)
t3 = TLS 握手(如有 HTTPS)
t4 = TTFB(发送请求 → 收到第一个字节)
t5 = 内容传输(收到剩余数据)用 curl -w 精确分解每个阶段:
# curl 的 -w 参数可以输出请求的各个时间指标
cat > curl-format.txt << 'EOF'
\n
time_namelookup: %{time_namelookup}s (DNS 解析)
time_connect: %{time_connect}s (TCP 连接建立)
time_appconnect: %{time_appconnect}s (TLS 握手)
time_pretransfer: %{time_pretransfer}s (开始发送请求)
time_starttransfer: %{time_starttransfer}s(TTFB)
└───────── %{time_total}s(总时间)
time_total: %{time_total}s
HTTP 状态码: %{http_code}
下载大小: %{size_download} 字节
speed_download: %{speed_download} B/s
EOF
curl -w "@curl-format.txt" -o /dev/null -s https://example.com输出类似:
time_namelookup: 0.021s (DNS: 21ms)
time_connect: 0.045s (TCP: 24ms → 0.045-0.021)
time_appconnect: 0.089s (TLS: 44ms → 0.089-0.045)
time_pretransfer: 0.089s
time_starttransfer: 0.312s (TTFB: 223ms → 0.312-0.089)
time_total: 0.428s
HTTP 状态码: 200不同阶段的问题诊断:
| 阶段 | 正常范围 | 如果很慢 | 可能原因 |
|---|---|---|---|
| DNS 解析 | < 50ms | > 500ms | DNS 服务器慢、递归查询链长、没有本地缓存 |
| TCP 连接 | < 100ms (同区域) | > 500ms | 物理距离远、中间设备加延迟、丢包导致 SYN 重传 |
| TLS 握手 | < 100ms (TLS 1.3) | > 500ms | TLS 1.2 完整握手(2-RTT)、证书链太长、OCSP 慢 |
| TTFB | < 200ms | > 1s | 服务端处理慢(数据库查询、模板渲染)、边缘缓存未命中 |
| 内容传输 | 取决于带宽 | 慢 | 带宽限制、拥塞控制窗口小、丢包率高 |
import subprocess, json, sys
def curl_perf_analysis(url: str, verbose: bool = False):
"""用 curl 做全链路性能分析"""
format_str = (
'{"dns":%{time_namelookup},"tcp":%{time_connect},'
'"tls":%{time_appconnect},"ttfb":%{time_starttransfer},'
'"total":%{time_total},"status":%{http_code},'
'"size":%{size_download},"speed":%{speed_download}}'
)
cmd = ["curl", "-w", format_str, "-o", "/dev/null", "-s", url]
if verbose:
cmd.insert(1, "-v")
result = subprocess.run(cmd, capture_output=True, text=True)
try:
data = json.loads(result.stdout)
except json.JSONDecodeError:
print(f"⚠️ curl 输出解析失败:\n{result.stdout}")
return
print(f"\n{'='*50}")
print(f"URL: {url}")
print(f"{'='*50}")
phases = [
("DNS 解析", data['dns']),
("TCP 连接", data['tcp'] - data['dns']),
("TLS 握手", data['tls'] - data['tcp']),
("TTFB", data['ttfb'] - data['tls']),
("内容传输", data['total'] - data['ttfb']),
]
total = data['total']
for name, duration in phases:
pct = (duration / total * 100) if total > 0 else 0
bar = '█' * int(pct / 2)
duration_ms = duration * 1000
print(f" {name:12s} {duration_ms:8.1f} ms ({pct:5.1f}%) {bar}")
print(f"{'─'*50}")
print(f" 总时间: {total*1000:8.1f} ms")
print(f" 状态码: {data['status']}")
print(f" 下载大小: {data['size']/1024:.1f} KB")
print(f" 下载速度: {data['speed']/1024:.1f} KB/s")
if __name__ == "__main__":
# 使用示例
url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
curl_perf_analysis(url)$ python curl_perf.py https://www.google.com
==================================================
URL: https://www.google.com
==================================================
DNS 解析 15.2 ms ( 5.0%)
TCP 连接 32.1 ms ( 10.6%)
TLS 握手 45.3 ms ( 14.9%)
TTFB 182.1 ms ( 60.0%) ████████████████████████████████
内容传输 29.3 ms ( 9.7%)
────────────────────────────────────────────────────
总时间: 303.9 ms看到这里你就该知道了:如果 TTFB 占了 60%+ 的总时间,问题不在网络,在服务端。如果 DNS 占了 60%,问题在 DNS 缓存或解析器。
第2战:DNS 优化——别让域名成为瓶颈
问题: curl 输出的域名解析法阵耗时 500ms。你在法师观测镜里第一次访问传送阵明显感觉到卡顿。怎么优化?
DNS 耗时的五个层次:
查询链: 浏览器缓存 → OS 缓存 → 递归解析器 → 根/TLD → 权威服务器
速度: ~0ms ~1ms ~20ms ~100ms ~40ms
↑ 几乎无成本 ↑ 主要耗时在这里优化策略(从最快到最慢):
策略 1:本地 DNS 缓存
# 安装本地 DNS 缓存代理
# Linux (dnsmasq 或 systemd-resolved)
sudo systemctl enable --now systemd-resolved
# macOS —— 内置 mDNSResponder 默认缓存
# Windows —— 默认缓存 86400 秒(可查 ipconfig /displaydns)策略 2:预解析(Prefetch/Preconnect)
<!-- HTML 中告诉浏览器提前解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="preconnect" href="https://api.example.com">
<!-- 更激进:提前建立连接并解析 -->
<link rel="preconnect" href="https://fonts.googleapis.com"
crossorigin>def analyze_preconnect_gain(dns_time_ms: int, tcp_time_ms: int,
requests: int):
"""估算预连接节省的时间"""
saved_per_request = dns_time_ms + tcp_time_ms
total_ms = saved_per_request * max(0, requests - 1)
print(f"每个请求节省: {saved_per_request} ms")
print(f"{requests} 个请求总计节省: {total_ms} ms")
print(f"相当于页面加载快了 {total_ms/1000:.1f} 秒")
analyze_preconnect_gain(200, 50, 10)
# 每个请求节省: 250 ms
# 10 个请求总计节省: 2250 ms
# 相当于页面加载快了 2.3 秒策略 3:CDN 任播 DNS
大型 CDN(Cloudflare、Akamai、AWS CloudFront)使用 Anycast DNS——同一个 DNS 服务器的 IP 地址在全球部署,你的 DNS 查询自动路由到最近的节点。
# 对比两个 DNS 解析器
echo "Google DNS:"
dig @8.8.8.8 example.com +stats | grep "Query time"
echo "Cloudflare DNS:"
dig @1.1.1.1 example.com +stats | grep "Query time"
echo "本地 DNS:"
dig example.com +stats | grep "Query time"每个查询减少 20-50ms。对单个页面不算多,但一个页面平均有 50-100 个 DNS 查询(CSS/JS/图片/字体/CDN 域名)。
第3战:TCP 连接建立 —— 减少握手开销
问题: TCP 三次法力握手至少 1 RTT。如果法师客户端在北京、信标塔在弗吉尼亚,RTT = 200ms,那三次魔力握手就干等了 200ms。加上 TLS 法力握手(TLS 1.2 又是 2 RTT),等连接建立就用掉 600ms 以上,还没发第一道法术信函。
优化策略:
策略 1:连接复用(Keep-Alive)
# 对比 keep-alive vs 短连接
echo "=== 禁用 Keep-Alive ==="
curl -w "总时间: %{time_total}s\n" \
-H "Connection: close" \
-o /dev/null -s https://example.com/file1
echo "=== 启用 Keep-Alive ==="
curl -w "总时间: %{time_total}s\n" \
-o /dev/null -s https://example.com/file1第二个请求使用同一个 TCP 连接,省掉 TCP + TLS 握手。
策略 2:TCP Fast Open(TFO,RFC 7413)
TFO 允许在 SYN 包中携带数据——第一次握手时就直接发送应用数据,省掉 1 RTT。
# 检查 TFO 是否启用
cat /proc/sys/net/ipv4/tcp_fastopen
# 0 = 禁用, 1 = 客户端, 2 = 服务端, 3 = 客户端+服务端
# 启用(需要 root)
echo 3 | sudo tee /proc/sys/net/ipv4/tcp_fastopen
# 永久配置
echo "net.ipv4.tcp_fastopen = 3" | sudo tee -a /etc/sysctl.confTFO 的限制:
- 只适用于重连场景(第一次连接不能带数据——需要服务器发 TFO cookie)
- 中间设备(防火墙、NAT)有时会丢弃带数据的 SYN 包
- Linux 3.7+,macOS 支持,Windows 10 1607+
策略 3:减少物理距离(从根本解决)
有时候最快的优化就是把服务器搬到离用户更近的地方——CDN 做这件事。
第4战:TLS 握手优化——HTTPS 不再该是性能负担
问题: 你做了 HTTPS 封印迁移,但法师抱怨"你们传送阵加密后变慢了"。实际上,TLS 1.2 完整法力握手需要 2 RTT 的额外延迟。
TLS 1.2 完整握手:
客户端 服务端
│ │
│── ClientHello ──────────────→│
│← ServerHello + Certificate ←│ 1 RTT
│← ServerHelloDone ←│
│ │
│── ClientKeyExchange ────────→│
│── ChangeCipherSpec ─────────→│ 1 RTT
│── Finished ────────────────→│
│← ChangeCipherSpec ←─────────│
│← Finished ←────────────────│
│ │
│===== HTTP 请求开始 ===========│TLS 1.3 握手(1 RTT):
客户端 服务端
│── ClientHello ──────────────→│
│ (+ key_share) │ (一次收到全部)
│← ServerHello ←──────────────│ 1 RTT
│← EncryptedExtensions ←─────│
│← Certificate ←─────────────│
│← Finished ←────────────────│
│── Finished ────────────────→│
│ │
│===== HTTP 请求开始 ===========│
│(实际上在 1 RTT 时就已加密) │0-RTT(TLS 1.3 会话恢复):
客户端(之前连过) 服务端
│── ClientHello ──────────────→│
│ 0-RTT Key + Early Data │ 发送 HTTP 请求!
│← ServerHello ←──────────────│ (接收+处理请求)
│← ... ──────────────────────│# 对比 TLS 1.2 vs 1.3 的手耗时
echo "=== TLS 1.2 ==="
curl --tlsv1.2 --tls-max 1.2 \
-w "TLS握手: %{time_appconnect}s\n总时间: %{time_total}s\n" \
-o /dev/null -s https://www.cloudflare.com
echo "=== TLS 1.3 ==="
curl --tlsv1.3 \
-w "TLS握手: %{time_appconnect}s\n总时间: %{time_total}s\n" \
-o /dev/null -s https://www.cloudflare.comTLS 优化清单:
- 升级到 TLS 1.3——最主要的一步,从 2 RTT 降到 1 RTT
- 启用会话复用(Session Resumption)——服务端缓存会话票据(Session Ticket),重连时 0-RTT
- OCSP Stapling——服务端自己拿着证书状态,不让客户端自己去 OCSP 服务器查
- 证书链优化——减少中间证书数量,不在握手时发 4 层证书链
- 裁减密码套件——只留最快的几个(AES-GCM / ChaCha20-Poly1305),移除老旧套件
# 检查服务器 TLS 配置
# 用 testssl.sh 做全面评估
docker run --rm drwetter/testssl.sh \
--quiet --color 0 \
https://example.com
# 或快速检查密码套件和证书链
openssl s_client -connect example.com:443 \
-tls1_3 -servername example.com \
< /dev/null 2>/dev/null | grep -E "^(Certificate chain|SSL handshake|Cipher)"Java 差异窗:Java 8 默认用 TLS 1.2,不支持 TLS 1.3。Java 11+ 默认 TLS 1.3。如果你的 Tomcat/Jetty 在 Java 8 上跑 HTTPS,在
-Dhttps.protocols=TLSv1.2之外,升级到 Java 11 直接把性能提升一个档次。
第5战:TTFB——服务端才是性能黑洞
问题: curl 显示 TTFB = 3.2 秒。驿道只用了 100ms(域名解析 + 三次法力握手 + 封印握手加起来不过 100ms),剩下的 3.1 秒全花在信标塔端。你的驿道优化做得再好,也救不了慢到爆的法术服务端。
TTFB 漫长的常见原因:
| 原因 | 特征 | 排查方式 |
|---|---|---|
| 数据库慢查询 | TTFB 和查询数量相关 | 开启慢查询日志 |
| 上游 API 慢 | 服务端也在等别人的网络 | 在服务端加 tracing |
| 模板渲染慢 | 页面大、模板复杂 | 优化 SSR,或加 CDN 缓存 |
| 磁盘 I/O | 文件读取/日志写入阻塞 | iostat / strace |
| 没有启用缓存 | 每次请求都重新计算 | 加缓存层(Redis/Memcached/CDN) |
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse
class SlowHandler(BaseHTTPRequestHandler):
"""模拟一个有性能问题的服务端"""
def do_GET(self):
path = urlparse(self.path).path
if path == "/fast":
# 快速响应
self._respond(200, b'{"status":"ok"}')
elif path == "/slow-db":
# 模拟慢查询——500ms
time.sleep(0.5)
self._respond(200, b'{"data":"from db"}')
elif path == "/slow-template":
# 模拟模板渲染——200ms
time.sleep(0.2)
self._respond(200, b'<html><body>...rendered...</body></html>')
elif path == "/slow-all":
# 一切都很慢
time.sleep(1.0)
time.sleep(0.3)
self._respond(200, b'{"finally":"done"}')
else:
self._respond(404, b'Not Found')
def _respond(self, code: int, body: bytes):
self.send_response(code)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def start_test_server(port: int = 8888):
server = HTTPServer(('127.0.0.1', port), SlowHandler)
print(f"测试服务器运行在 http://127.0.0.1:{port}")
print(" 在另一个终端运行:")
print(f" curl -w '@curl-format.txt' http://127.0.0.1:{port}/slow-db")
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__ == "__main__":
# 用这个 start_test_server 来模拟慢响应
# 新建一个终端,用 curl 观察 TTFB 的区别
print("这个脚本不能直接服务器运行——参考上面的 HTTP 服务器例子")核心认知:当 TTFB >> DNS + TCP + TLS 的总和时,你的问题不在网络层。别调 TCP 参数了,去查数据库。
第6战:TCP 内核调优——从 sysctl 开始
问题: 你的信标塔驿道容量 10Gbps,但从新加坡的法师连接平均吞吐只有 200 Mbps。你在 iperf3 测试中也看到了——驿道容量远低于链路容量。这通常是 TCP 法阵核心参数的问题。
BDP(带宽-延迟积)
BDP = 带宽 × RTT
例子:
新加坡 → 弗吉尼亚: RTT ≈ 200ms, 带宽 1Gbps
BDP = (1 × 10⁹ bps) × 0.2s = 200,000,000 bit ≈ 24 MB如果 TCP 接收窗口 < BDP,带宽利用就受限于窗口大小。这就是"带宽够用但速度起不来"的根本原因。
Linux TCP 缓冲区调优
# 查看当前 TCP 缓冲区设置
sysctl net.ipv4.tcp_rmem # 接收缓冲区: min default max
sysctl net.ipv4.tcp_wmem # 发送缓冲区: min default max
sysctl net.ipv4.tcp_mem # TCP 总内存: low pressure high
# 典型输出:
# net.ipv4.tcp_rmem = 4096 131072 6291456
# (4KB min) (128KB default) (6MB max)
# net.ipv4.tcp_wmem = 4096 16384 4194304对高 BDP 链路的优化推荐:
# 增大缓冲区应对高 BDP
cat >> /etc/sysctl.d/99-network-performance.conf << 'EOF'
# 接收缓冲区:min=64KB, default=256KB, max=16MB
net.ipv4.tcp_rmem = 65536 262144 16777216
# 发送缓冲区:min=64KB, default=256KB, max=16MB
net.ipv4.tcp_wmem = 65536 262144 16777216
# TCP 窗口缩放:启用(Linux 默认就是 1)
net.ipv4.tcp_window_scaling = 1
# 增大初始拥塞窗口(10 包 → 可以到 64 包,视场景而定)
net.ipv4.tcp_slow_start_after_idle = 0
# 启用 TCP 选择性确认(SACK)
net.ipv4.tcp_sack = 1
# 启用 Forward Acknowledgement
net.ipv4.tcp_fack = 1
# 增大 TCP 连接队列
net.core.somaxconn = 65535
EOF
# 立即生效
sysctl --system理解 tcp_rmem 的机制
def explain_tcp_buffer(bdp_bytes: int,
default_buffer: int = 131072):
"""解释 BDP 和缓冲区的关系"""
bandwidth_gbps = bdp_bytes * 8 / 0.2 / 1e9 # 假设 RTT=200ms
print(f"BDP: {bdp_bytes / 1024 / 1024:.1f} MB")
print(f"对应带宽 @200ms RTT: {bandwidth_gbps:.2f} Gbps")
print(f"当前默认接收缓冲区: {default_buffer / 1024:.0f} KB")
if default_buffer < bdp_bytes:
ratio = bdp_bytes / default_buffer
print(f"\n⚠️ 缓冲区不足!只有 BDP 的 1/{ratio:.0f}")
print(f" 最大理论吞吐: {default_buffer / bdp_bytes * bandwidth_gbps:.2f} Gbps")
print(f" 解决方案: 增大 tcp_rmem 的默认值或 max 值")
else:
print("✅ 缓冲区充足")
# 例子:1Gbps @200ms RTT
bdp_1g_200ms = int(1e9 * 0.2 / 8) # 25,000,000 bytes
explain_tcp_buffer(bdp_1g_200ms)输出:
BDP: 23.8 MB
对应带宽 @200ms RTT: 1.00 Gbps
当前默认接收缓冲区: 128 KB
⚠️ 缓冲区不足!只有 BDP 的 1/186
最大理论吞吐: 0.01 Gbps
解决方案: 增大 tcp_rmem 的默认值或 max 值拥塞控制算法选择
# 查看当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# → cubic(默认)
# 查看可用算法
sysctl net.ipv4.tcp_available_congestion_control
# → cubic reno
# 启用 BBR(需要内核 4.9+)
modprobe tcp_bbr
echo "net.ipv4.tcp_congestion_control = bbr" >> /etc/sysctl.conf
# BBR + fq qdisc 搭配使用(BBR 需要 pacing)
echo "net.core.default_qdisc = fq" >> /etc/sysctl.conf
sysctl -pdef congestion_algorithm_guide():
"""拥塞控制算法选择指南"""
algos = {
"cubic": {
"场景": "通用服务器、长肥网络(Linux 默认)",
"优点": "大带宽链路表现好、三次函数比 Reno 平滑",
"缺点": "对随机丢包敏感、bufferbloat 环境下 RTT 高",
},
"bbr": {
"场景": "无线/移动网络、丢包率 > 0.1% 的环境、视频流媒体",
"优点": "不依赖丢包信号、低延迟、高吞吐",
"缺点": "与 CUBIC 共存时可能不公平、新算法(仍在演进)",
},
"bbr3": {
"场景": "BBR 的第三代改进版(2024+ 内核)",
"优点": "改善了 BBR 在长 RTT 和 CUBIC 共存场景的表现",
"缺点": "仍较新,需要内核 6.x+",
},
}
for name, info in algos.items():
print(f"\n{name.upper()}")
for k, v in info.items():
print(f" {k}: {v}")
congestion_algorithm_guide()initcwnd —— 初始拥塞窗口
RFC 6928 将初始拥塞窗口从 1 或 2 增大到 10 MSS(约 14KB)。有些场景下可以更大:
# 调整某条路由的 initcwnd
# 查看当前路由的 initcwnd
ip route show | head -1
# 设置 initcwnd = 30(约 42KB——适合首次访问加速)
sudo ip route change \
default via 192.168.1.1 dev eth0 \
initcwnd 30
# 永久保留(需要加到网络配置脚本中)initcwnd 大小的选择:
| initcwnd | 发送量(MSS=1460) | 推荐场景 |
|---|---|---|
| 10 | ~14 KB | RFC 6928 标准值(所有场景) |
| 20 | ~29 KB | 小文件多、RTT 高的场景 |
| 30 | ~43 KB | CDN、静态资源 |
| 64 | ~93 KB | 大文件下载,但注意拥塞风险 |
注意:initcwnd 不是越大越好。在低速链路(家用 ADSL)上,过大的 initcwnd 会立即撑爆缓冲区,导致丢包和性能下降。生产环境应该用 iperf3 加不同 initcwnd 对比测试。
第7战:HTTP 优化——减少请求数量,压缩传输体积
问题: 你的传送阵 TTFB 只有 200ms,但整体加载时间 6 秒——因为要下载 50 个法术资源(样式卷轴、法术脚本、魔导图片、咒语字体),每个都要建立连接(或等待前一个完成)。
Keep-Alive 和连接池
import socket, ssl, time, threading
from http.client import HTTPConnection, HTTPSConnection
class ConnectionPool:
"""简单的 HTTP 连接池——复用 TCP 连接"""
def __init__(self, host: str, port: int = 443, use_ssl: bool = True,
pool_size: int = 4):
self.host = host
self.port = port
self.use_ssl = use_ssl
self._pool = []
self._lock = threading.Lock()
self._pool_size = pool_size
def _create_conn(self):
if self.use_ssl:
conn = HTTPSConnection(self.host, self.port, timeout=5)
else:
conn = HTTPConnection(self.host, self.port, timeout=5)
conn.connect()
return conn
def request(self, method: str, path: str) -> bytes:
"""从池子里拿一个连接,发请求"""
conn = None
with self._lock:
if self._pool:
conn = self._pool.pop()
if conn is None:
conn = self._create_conn()
try:
conn.request(method, path)
resp = conn.getresponse()
data = resp.read()
# 放回池子
with self._lock:
if len(self._pool) < self._pool_size:
self._pool.append(conn)
else:
conn.close()
return data
except Exception:
conn.close()
raise
# 测试:连接池 vs 每次新建连接
def benchmark_without_pool(host: str, path: str, count: int = 10):
"""无连接池:每次请求新建连接"""
times = []
for _ in range(count):
t0 = time.time()
conn = HTTPSConnection(host, 443, timeout=5)
conn.request("GET", path)
resp = conn.getresponse()
_ = resp.read()
conn.close()
times.append((time.time() - t0) * 1000)
return times
def benchmark_with_pool(host: str, path: str, count: int = 10):
"""有连接池:4 个连接复用"""
pool = ConnectionPool(host, 443, pool_size=4)
times = []
for _ in range(count):
t0 = time.time()
_ = pool.request("GET", path)
times.append((time.time() - t0) * 1000)
return times
if __name__ == "__main__":
HOST = "example.com"
PATH = "/"
print("=== 无连接池(10 次请求)===")
t1 = benchmark_without_pool(HOST, PATH, 10)
print(f" 平均: {sum(t1)/len(t1):.1f} ms")
print(f" 第一次: {t1[0]:.1f} ms, 后续: {sum(t1[1:])/len(t1[1:]):.1f} ms")
print("\n=== 有连接池(10 次请求)===")
t2 = benchmark_with_pool(HOST, PATH, 10)
print(f" 平均: {sum(t2)/len(t2):.1f} ms")
print(f" 第一次: {t2[0]:.1f} ms, 后续: {sum(t2[1:])/len(t2[1:]):.1f} ms")HTTP/2 多路复用
HTTP/1.1 的队头阻塞问题:一个连接上一个请求没完成,其他请求就要等着。HTTP/2 用一个连接同时传输多个流:
# 对比 HTTP/1.1 vs HTTP/2
echo "=== HTTP/1.1 ==="
curl --http1.1 -w "总时间: %{time_total}s\n" \
-o /dev/null -s "https://http2.akamai.com/demo"
echo "=== HTTP/2 ==="
curl --http2 -w "总时间: %{time_total}s\n" \
-o /dev/null -s "https://http2.akamai.com/demo"
# 用 httpstat 做可视化
pip install httpstat
httpstat https://example.com资源压缩
# 检查服务器是否启用了压缩
curl -H "Accept-Encoding: gzip, br" \
-o /dev/null \
-w "下载大小: %{size_download} 字节\n压缩比: %{size_download} / %{size_request}\n" \
-s https://example.com没有 gzip/Brotli: 下载大小: 25273 字节 启用 Brotli: 下载大小: 5430 字节 — 压缩比 4.7×!
缓存策略
# 检查响应头的缓存策略
curl -I https://example.com/static/app.js
# 理想输出:
# Cache-Control: public, max-age=31536000, immutable
# ETag: "abc123def456"
# 对比:有缓存 vs 无缓存
echo "=== 无缓存(首次访问)==="
curl -w "总时间: %{time_total}s\n大小: %{size_download} 字节\n" \
-o /dev/null -s https://cdn.example.com/app.js
echo "=== 有缓存(条件请求)==="
curl -w "总时间: %{time_total}s\n大小: %{size_download} 字节\n" \
-H "If-None-Match: \"abc123def456\"" \
-o /dev/null -s https://cdn.example.com/app.js
# 有 ETag 匹配时返回 304 Not Modified,0 字节内容缓存策略推荐:
| 资源类型 | Cache-Control | 说明 |
|---|---|---|
版本化静态文件(app.v1.js) | public, max-age=31536000, immutable | 一年不变,浏览器直接读磁盘 |
| 非版本化 CSS/图片 | public, max-age=86400 | 缓存一天 |
| HTML 页面 | no-cache, must-revalidate | 每次验证 ETag |
| API 响应 | private, max-age=60 或 no-cache | 动态内容,客户端按需决定 |
预加载(Preload/Prefetch/Preconnect)
<!-- preconnect:提前建立连接(最重,尽早做) -->
<link rel="preconnect" href="https://api.example.com">
<!-- dns-prefetch:仅做 DNS 解析(轻量,资源不够时做) -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- preload:立即下载关键资源 -->
<link rel="preload" href="/critical-font.woff2" as="font" crossorigin>
<!-- prefetch:空闲时预加载下一页会用到的资源 -->
<link rel="prefetch" href="/next-page.js">
<!-- preconnect 比 dns-prefetch 强更多,但消耗也更大 -->def resource_hint_benchmark():
"""不同预加载策略的资源消耗对比"""
hints = {
"dns-prefetch": {"bandwidth": 0, "cpu": "~0", "connection": False},
"preconnect": {"bandwidth": 0, "cpu": "~0", "connection": True,
"note": "建立 TCP+TLS"},
"preload": {"bandwidth": "最高", "cpu": "中", "connection": True,
"note": "强制浏览器立即下载"},
"prefetch": {"bandwidth": "中", "cpu": "低", "connection": False,
"note": "空闲时才下载"},
}
print(f"{'Hint':16s} {'带宽':8s} {'CPU':8s} {'连接':10s} 备注")
print("-" * 60)
for name, info in hints.items():
conn = "是" if info["connection"] else "否"
note = info.get("note", "")
print(f"{name:16s} {str(info['bandwidth']):8s} "
f"{str(info['cpu']):8s} {conn:10s} {note}")
resource_hint_benchmark()第8战:CDN 与边缘计算——把内容推给用户
问题: 信标塔在北京。法师在新加坡 —— RTT = 200ms。法师在南美 —— RTT = 300ms。不管你怎么优化 TCP 参数,魔力传送延迟的极限在那里:法术光速再快也得绕地脉三分之一圈。
CDN(内容分发网络) 的核心思想很简单——把内容缓存到离用户最近的节点。
[用户东京] ───→ [CDN 东京节点] ←──→ [源站弗吉尼亚]
↓ ↓
磁盘缓存命中 (2ms) 缓存未命中才回源 (200ms)三种缓存策略对比:
| 策略 | TTFB | 命中率 | 内容新鲜度 | 成本 |
|---|---|---|---|---|
| 全量缓存 | 2-10ms (同城节点) | 高 | 低(定期更新) | 低 |
| 边缘动态加速 | 10-30ms | 动态路由优化 | 实时 | 高 |
| 分层缓存(L1/L2) | 2-20ms | 极高 | TTL 控制 | 中 |
def cdn_simulation(cache_hit_ttfb_ms: int, cache_miss_ttfb_ms: int,
cache_hit_rate: float, total_requests: int = 10000):
"""模拟 CDN 性能:缓存命中 vs 未命中"""
hit_count = int(total_requests * cache_hit_rate)
miss_count = total_requests - hit_count
total_time = (hit_count * cache_hit_ttfb_ms +
miss_count * cache_miss_ttfb_ms)
avg_ttfb = total_time / total_requests
print(f"总请求: {total_requests}")
print(f"缓存命中率: {cache_hit_rate*100:.0f}%")
print(f" 命中单次 TTFB: {cache_hit_ttfb_ms}ms")
print(f" 未命中单次 TTFB: {cache_miss_ttfb_ms}ms")
print(f" 平均 TTFB: {avg_ttfb:.1f}ms")
# 无 CDN 对比
print(f"\n无 CDN(直接回源): {cache_miss_ttfb_ms}ms")
print(f"CDN 加速倍率: {cache_miss_ttfb_ms / avg_ttfb:.1f}x")
print("=== 典型 CDN 场景 ===")
cdn_simulation(cache_hit_ttfb_ms=5, cache_miss_ttfb_ms=200,
cache_hit_rate=0.95)输出:
=== 典型 CDN 场景 ===
总请求: 10000
缓存命中率: 95%
命中单次 TTFB: 5ms
未命中单次 TTFB: 200ms
平均 TTFB: 14.8ms
无 CDN(直接回源): 200ms
CDN 加速倍率: 13.5xCDN 的源站保护机制:
- 缓存未命中才回源 — 大量请求被 CDN 截住,源站只处理 5-20% 的请求
- 请求限制 —— CDN 节点可以限制每个客户端每秒的请求数(Rate Limiting)
- WAF 过滤 —— 在边缘层拦截 SQL 注入、XSS 等攻击
- DDoS 吸收 — CDN 分布式架构可以吸收几百 Gbps 的攻击流量
第9战:负载均衡——把流量均匀分配
问题: 一座信标塔的处理能力有上限(比如 5000 每秒法术处理)。你的驿道用法增长到 20000 每秒法术处理。加信标塔很简单,但怎么让魔力流量平均分配到 4 座信标塔?
三种负载均衡方案,从简单到复杂:
方案 1:DNS 轮询(最简单,最弱)
# 同一个域名返回多个 IP——浏览器随机选一个
dig A api.example.com +short
# 203.0.113.1
# 203.0.113.2
# 203.0.113.3
# 203.0.113.4问题:DNS 缓存导致负载不均衡、无法处理服务器宕机(DNS TTL 期间宕机了流量还是往它打)。
方案 2:四层负载均衡(LVS,IPVS)
工作在传输层(TCP/UDP),只负责按 IP + 端口转发包,不改包内容。
# 用 Linux IPVS 做一个简单的四层负载均衡
# 安装 ipvsadm
sudo apt install ipvsadm
# 创建一个虚拟服务(VIP: 203.0.113.100:80)
sudo ipvsadm -A -t 203.0.113.100:80 -s rr # rr = round-robin
# 添加真实服务器
sudo ipvsadm -a -t 203.0.113.100:80 -r 10.0.0.1:80 -m # NAT 模式
sudo ipvsadm -a -t 203.0.113.100:80 -r 10.0.0.2:80 -m
sudo ipvsadm -a -t 203.0.113.100:80 -r 10.0.0.3:80 -m
# 查看状态
sudo ipvsadm -ln --stats
# IP Virtual Server version 1.2.1
# Prot LocalAddress:Port Conns InPkts OutPkts InBytes OutBytes
# TCP 203.0.113.100:80 24567 1245678 2345678 1.8G 3.2G
# -> 10.0.0.1:80 8198 415632 781234 600M 1.0G
# -> 10.0.0.2:80 8345 422567 789012 610M 1.1G
# -> 10.0.0.3:80 8024 401479 741432 590M 1.1GLVS 三种模式:
| 模式 | 缩写 | 回包路径 | 性能 | 场景 |
|---|---|---|---|---|
| NAT | VS/NAT | 必须经过 LB | 中 | 简单、所有 OS 兼容 |
| 直接路由 | VS/DR | 直接回客户端 | 高 | 需要 VIP 配置在 RS 上 |
| 隧道 | VS/TUN | 封装回客户端 | 非常高 | 跨网段服务器 |
方案 3:七层负载均衡(NGINX/HAProxy)
工作在应用层(HTTP),可以基于 URL、Cookie、Header 做路由。
# NGINX 七层负载均衡配置
upstream backend {
# 加权轮询:更强机器加权更高
server 10.0.0.1:8080 weight=3;
server 10.0.0.2:8080 weight=2;
server 10.0.0.3:8080 weight=1;
server 10.0.0.4:8080 backup; # 备用,其他全挂了才启用
# 保活连接池
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 超时设置
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 10s;
# 缓存上游响应
proxy_cache my_cache;
proxy_cache_valid 200 60s;
proxy_cache_use_stale error timeout;
}
# 健康检查(NGINX Plus 或使用 upstream check 模块)
# location /health { proxy_pass http://backend; }
}def load_balancing_strategies():
"""抽象负载均衡算法的表现"""
strategies = {
"round-robin": {
"机制": "轮流分配请求",
"公平性": "请求数公平,但不考虑服务器负载",
"适用": "同配置服务器,无状态服务",
},
"least-connections": {
"机制": "分配给当前活动连接数最少的服务器",
"公平性": "更好——考虑到正在处理的请求",
"适用": "请求处理时间不均匀的场景",
},
"ip-hash": {
"机制": "对客户端 IP 做 hash,同一 IP 分配到同一台服务器",
"公平性": "不均衡(少数大 IP 可能压到一台)",
"适用": "需要 session 保持的场景(不如用 Redis session)",
},
"weighted": {
"机制": "每台服务器一个权重,权重高的分配更多请求",
"公平性": "管理员自定义",
"适用": "异构服务器配置",
},
}
for name, info in strategies.items():
print(f"\n{name}")
for k, v in info.items():
print(f" {k}: {v}")
load_balancing_strategies()第10战:测量工具——你的眼睛和耳朵
问题: 你觉得驿道慢,但没数据支撑。没有测量,就没有优化。以下是驿道排障工具箱。
iperf3 —— 带宽测量
# 服务端
iperf3 -s -p 5201
# 客户端(测量上传带宽)
iperf3 -c <server-ip> -p 5201 -t 30
# 客户端(同时测试 TCP + UDP)
iperf3 -c <server-ip> -p 5201 -t 30 -l 64K
iperf3 -c <server-ip> -p 5201 -t 30 -u -b 1G
# 反向测试(测量下载带宽)
iperf3 -c <server-ip> -p 5201 -t 30 -R
# 并行流(多 TCP 连接叠加带宽)
iperf3 -c <server-ip> -p 5201 -t 30 -P 4import subprocess, json
def run_iperf3(server: str, duration: int = 10, reverse: bool = False,
parallel: int = 1):
"""运行 iperf3 测试并解析 JSON 输出"""
cmd = [
"iperf3", "-c", server, "-t", str(duration),
"--json"
]
if reverse:
cmd.append("-R")
if parallel > 1:
cmd.extend(["-P", str(parallel)])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=duration+10)
data = json.loads(result.stdout)
# 解析测试摘要
end = data.get("end", {})
streams = end.get("sum_received", end.get("sum_sent", {}))
if streams:
bits_per_second = streams["bits_per_second"]
mbps = bits_per_second / 1_000_000
retransmits = streams.get("retransmits", 0)
print(f"iperf3 → {server}")
print(f" 带宽: {mbps:.1f} Mbps")
print(f" 重传: {retransmits}")
print(f" 总数据: {streams.get('bytes', 0)/1024/1024:.1f} MB")
else:
print("⚠️ 无有效数据。服务器运行了吗?")
except FileNotFoundError:
print("请先安装 iperf3: apt install iperf3")
except subprocess.TimeoutExpired:
print("测试超时——检查连接或修改防火墙")
except json.JSONDecodeError as e:
print(f"解析输出失败: {e}")
if __name__ == "__main__":
import sys
server = sys.argv[1] if len(sys.argv) > 1 else "localhost"
run_iperf3(server, 10)ss -ti —— TCP 连接实时状态
# 查看所有 TCP 连接,显示 TCP 信息
ss -ti
# 只看特定的端口
ss -ti 'dport = :443 or sport = :443'
# 只看拥塞窗口在变化的连接
ss -ti | grep -E "cwnd|ssthresh"
# 按 RTT 排序
ss -ti | grep -oP 'rtt:\K[\d.]+' | sort -rn | head -5
# 输出解读:
# cwnd:10 ← 当前拥塞窗口(10 个 MSS)
# rtt:42.3 ← 当前 RTT(毫秒)
# ssthresh:30 ← 慢启动阈值
# cubic ← 使用的拥塞控制算法nload / iftop —— 实时流量监控
# nload — 简单,按接口显示带宽
nload eth0
# 显示:入站速率、出站速率、平均值、峰值
# iftop — 按连接显示流量(类似 top)
sudo iftop -i eth0 -n -P
# -n 不解析 DNS(更快)
# -P 显示端口
# 关注点:
# 1. 哪个 IP 占用最多带宽?
# 2. 哪个端口流量最大?
# 3. 峰值什么时候出现?常见陷阱:网络调优实战中的大坑
坑 1:Window Scaling 被中间设备破坏
你调大了 tcp_rmem 到 16MB,但实际通量还是只有几百 KB。用 ss -ti 看,接收窗口显示只有 64KB。
原因:路径上的某个防火墙或 NAT 设备把 TCP 选项中的 Window Scale 因子清零了。TCP 握手时的 Window Scale 选项(让窗口能超过 65,535 字节)被中间设备修改或者丢弃。
# 检查是否启用了 Window Scaling
ss -ti | grep -oP 'wscale:\K\d+'
# 正常应该显示 wscale:7(对应 128 倍 = 最大窗口约 8MB)
# 如果没显示或显示 0 —— 被中间设备破坏了
# 抓包确认
sudo tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0' \
-c 5 -X | grep -A2 "wscale"怎么解决:你没法控制中间设备。你能做的是:
- 联系网络管理员确认中间设备没有修改 TCP 选项
- 如果必须经过特定设备,考虑 VPN 封装绕过
- 使用 QUIC(基于 UDP,不受 TCP 中间设备影响)
坑 2:initcwnd 太大导致拥塞崩溃
你看到一篇博客说"initcwnd=64 让我的网站快了一倍",你就把服务器的 initcwnd 改成了 64。然后你的用户报告在一些网络下页面打不开或极度缓慢。
原因:initcwnd=64 意味着连接建立后第一次发送 64 个 MSS(约 93KB)。在低速链路(3G 网络、共享宿舍带宽)上,这个量可能超过链路缓冲区可以承受的范围,所有包都被缓冲或丢弃,导致 TCP 进入长时间的慢启动恢复。
def initcwnd_risk_estimate(rtt_ms: int, bandwidth_mbps: float,
initcwnd: int, mss: int = 1460):
"""估算 initcwnd 是否安全"""
initial_bytes = initcwnd * mss
initial_bits = initial_bytes * 8
# 链路在 1 RTT 内可以传送多少
capacity_bits = bandwidth_mbps * 1_000_000 * (rtt_ms / 1000)
capacity_bytes = capacity_bits / 8
ratio = initial_bytes / capacity_bytes if capacity_bytes > 0 else float('inf')
print(f"initcwnd={initcwnd} → 首次发送: {initial_bytes/1024:.1f} KB")
print(f"链路容量 @{bandwidth_mbps}Mbps/{rtt_ms}ms: {capacity_bytes/1024:.1f} KB")
print(f"占用比例: {ratio*100:.0f}%")
if ratio > 0.3:
print(f"⚠️ 风险高!首次发送超过容量的 {ratio*100:.0f}%")
print(f" 可能触发拥塞,建议减小 initcwnd")
elif ratio > 0.1:
print(f"⚡ 中等风险 ({ratio*100:.0f}%),建议在目标网络测试")
else:
print(f"✅ 安全")
print("=== 不同场景的 initcwnd 风险评估 ===")
initcwnd_risk_estimate(200, 10, 64) # 3G 网络
initcwnd_risk_estimate(50, 100, 30) # 企业 WiFi
initcwnd_risk_estimate(10, 1000, 64) # 数据中心输出:
=== 不同场景的 initcwnd 风险评估 ===
initcwnd=64 → 首次发送: 91.3 KB
链路容量 @10Mbps/200ms: 244.1 KB
占用比例: 37%
⚠️ 风险高!首次发送超过容量的 37%
可能触发拥塞,建议减小 initcwnd坑 3:Path MTU 发现失败
你的服务器到某个网络路径上有 MTU < 1500 的链路(比如 PPPoE 的 1492 字节)。TCP 通过 ICMPv4 "Fragmentation Needed" 消息来发现这个,如果 ICMP 消息被防火墙拦截了,TCP 连接就会卡住——数据包发了但收不到响应,超时重传,反复失败。
# 测试路径 MTU
# 方法:发 DF 位设置的各种大小包,看哪个被丢弃
for size in 1500 1492 1472 1452 1432 1400; do
ping -c 2 -M do -s $size example.com 2>&1 | \
grep -E "(bytes from|exceeded|100% packet loss)"
done
# 临时解决方法:设置 MSS clamping
sudo iptables -t mangle -A FORWARD -p tcp \
--tcp-flags SYN,RST SYN \
-j TCPMSS --clamp-mss-to-pmtu坑 4:TFO 被中间设备破坏
TFO 依赖 SYN 包中携带数据(cookie + early data)。一些老旧防火墙认为 SYN 包不应该有数据,直接丢弃。
# 验证 TFO 是否正常工作
# 检查服务器能否给 TFO cookie
curl -H "Connection: Upgrade" --tcp-fastopen https://example.com
# 用 ss 看 TFO 连接统计
nstat -az | grep TcpExtTCPFastOpen
# TcpExtTCPFastOpenPassive 57 ← 服务端接受 TFO 连接数
# TcpExtTCPFastOpenActive 32 ← 客户端支持 TFO 连接数
# 如果全部为 0 → TFO 没在工作坑 5:HTTP/2 队头阻塞——TCP 层面的 TCP 队头阻塞
你说"用了 HTTP/2,多路复用,快!"——但在 2% 丢包的无线网络上,"多路复用"变成了灾难。
是因为 HTTP/2 的所有流共享同一个 TCP 连接。如果一个包丢了,TCP 必须重传那个包,所有流都要等——即使其他流的数据已经到达了接收端。
这就是 HTTP/2 的 TCP 队头阻塞(Head-of-Line Blocking)。
def http2_hol_blocking(packet_loss_rate: float, num_streams: int):
"""模拟 HTTP/2 在丢包时的队头阻塞"""
# 简化模型:共享 TCP 窗口,任何丢包导致所有流暂停
lost_packets_per_window = num_streams * packet_loss_rate
print(f"丢包率: {packet_loss_rate*100:.1f}%")
print(f"并行流: {num_streams}")
print(f"每窗口预计丢包: {lost_packets_per_window:.2f}")
if lost_packets_per_window > 0.2:
print(f"⚠️ 高概率触发所有流等待重传")
print(f" → 考虑 HTTP/3 (QUIC) 或分离域名做多连接")
http2_hol_blocking(0.02, 100)
# 丢包率: 2.0%
# 并行流: 100
# ⚠️ 高概率触发所有流等待重传这就是 QUIC/HTTP3(第11章)解决的问题——用 UDP + 独立流解决了 TCP 层面的队头阻塞。
通关挑战
🗡 挑战 1:用 curl -w 分析三个网站的性能(15 分钟)
分别测试 google.com、你的公司网站、以及一个你访问慢的网站,用 curl 格式文件输出全链路耗时。指出每个网站的瓶颈在哪阶段。
# 提示:用一个脚本批量跑
for url in "https://google.com" "https://github.com" "https://your-site.com"; do
echo "=== $url ==="
curl -w "@curl-format.txt" -o /dev/null -s "$url" 2>/dev/null
echo
done挑战 2:用 Python 实现一个 HTTP Keep-Alive 测试(30 分钟)
写一个脚本:
- 连接到一个 HTTPS 服务器
- 用同一个连接发送 20 个 GET 请求
- 记录每个请求的时间
- 对比第一次和后续请求的耗时
结论应该是:第一次 ≈ TCP+TLS+请求+响应,后续 ≈ 请求+响应(省了 50-300ms)。
挑战 3:用 ss -ti 观察实时 TCP 状态(15 分钟)
打开一个下载大文件(比如 wget 一个 ISO 文件),同时在另一个终端观察:
# 在下载过程中,每秒钟观察 TCP 连接状态
watch -n 1 'ss -ti | grep -E "cwnd|rtt|cubic|bbr" | tail -5'观察:
- cwnd 是增长还是保持稳定?什么时候碰到 ssthresh?
- RTT 有没有随 cwnd 增长而变大?(这是缓冲区在膨胀的迹象)
- 有没有丢包(retransmit 计数变化)?
挑战 4:性能调优前后对比(45 分钟)
在一台 Linux 服务器上(可以在云上开一台测试机):
- 基线:用 iperf3 测当前带宽
- 调优一:增大 tcp_rmem 和 tcp_wmem,再次 iperf3
- 调优二:切换到 BBR,再次 iperf3
- 调优三:调整 initcwnd 到 30,再次用 curl 测 TTFB
记录每次调优的数值变化。哪些改动收益最大?
验收标准
完成本章后,你应当:
- [ ] 面对"用户说慢"的报修,能用 curl -w 分解出是 DNS、TCP、TLS、TTFB 还是内容传输的问题
- [ ] 为 Linux 服务器做基本的 TCP 性能调优(增大缓冲区、启用 BBR、调整 initcwnd)
- [ ] 说出 HTTP/2 多路复用 + TLS 1.3 0-RTT 的组合能提升多少性能(直觉上)
- [ ] 解释 CDN 缓存命中率和 TTFB 的关系
- [ ] 用 ss -ti 查看实时 TCP 状态(cwnd、rtt、ssthresh、拥塞算法)
- [ ] 说出 initcwnd 太大可能带来的拥塞风险
- [ ] 使用 iperf3 做带宽测试
常见卡点
| 卡点 | 原因 | 解药 |
|---|---|---|
| TTFB 很长时间不知道是网络还是服务端 | 没有分解 DNS/TCP/TLS/TTFB | 用 curl -w 分解每个阶段 |
| 调大了缓冲区但吞吐没变 | Window Scaling 被中间设备破坏 | 用 ss -ti 看实际 wscale |
| BBR 切换后没效果 | 需要 fq qdisc 配合 | sysctl net.core.default_qdisc = fq |
| initcwnd 改了不生效 | 路由更新的持久化问题 | 写到 /etc/network/interfaces 或 systemd 网络配置 |
| iperf3 带宽高但实际 HTTP 性能差 | iperf3 是大流持续发送,HTTP 是突发小请求 | 用 wrk/ab(压力测试工具)模拟真实负载 |
| CDN 缓存没命中,回源很慢 | TTL 太短、缓存分级没配好、源站本身慢 | 先优化源站,再拉长 CDN TTL |
| 改了 sysctl 重启后不生效 | 没有写到 /etc/sysctl.d/ | 写文件后 sysctl --system 或 sysctl -p |
现在不需要理解
- TCP 的精确带宽计算公式(Mathis 公式):
throughput = MSS × √(3/2) / (RTT × √loss_rate)。知道"丢包率每降 10 倍,吞吐量升约 3 倍"这个直觉就够了。精确公式属于拥塞控制章节的深水区。 - TUN/TAP 虚拟网络设备:构建 VPN 的技术基础。知道"可以创建软件层面的网卡"就够了。
- DPDK 和 XDP:绕过 Linux 内核网络栈直接操作网卡。知道"它们能让包处理从微秒级降到纳秒级"就够了。
- eBPF 网络优化:在内核中动态注入网络处理程序。知道"这是一个强大的可编程网络技术"就够了,具体实现是大专题。
- Netfilter/iptables/nftables 的完整链和表:知道"Linux 防火墙有三张表和多条链"的概念就够了,具体配置看专题教程。
- RSS/RPS 网卡多队列:知道"现代网卡可以把包分发到多个 CPU 核心"就行了,不需要了解具体的哈希算法。
旅人笔记
这一章没有新协议。它是一场毕业演练——把所有学过的网络知识,变成一个实实在在的排障工具箱。
当你面对"慢"的问题,你不再瞎猜。你的第一次判断是:
- curl -w 分解 → 哪个阶段是瓶颈
- ss -ti 看 TCP 实时状态 → cwnd/RTT/丢包
- iperf3 测带宽 → 链路容量
- 如果是 TTFB 慢 → 去查服务端代码,别碰网络
- 如果是 DNS 慢 → 加缓存或换解析器
- 如果是 TCP/TLS 慢 → 启用 BBR、开 TFO、升级 TLS 1.3
- 如果是内容传输慢 → initcwnd、缓冲区、HTTP/2、CDN
有一条原则贯穿一切:在开始调优之前,先测量。不要猜,不要靠感觉。
Vol 4 全卷最后一句话:驿道是一个多层堆叠的复杂法术系统——每一层有自己的节奏、自己的瓶颈、自己的优化空间。学完整卷后,真正的技能不是"背出了法术字段",而是当法师说"慢了",你能在三分钟内定位到最可能的层,拿出正确的法具验证,然后要么解决问题,要么确认"问题不在我这里"。
→ 下一站预告
Vol 5:数据库与数据系统
你在驿道上把所有知识打通了——装法术信函、发传送咒、排队、调优——你已经能驾驭从魔力脉冲到 HTTP 法术请求的整条驿道。
但数据最终要去的地方是哪里?法术核心磁盘、魔力卷轴内存、咒语索引、法术事务。Vol 5 你走进数据堡垒——从 SQL 到 B+树,从 LSM-Tree 到 MVCC,从事务隔离到分布式法术数据库。在驿道层的尽头,是数据的家。
驿道的尽头,在荒野的另一端,你看到一座巨大的堡垒在雾中若隐若现。那里是数据堡垒——法术数据库系统的世界。老陈的传送信函还在你的法术背包里,最后一句话有些褪色了:"你的法术数据……存好了吗?"