Skip to content

叙事密度:低 | 主语言: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 三次弹簧术"和"当法师说慢时你能找到根因"之间,差了十条驿道。

这一章就是那条驿道。我们不学任何"新传送咒"。我们学怎么把之前所有知识当成排障工具箱,遇到驿道问题时打出正确的工具。


你的任务

学完本章,你应当:

  1. 面对"慢"的投诉,系统性地排查全链路:DNS → TCP 连接 → TLS 握手 → TTFB → 内容传输
  2. 使用 curl -w、httpstat 做请求分阶段计时
  3. 用 ss -ti 监控实时 TCP 连接拥塞窗口和 RTT
  4. 在 Linux 上调优 TCP 内核参数(缓冲区、窗口缩放、拥塞算法)
  5. 配置 HTTP 缓存策略和连接复用
  6. 用 iperf3 测量带宽
  7. 解释 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 精确分解每个阶段:

bash
# 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> 500msDNS 服务器慢、递归查询链长、没有本地缓存
TCP 连接< 100ms (同区域)> 500ms物理距离远、中间设备加延迟、丢包导致 SYN 重传
TLS 握手< 100ms (TLS 1.3)> 500msTLS 1.2 完整握手(2-RTT)、证书链太长、OCSP 慢
TTFB< 200ms> 1s服务端处理慢(数据库查询、模板渲染)、边缘缓存未命中
内容传输取决于带宽带宽限制、拥塞控制窗口小、丢包率高
python
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 缓存

bash
# 安装本地 DNS 缓存代理
# Linux (dnsmasq 或 systemd-resolved)
sudo systemctl enable --now systemd-resolved

# macOS —— 内置 mDNSResponder 默认缓存
# Windows —— 默认缓存 86400 秒(可查 ipconfig /displaydns)

策略 2:预解析(Prefetch/Preconnect)

html
<!-- 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>
python
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 查询自动路由到最近的节点。

bash
# 对比两个 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)

bash
# 对比 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。

bash
# 检查 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.conf

TFO 的限制:

  • 只适用于重连场景(第一次连接不能带数据——需要服务器发 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 ←──────────────│  (接收+处理请求)
  │← ... ──────────────────────│
bash
# 对比 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.com

TLS 优化清单:

  1. 升级到 TLS 1.3——最主要的一步,从 2 RTT 降到 1 RTT
  2. 启用会话复用(Session Resumption)——服务端缓存会话票据(Session Ticket),重连时 0-RTT
  3. OCSP Stapling——服务端自己拿着证书状态,不让客户端自己去 OCSP 服务器查
  4. 证书链优化——减少中间证书数量,不在握手时发 4 层证书链
  5. 裁减密码套件——只留最快的几个(AES-GCM / ChaCha20-Poly1305),移除老旧套件
bash
# 检查服务器 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)
python
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 缓冲区调优

bash
# 查看当前 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 链路的优化推荐:

bash
# 增大缓冲区应对高 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 的机制

python
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 值

拥塞控制算法选择

bash
# 查看当前拥塞控制算法
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 -p
python
def 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)。有些场景下可以更大:

bash
# 调整某条路由的 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 KBRFC 6928 标准值(所有场景)
20~29 KB小文件多、RTT 高的场景
30~43 KBCDN、静态资源
64~93 KB大文件下载,但注意拥塞风险

注意:initcwnd 不是越大越好。在低速链路(家用 ADSL)上,过大的 initcwnd 会立即撑爆缓冲区,导致丢包和性能下降。生产环境应该用 iperf3 加不同 initcwnd 对比测试。


第7战:HTTP 优化——减少请求数量,压缩传输体积

问题: 你的传送阵 TTFB 只有 200ms,但整体加载时间 6 秒——因为要下载 50 个法术资源(样式卷轴、法术脚本、魔导图片、咒语字体),每个都要建立连接(或等待前一个完成)。

Keep-Alive 和连接池

python
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 用一个连接同时传输多个流:

bash
# 对比 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

资源压缩

bash
# 检查服务器是否启用了压缩
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×

缓存策略

bash
# 检查响应头的缓存策略
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.jspublic, max-age=31536000, immutable一年不变,浏览器直接读磁盘
非版本化 CSS/图片public, max-age=86400缓存一天
HTML 页面no-cache, must-revalidate每次验证 ETag
API 响应private, max-age=60no-cache动态内容,客户端按需决定

预加载(Preload/Prefetch/Preconnect)

html
<!-- 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 强更多,但消耗也更大 -->
python
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 控制
python
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.5x

CDN 的源站保护机制:

  • 缓存未命中才回源 — 大量请求被 CDN 截住,源站只处理 5-20% 的请求
  • 请求限制 —— CDN 节点可以限制每个客户端每秒的请求数(Rate Limiting)
  • WAF 过滤 —— 在边缘层拦截 SQL 注入、XSS 等攻击
  • DDoS 吸收 — CDN 分布式架构可以吸收几百 Gbps 的攻击流量

第9战:负载均衡——把流量均匀分配

问题: 一座信标塔的处理能力有上限(比如 5000 每秒法术处理)。你的驿道用法增长到 20000 每秒法术处理。加信标塔很简单,但怎么让魔力流量平均分配到 4 座信标塔?

三种负载均衡方案,从简单到复杂:

方案 1:DNS 轮询(最简单,最弱)

bash
# 同一个域名返回多个 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 + 端口转发包,不改包内容。

bash
# 用 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.1G

LVS 三种模式:

模式缩写回包路径性能场景
NATVS/NAT必须经过 LB简单、所有 OS 兼容
直接路由VS/DR直接回客户端需要 VIP 配置在 RS 上
隧道VS/TUN封装回客户端非常高跨网段服务器

方案 3:七层负载均衡(NGINX/HAProxy)

工作在应用层(HTTP),可以基于 URL、Cookie、Header 做路由。

nginx
# 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; }
}
python
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 —— 带宽测量

bash
# 服务端
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 4
python
import 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 连接实时状态

bash
# 查看所有 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 —— 实时流量监控

bash
# 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 字节)被中间设备修改或者丢弃。

bash
# 检查是否启用了 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"

怎么解决:你没法控制中间设备。你能做的是:

  1. 联系网络管理员确认中间设备没有修改 TCP 选项
  2. 如果必须经过特定设备,考虑 VPN 封装绕过
  3. 使用 QUIC(基于 UDP,不受 TCP 中间设备影响)

坑 2:initcwnd 太大导致拥塞崩溃

你看到一篇博客说"initcwnd=64 让我的网站快了一倍",你就把服务器的 initcwnd 改成了 64。然后你的用户报告在一些网络下页面打不开或极度缓慢。

原因:initcwnd=64 意味着连接建立后第一次发送 64 个 MSS(约 93KB)。在低速链路(3G 网络、共享宿舍带宽)上,这个量可能超过链路缓冲区可以承受的范围,所有包都被缓冲或丢弃,导致 TCP 进入长时间的慢启动恢复。

python
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 连接就会卡住——数据包发了但收不到响应,超时重传,反复失败。

bash
# 测试路径 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 包不应该有数据,直接丢弃。

bash
# 验证 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)

python
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 格式文件输出全链路耗时。指出每个网站的瓶颈在哪阶段。

bash
# 提示:用一个脚本批量跑
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 分钟)

写一个脚本:

  1. 连接到一个 HTTPS 服务器
  2. 用同一个连接发送 20 个 GET 请求
  3. 记录每个请求的时间
  4. 对比第一次和后续请求的耗时

结论应该是:第一次 ≈ TCP+TLS+请求+响应,后续 ≈ 请求+响应(省了 50-300ms)。

挑战 3:用 ss -ti 观察实时 TCP 状态(15 分钟)

打开一个下载大文件(比如 wget 一个 ISO 文件),同时在另一个终端观察:

bash
# 在下载过程中,每秒钟观察 TCP 连接状态
watch -n 1 'ss -ti | grep -E "cwnd|rtt|cubic|bbr" | tail -5'

观察:

  • cwnd 是增长还是保持稳定?什么时候碰到 ssthresh?
  • RTT 有没有随 cwnd 增长而变大?(这是缓冲区在膨胀的迹象)
  • 有没有丢包(retransmit 计数变化)?

挑战 4:性能调优前后对比(45 分钟)

在一台 Linux 服务器上(可以在云上开一台测试机):

  1. 基线:用 iperf3 测当前带宽
  2. 调优一:增大 tcp_rmem 和 tcp_wmem,再次 iperf3
  3. 调优二:切换到 BBR,再次 iperf3
  4. 调优三:调整 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 --systemsysctl -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 核心"就行了,不需要了解具体的哈希算法。

旅人笔记

这一章没有新协议。它是一场毕业演练——把所有学过的网络知识,变成一个实实在在的排障工具箱。

当你面对"慢"的问题,你不再瞎猜。你的第一次判断是:

  1. curl -w 分解 → 哪个阶段是瓶颈
  2. ss -ti 看 TCP 实时状态 → cwnd/RTT/丢包
  3. iperf3 测带宽 → 链路容量
  4. 如果是 TTFB 慢 → 去查服务端代码,别碰网络
  5. 如果是 DNS 慢 → 加缓存或换解析器
  6. 如果是 TCP/TLS 慢 → 启用 BBR、开 TFO、升级 TLS 1.3
  7. 如果是内容传输慢 → initcwnd、缓冲区、HTTP/2、CDN

有一条原则贯穿一切:在开始调优之前,先测量。不要猜,不要靠感觉。

Vol 4 全卷最后一句话:驿道是一个多层堆叠的复杂法术系统——每一层有自己的节奏、自己的瓶颈、自己的优化空间。学完整卷后,真正的技能不是"背出了法术字段",而是当法师说"慢了",你能在三分钟内定位到最可能的层,拿出正确的法具验证,然后要么解决问题,要么确认"问题不在我这里"。


→ 下一站预告

Vol 5:数据库与数据系统

你在驿道上把所有知识打通了——装法术信函、发传送咒、排队、调优——你已经能驾驭从魔力脉冲到 HTTP 法术请求的整条驿道。

但数据最终要去的地方是哪里?法术核心磁盘、魔力卷轴内存、咒语索引、法术事务。Vol 5 你走进数据堡垒——从 SQL 到 B+树,从 LSM-Tree 到 MVCC,从事务隔离到分布式法术数据库。在驿道层的尽头,是数据的家。

驿道的尽头,在荒野的另一端,你看到一座巨大的堡垒在雾中若隐若现。那里是数据堡垒——法术数据库系统的世界。老陈的传送信函还在你的法术背包里,最后一句话有些褪色了:"你的法术数据……存好了吗?"

Built with VitePress | Software Systems Atlas