是的,net.ipv4.tcp_tw_recycleLinux 4.12 已经彻底删除(commit 4396e46187ca "tcp: remove tcp_tw_recycle",2017-07)。4.12 以后的内核上这个文件根本不存在,sysctl -w net.ipv4.tcp_tw_recycle=1 会直接报 "No such file or directory"——网上那些"三件套调优"(tw_reuse + tw_recycle + tcp_fin_timeout)的帖子在现代内核上是过期的。

为什么删

tcp_tw_recycle=1 的机制是:内核在 tcp_metrics按对端 IP 缓存最近见到的 TCP timestamp(TSval),60 秒内如果收到同一个 IP 过来的 SYN 且携带的时间戳比缓存值小,就静默丢弃。这样 TIME_WAIT 就不用等 60 秒,可以快速复用。

问题出在这个"按 IP"上:

  • NAT 后面的客户端共享一个源 IP,而各台机器的 timestamp 时钟彼此独立、完全不同步。A 机器的 TSval 比 B 大,B 的 SYN 就被丢掉 → 客户端表现为连接超时、重试几次才通,或者干脆某几台机器连不上。这是生产环境里最经典的"诡异丢包"事故来源之一。
  • 触发条件还要求两端 tcp_timestamps=1(默认开),所以踩坑门槛很低。

压死它的最后一根稻草是 4.10 的 commit 95a22caee396 "tcp: randomize tcp timestamp offsets for each connection":每条连接的 timestamp 加了随机偏移,同一个 IP 不同连接的 TSval 之间不再单调可比,tw_recycle 的前提直接没了——功能变成"只会误伤、不会生效",于是 4.12 删掉。

还留着的是什么

参数 状态 作用
tcp_tw_recycle 4.12 删除
tcp_tw_reuse 保留 只影响主动 connect():复用处于 TIME_WAIT 的本地端口,要求 timestamps 开启且该 TW socket 已存在 >1s(PAWS 保证旧报文能被识别)
tcp_max_tw_buckets 保留 TW socket 数量上限,超了直接干掉并打 TCP: time wait bucket table overflow
tcp_fin_timeout 保留 管的是 FIN_WAIT_2,不是 TIME_WAIT

两个容易被问到的点:

  1. tcp_tw_reuse 救不了服务端。 它只在 connect() 选端口时起作用。服务端的 TIME_WAIT 堆积(本地端口固定为 80/443,四元组由客户端决定)跟它没关系,服务端的 TIME_WAIT 也不消耗端口资源,只占内存和 hash 表条目。
  2. TIME_WAIT 的 60 秒是编译期常量 TCP_TIMEWAIT_LENinclude/net/tcp.h60*HZ),没有 sysctl 可调,改只能改代码重编译。tcp_fin_timeout 调它是流传很广的误解。

Linux 5.6 起 tcp_tw_reuse 的默认值从 0 改成 2 = "仅对回环地址生效",因为环回上不可能有旧报文绕路回来,开着绝对安全。(版本号凭记忆,未实测,可 git log --oneline -- Documentation/networking/ip-sysctl.txt 核)

面试里该怎么答"TIME_WAIT 太多怎么办"

正确方向不是调 sysctl 绕过 TIME_WAIT,而是别产生那么多短连接

  • 客户端侧:连接池 / HTTP keep-alive / gRPC 多路复用,把短连接改长连接
  • 客户端先 close,TIME_WAIT 落在客户端(分散到 N 台机器,而不是压在一台服务端上)
  • 客户端确实要发起海量出向连接时:扩 net.ipv4.ip_local_port_range,配 tcp_tw_reuse=1,或者用多个源 IP 扩四元组空间
  • 绝不要为了"清 TIME_WAIT"去开 tw_recycle(现在也开不了)或粗暴调小 tcp_max_tw_buckets——后者等于让内核跳过 TIME_WAIT 状态,丢掉它保护的两件事:旧的重复报文被新连接误收,以及最后那个 ACK 丢失时能重发。

分两种情况,解法完全不同——面试里先把这两种拆开讲,基本就赢了一半。


A. 出向:自家海量机器经单个 SNAT 网关出去

这是"端口耗尽"的经典场景。先把数学算清楚。

极限到底在哪

NAT 网关要保证转换后的五元组唯一,所以限制是:

(网关公网IP, 源端口) 对每一个 (目的IP, 目的端口) 唯一

注意是 per-destination,不是全局。Linux netfilter 的 nf_nat_used_tuple() 比对的是完整元组,所以同一个源端口可以同时用于访问 A 和 B 两个不同的目的地。这意味着:

  • 打到同一个后端 VIP:443 的并发连接上限 ≈ 端口范围大小(默认 32768-60999,只有 28232 个,不是 65535)
  • 打到 1000 个不同目的地,总量可以是 2.8 万 × 1000

有两个东西会先于端口撞墙:

先撞的墙 参数 默认值(典型)
conntrack 表满 net.netfilter.nf_conntrack_max 按内存算,常见只有 26 万左右;满了 nf_conntrack: table full, dropping packet
TIME_WAIT 条目滞留 net.netfilter.nf_conntrack_tcp_timeout_time_wait 120s(比内核 socket 的 60s TIME_WAIT 还长一倍,网关上占位更久)
空闲连接被静默回收 nf_conntrack_tcp_timeout_established 432000s(5 天);但云厂商 NAT 网关通常是 350s 左右

云厂商的口径(AWS 文档,未实测):单个 NAT Gateway 对每个唯一目的地支持约 55,000 并发连接,超了看 CloudWatch 的 ErrorPortAllocation。GCP Cloud NAT 是 minPortsPerVm(默认 64)+ 动态端口分配。本质是同一套数学。

解法,按性价比排序

1. 把短连接改成长连接 —— 唯一的治本方案

一条 HTTP/2 或 gRPC 连接跑 10k RPS 只占 1 个端口。连接池 + keep-alive 之后,端口数从"和 QPS 成正比"变成"和后端实例数成正比",通常直接从几万降到几百。所有 sysctl 调优加起来的收益都不如这一条。

配套要做的:TCP keepalive 间隔必须小于 NAT 的空闲超时,否则长连接被网关默默丢掉 conntrack 条目,下次发包收到 RST 或者干脆黑洞。

sysctl -w net.ipv4.tcp_keepalive_time=180 net.ipv4.tcp_keepalive_intvl=30 net.ipv4.tcp_keepalive_probes=3

云 NAT 空闲超时常见 350s,设 180s 留足余量。应用层也可以开 HTTP/2 PING、gRPC keepalive。

2. 扩的是 IP,不是端口

端口空间 6 万封顶,公网 IP 可以加。N 个 IP = N × 6 万。

# nftables SNAT 地址池
nft add rule ip nat postrouting oif eth0 snat to 203.0.113.10-203.0.113.25
# iptables 等价写法
iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to-source 203.0.113.10-203.0.113.25

云上对应的是部署多个 NAT 网关,按子网 / AZ 拆路由表,把流量分摊开。这也顺带解决了单点故障——"单一 gateway"本身就是个可用性问题,不只是端口问题。

3. 把大流量从网关上摘出去

最大头的流量往往根本不该走 SNAT:

  • 访问对象存储 / 数据库 / 内部服务 → VPC Endpoint / PrivateLink / 私网直连,不经过 NAT
  • 服务间调用 → 走内网 IP 直连或 service mesh,sidecar 到 sidecar 不做地址转换
  • 跨 IDC → 专线 / 对等连接

4. IP_BIND_ADDRESS_NO_PORT(Linux 4.2+,容易被忽略)

如果客户端代码做了 bind() 指定源 IP 再 connect()(多网卡场景很常见),内核在 bind() 时还不知道目的地,只能全局分配一个没被占用的端口——限制立刻从 per-destination 退化成全局 2.8 万。

int one = 1;
setsockopt(fd, IPPROTO_IP, IP_BIND_ADDRESS_NO_PORT, &one, sizeof(one));
bind(fd, src_addr, len);   // 此时不分配端口
connect(fd, dst_addr, len); // 知道四元组后才选端口,恢复 per-destination 上限

curl、部分 HTTP 库已经默认开了。自研的连接池要自己加。

5. 剩下的 sysctl(收益最小,但免费)

sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w net.ipv4.tcp_tw_reuse=1        # 只对主动 connect 生效,PAWS 保证安全
sysctl -w net.netfilter.nf_conntrack_max=2097152
sysctl -w net.netfilter.nf_conntrack_buckets=524288
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

1024 起要注意别和本机监听端口冲突,用 net.ipv4.ip_local_reserved_ports 把要保留的挖掉。调 nf_conntrack_max 前先估内存:每条 conntrack 约 300 字节,200 万条约 600MB。


B. 入向:你是服务端,客户端来自别人的大 NAT

这才是上一个回答里 tcp_tw_recycle 翻车的那一面,而且问题至今存在——只是换了个马甲。

核心原则:在这个时代,源 IP 不再等于"一个用户"。 任何以源 IP 为 key 的决策,在运营商 CGNAT、大公司出口、云厂商 NAT 后面都会误伤一整片人。

典型踩坑:

按源 IP 做的事 后果
tcp_tw_recycle(已删) 整个 NAT 后面的机器随机连不上
限流 / 封禁 / WAF 计数 一个人刷接口,整栋楼被封
单 IP 最大连接数限制(nginx limit_conn 大客户整个办公室只能开 100 条连接
SYN flood 启发式、fail2ban 误判成攻击源
风控、验证码触发 正常用户反复被挑战

解法是换 key,按真实身份粒度来:

  • 四层:PROXY protocol 把真实客户端地址透传到后端(HAProxy / Nginx stream / Envoy 都支持)
  • 七层:X-Forwarded-For / X-Real-IP,但必须配可信代理链(nginx set_real_ip_fromreal_ip_recursive on),否则被伪造
  • 限流优先按 账号 ID / API Key / session / 设备指纹 分桶,IP 只作为兜底的粗粒度维度
  • IPv6 场景按 /64 前缀聚合,而不是单地址(单机可以有海量 v6 地址)

服务端自身的 TIME_WAIT 堆积则不用治:服务端监听端口固定,TIME_WAIT 不消耗端口资源,只占 hash 表和内存,几十万条也就百来兆。真正要看的是 ss -s 里的总量和 nf_conntrack_max,而不是急着去调 sysctl。


面试时的一句话总结

单网关的端口耗尽,本质是"并发短连接数"超过了"IP 数 × 端口空间"。正确顺序是:先用长连接+多路复用把分子压下来,再用多 IP / 多网关把分母抬上去,最后才是调 conntrack 和端口范围这些边角。反过来先调 sysctl 的,通常几个月后会再撞一次墙。

而作为服务端,面对别人的单网关,要做的是停止用源 IP 当用户标识

你这个问题里有一个隐含前提需要先拆掉:"一个 IP" ≠ "一台 nginx"。 Google 那个 IP 后面不是一台机器,甚至不是一个机房。把这层展开之后,你会发现"nginx 不够用"的地方和你想的不是同一个地方。


一、一个 IP 是怎么被几百台机器同时持有的

  用户(1 亿)
     │   DNS:GeoDNS / 多 A 记录,不同地区拿到不同 VIP,TTL 几十秒
     ▼
  ┌ Anycast VIP  203.0.113.1 ┐  ← 同一个 IP 由全球 200+ PoP 用 BGP 同时宣告
  └──────────┬───────────────┘
             │   BGP 选路:用户就近落到某个 PoP(这一步已经把 1 亿分摊到几百份)
             ▼
      机房路由器:ECMP 按五元组哈希
             │
     ┌───────┼───────┬───────┐
     ▼       ▼       ▼       ▼
   L4LB#1  L4LB#2  L4LB#3 … L4LB#N     ← Maglev / Katran / Cilium,XDP 里转发
     │  一致性哈希 + flow table:同一条 TCP 流永远落到同一台 L7
     └───────┬───────────────┘
             ▼
  ┌──── L7 代理集群(nginx / Envoy / GFE),数百~数千台 ────┐
  │  TLS 终结、HTTP/2·3 多路复用、路由、限流                  │
  └──────────────────┬───────────────────────────────────────┘
                     │ upstream keepalive 连接池
                     ▼
               后端服务(数万实例)

关键技术点:

技术 解决什么
Anycast(BGP 宣告同一前缀) 一个 IP 变成全球几百个接入点,天然就近 + 天然分摊
ECMP 单机房内把这个 VIP 分给 N 台 L4 LB,路由器按五元组哈希
一致性哈希 L4 LB(Google Maglev / FB Katran / Cloudflare Unimog / GitHub GLB) LB 扩缩容时,已建立的 TCP 连接不会被打到别的后端上
DSR(直接服务器返回) 响应不经过 LB。请求几百字节、响应几十 KB,LB 的带宽压力立刻降一个数量级
XDP / eBPF 在网卡驱动层转发,单机线速几千万 pps,不进协议栈

所以"一亿人访问一个 IP",实际是一亿人被摊到了几百个 PoP × 几十台 L4 × 几百台 L7 上。横向扩展是在 IP 之下发生的,不是在 IP 之上。


二、服务端的 accept 侧,根本不存在 64K 端口限制

这是最容易搞混的一点。四元组是:

(客户端IP, 客户端端口, 服务端IP, 服务端端口)

对服务端来说,服务端IP:443固定的,变化的是客户端那一半。理论空间是 2³² × 2¹⁶ ≈ 2.8×10¹⁴。端口耗尽是客户端(主动 connect 一方)的问题,服务端监听 socket 没有这个约束。

单台 nginx 的真实上限是资源,不是端口:

资源 参数 量级
文件描述符 fs.nr_openulimit -n、nginx worker_connections 调到百万级没问题
内核 socket 内存 每条 established 空闲连接约 3–10 KB 100 万连接 ≈ 3–10 GB
TLS 缓冲区 nginx ssl_buffer_size(默认 16K,收发各一份) 这才是大头:100 万 TLS 连接可能吃掉几十 GB
accept 队列 net.core.somaxconntcp_max_syn_backlog 默认偏小,高并发必调
多核 accept 扩展 SO_REUSEPORT(nginx reuseport 避免惊群、每 worker 独立队列

实践中单台 L7 扛 50 万~200 万并发长连接是常规操作(C10M 那套)。1 亿 注册/日活用户,同时在线可能只有百分之几,再摊到几百台上,每台就是几万到几十万——完全在射程内。

把 DAU 当成并发连接数,是这类题里最常见的口径错误。


三、真正会耗尽端口的是 nginx → 后端这一段

这一侧 nginx 是客户端,才有 (本机IP, 本机端口) 对每个 (后端IP, 后端端口) 唯一的约束,默认端口范围只有 28232 个。

先算清楚到底需要多少条上游连接

用 Little's Law:并发连接数 = QPS × 后端响应时间,和用户总数完全无关。

场景 单台 nginx QPS 后端 p99 需要的上游连接 结论
开 keepalive 100,000 20 ms 2,000 轻松
开 keepalive,后端慢 100,000 200 ms 20,000 接近单个后端 IP:PORT 的上限,要摊开
不开 keepalive 100,000 20 ms 100,000×60s = 600 万个端口(TIME_WAIT 压着 60 秒) 秒挂

结论很清楚:"长连接不够用"这个前提通常不成立——不够用的是没开 keepalive 的情况。 而且 nginx 的 upstream 默认不开 keepalive,必须显式配:

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    keepalive 512;              # 每个 worker 保留的空闲长连接数
    keepalive_requests 100000;  # 默认只有 1000,太小会频繁重建
    keepalive_timeout 60s;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;      # 必须,默认 1.0 不支持 keepalive
        proxy_set_header Connection ""; # 必须,否则透传了 Connection: close
    }
}

这三行漏一行,keepalive 就静默失效。线上排查时直接看 ss -tan state time-wait | wc -l,几十万就说明没生效。

真的还不够时的扩展手段

手段 效果 说明
增加后端端点数 上限 = N × 64K 限制是 per-(dst IP, dst port)。100 个后端实例就是 640 万。这是最自然的扩法
一个后端监听多个端口 ×M 后端实例少但很壮时有用
nginx 绑多个源 IP ×K proxy_bind 轮换源地址,或一台机器跑多个 nginx 实例各绑一个 IP
HTTP/2 / gRPC 到上游 一条连接跑几百个并发请求 nginx 只对 gRPC 支持上游多路复用;要 HTTP/2 上游用 Envoy 或 HAProxy
Unix domain socket 完全不占端口 仅限 nginx 和后端同机(sidecar 模式)
IP_BIND_ADDRESS_NO_PORT 全局上限 → per-destination 上限 只要用了 proxy_bind 就必须确认开了这个(nginx 新版本会自动用)
扩端口范围 28K → 64K,2 倍 net.ipv4.ip_local_port_range="1024 65535",收益最小的一条

四、HTTP/3 带来的变化(加分项)

QUIC 跑在 UDP 上,连接标识从四元组换成了 Connection ID。这意味着:

  • 用户从 WiFi 切到 5G,IP 变了,连接不断(连接迁移)——四元组时代做不到
  • 但 L4 LB 不能再按五元组哈希了,否则迁移后的包会打到别的机器。得改成按 Connection ID 路由(QUIC-LB 草案:把后端标识编码进 CID)
  • 0-RTT 建连,把"用户量大 → 建连开销大"这个问题也削掉一块

大厂现在的前端基本都是 H3 优先、H2 回落。


面试时的答法

首先,一个 IP 背后不是一台机器:Anycast 让同一个 IP 在全球几百个 PoP 同时宣告,机房内再用 ECMP + 一致性哈希 L4 LB(Maglev/Katran)摊到几百台 L7,横向扩展发生在 IP 之下。

其次,服务端的 accept 侧没有 64K 端口限制——四元组里变化的是客户端那一半,单机瓶颈是 fd、内存和 TLS 缓冲,不是端口。

真正会端口耗尽的是 L7 到后端这一跳,因为这时 L7 是客户端。解法是 upstream keepalive:连接数 = QPS × 后端延迟,与用户总数无关,10 万 QPS、20ms 延迟只需要 2000 条连接。不够时按 (后端IP, 后端端口) 维度摊开,或者换 Envoy 用 HTTP/2 上游多路复用。

先给结论:"单台 L4 LB 扛一亿用户"无解,而且卡死它的是物理,不是架构。 有解的是另一个问题——让"某一台设备面对一亿用户"这个状态根本不出现。这两件事必须分开谈。


一、先用带宽证明单机不可能

不用谈连接数,光算带宽:

1 亿用户 × 人均 1 kbps(这已经是近乎空闲的量级)= 100 Gbps

一张 100G 网卡就满了。真实浏览场景人均几十上百 kbps,那就是 几 Tbps。单机装不下几 Tbps 的网卡,这是物理上限,任何软件优化都跨不过去。

包速率同理(量级估算,取决于网卡/CPU/包长,未实测):

用户行为 人均包速率 1 亿用户总 pps 单机 XDP 能力量级 需要几台
长连接空闲,30s 一次 keepalive 0.03 pps ~3 Mpps ~10–40 Mpps 1 台(理论上)
轻度交互 1 pps 100 Mpps 同上 3–10 台起
正常浏览 10 pps 1 Gpps 同上 几十台

所以问题不是"有没有解",是必须是 N 台。那么真正该问的是:N 台怎么分摊,才能保证没有任何一台落到一亿?


二、ECMP 就是那个分摊机制,它的上限和解法

L4 LB 的用户量不是"分配"来的,是路由器 ECMP 按五元组哈希摊过来的:每台 LB 用 BGP 宣告同一个 VIP,路由器看到 N 条等价路径,每台拿到 1/N。

加一台 LB = 多一台机器宣告同一个 VIP,不需要任何协调。所以"某台有一亿"只在 N=1 时成立。

真正的天花板在这里:路由器 ECMP 的 next-hop 宽度有限,交换芯片通常 64 / 128 / 256 条。超了怎么办:

手段 做法
多级 ECMP 上层交换机 ECMP 到 64 台下层交换机,每台再 ECMP 到 64 台 LB → 4096 台。层级化是标准解
多 VIP 一个 VIP 撑不住就宣告 8 个,DNS 轮询/GeoDNS 分发。DNS 是免费的第 0 级负载均衡
多 PoP(Anycast) 最有效的一级:BGP 直接把用户按地理位置劈成几百份,各 PoP 之间零协调

这三级叠起来:几百 PoP × 几千 LB/PoP,1 亿摊下去每台几千到几万,全在舒适区。


三、要让单台逼近它的物理极限,唯一出路是无状态

如果 LB 为每条连接维护表项,那它的上限就变成内存和 cache,远早于带宽撞墙:

LB 模式 每连接状态 1 亿连接的代价 结论
IPVS / conntrack NAT ~300 B + 哈希表 30–60 GB,且每个包一次随机内存访问 内存勉强塞下,cache miss 直接把 pps 打到地板,不可行
Katran / Maglev:LRU 软状态 LRU 固定上限(比如 100 万条) 几百 MB,与用户数无关 可行
纯无状态一致性哈希 0 0 可行

关键设计是 Maglev 一致性哈希:LB 查表未命中时,直接用一致性哈希算出后端,结果和有表时一致。于是连接表退化成一个纯粹的性能缓存——丢了不影响正确性。这带来两个决定性好处:

  1. 状态量和用户数解耦,单台的上限重新由带宽/pps 决定
  2. ECMP 重哈希不再是灾难。加一台 LB 时路由器哈希会变,老连接被甩到另一台 LB 上——有状态 LB 会因为查不到表项而把连接打到错误后端、全量断连;一致性哈希 LB 算出的还是同一个后端,连接活着

这就是为什么 Google 要写 Maglev、Facebook 要写 Katran:不是为了快,是为了让扩容不杀连接

再叠上 DSR:响应不经过 LB。请求几百字节、响应几十 KB,LB 的带宽需求直接降一个数量级——上面那个"100 Gbps"的账,只算入向就够了。


四、把"用户数"翻译成"pps",这里有真正的杠杆

LB 眼里没有"用户",只有包。一个挂着空闲 HTTP/2 长连接的用户,对无状态 LB 的成本几乎为零——只有心跳包。于是:

杠杆 效果
keepalive 间隔 30s → 300s LB 的 pps 直接降 10 倍
HTTP/2 / HTTP/3 多路复用 一个用户一条连接,而不是 6 条
客户端合并请求、批量上报 包数和用户行为解耦
MTU / GRO / GSO 同样字节数,包数更少(LB 通常是 pps 受限而非 bps 受限)

「一亿用户」在 LB 这一层,被翻译成了「一亿 × 心跳频率」。心跳频率是你能改的数字。


五、真正无解的那个场景:同步重连风暴

稳态可以靠上面这些扛住。真正会打穿的是所有人同时重连

  • 一次故障恢复,1 亿个客户端在同一秒发起 SYN
  • 整点开抢、直播开始、推送触达
  • 某个上游抖动,全网客户端同时超时重试

这时候扩容来不及——因为峰值可能是稳态的几百倍。这里没有容量解,只有准入控制解

手段 说明
客户端抖动退避 重连延迟 random(0, T) + 指数退避。这是最重要的一条,而且必须写在客户端,服务端补救不了
XDP 层丢包 超限的包在网卡驱动里就丢,不进协议栈——这是代价最低的拒绝方式
SYN cookie net.ipv4.tcp_syncookies=1,半连接队列满时不占内存也能完成握手
分批放行 / 排队 客户端拿到"稍后重试 + 指定时间窗",把尖峰摊成平台
降级 优先保住核心链路,非关键功能直接拒

面试里能主动提到"重连风暴需要客户端侧抖动",比背出任何 sysctl 都加分——因为这说明你知道有些问题必须在系统边界之外解决


一句话总结

一亿用户压到任何单点上都无解——100 Gbps 网卡和几十 Mpps 就是硬墙。所以设计目标从来不是"让单点变强",而是让每一层的扇入因子有界(通常控制在 100–1000),靠 Anycast、多级 ECMP、DNS 叠出 3 层,1 亿就被摊成了每台几千。

支撑这套分摊的前提是 L4 LB 必须无状态:用 Maglev 一致性哈希代替连接表,状态量与用户数解耦,ECMP 重哈希时连接也不会断。

稳态之外还有一个容量解决不了的问题——同步重连风暴,那里只能靠客户端抖动退避和 XDP 层准入控制。

作者:程序员小富 链接:https://www.zhihu.com/question/6058311194/answer/1982008422640673789 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

HTTP/3 最大的改动根本不在 HTTP 层,而在于它把底层协议从 TCP 换成了 UDP 的 QUIC 协议,改动还是比较大的。 HTTP/3 有啥好处?解决了慢HTTP/2 有个问题导致它跑不快,就是 TCP 层的队头阻塞。HTTP/2 在应用层把数据拆成了很多流(Stream),好像大家可以并行传输了。但是! 底下的 TCP 协议不知道啊,TCP 依然认为这就是一个有序的字节流。一旦丢了一个包,TCP 为了保证数据有序,会暂停所有后续数据的交付,直到那个丢失的包重传回来。结果 一个请求卡住,整条连接上的几十个请求全得陪着等。在弱网环境下,HTTP/2 甚至比 HTTP/1.1 还慢。HTTP/3 基于 UDP 的 QUIC 协议,流相对独立。流 A 的包丢了?没关系,只重传流 A 的,流 B、流 C 继续传输,完全不受影响。我觉得这是 HTTP/3 最核心的价值,在弱网丢包严重环境下,性能吊打 HTTP/2。连接迁移你拿着手机刷视频,从家里的 Wi-Fi 切换到了 4G 信号。TCP 连接是基于 四元组源IP、源端口、目的IP、目的端口的。你 IP 变了,TCP 连接就断了,视频瞬间卡顿,需要重新握手建连。HTTP/3 QUIC 协议引入了 Connection ID (CID) 的概念。不管你的 IP 怎么变,只要 CID 没变,服务器就不会重连了。所以网络切换无感,连接不断,体验极度丝滑。握手延迟的降低 八股文都背过 TCP握手, 三次握手 + TLS 握手,一来一回要好几趟。用户打开网页前,光是建立连接就得耗好几百毫秒。HTTP/3 的 QUIC 把传输层握手和加密层握手(TLS 1.3)合并了。升级简单了TCP 协议栈是写在操作系统内核(Kernel)里的。想升级 TCP 的算法?你得指望全球几十亿用户升级 Windows/Linux/Android 系统,这几乎不可能。HTTP/3 的 QUIC 是在用户态实现的。想升级更新一下浏览器或者 App 及其依赖库就行了,迭代速度像软件更新一样快。HTTP/3 又引入了什么新问题?能这么思考问题的人,我觉得才是学技术的正确方式,引入新技术就会带来新问题,这是我开发工作多年的切实感受UDP 看着都挺好,目前也遇到不少问题。中间设备不兼容互联网上有无数的防火墙、NAT 网关、路由器。在过去很长时间里,很多设备默认策略是:“只信任 TCP,UDP 往往是攻击流量 DDoS,给我限速或者直接丢弃。”有点像Jdk21 太好了简直神器,但是你发任你发,我用java 8。导致在很多企业内网或运营商环境下,HTTP/3 的连接建立失败率很高,谁敢冒险升级,不得不降级回 HTTP/2。硬件兼容性TCP 经过几十年的优化,现在的网卡都有 TOE。网卡硬件就能帮你处理分包、校验和,CPU 几乎不累。网卡目前还不认识 QUIC,所有的加密、解密、流控、重传,全都要靠 CPU 在用户态硬算。同样的流量,HTTP/3 可能会让服务器的 CPU 负载增加一倍甚至更多。实现难度极高TCP 的复杂性被操作系统屏蔽了,开发者只管 Socket 读写。HTTP/3 要求开发者在应用层用户态重新实现一套极其复杂的TCP 机制,比如可靠传输、拥塞控制、流量控制。目前成熟、稳定的 QUIC 库又不多,调试和排查问题的门槛比 TCP 高了一个数量级。为了保稳定,未来很长一段时间 TCP 还会是主流,HTTP/3 很强但是使用成本太高了

https://zhuanlan.zhihu.com/p/622683704 https://zhuanlan.zhihu.com/p/603138927