网络
是的,net.ipv4.tcp_tw_recycle 在 Linux 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 |
两个容易被问到的点:
tcp_tw_reuse救不了服务端。 它只在connect()选端口时起作用。服务端的 TIME_WAIT 堆积(本地端口固定为 80/443,四元组由客户端决定)跟它没关系,服务端的 TIME_WAIT 也不消耗端口资源,只占内存和 hash 表条目。- TIME_WAIT 的 60 秒是编译期常量
TCP_TIMEWAIT_LEN(include/net/tcp.h,60*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 或者干脆黑洞。
云 NAT 空闲超时常见 350s,设 180s 留足余量。应用层也可以开 HTTP/2 PING、gRPC keepalive。
2. 扩的是 IP,不是端口
端口空间 6 万封顶,公网 IP 可以加。N 个 IP = N × 6 万。
# nftables SNAT 地址池
# iptables 等价写法
云上对应的是部署多个 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;
;
; // 此时不分配端口
; // 知道四元组后才选端口,恢复 per-destination 上限
curl、部分 HTTP 库已经默认开了。自研的连接池要自己加。
5. 剩下的 sysctl(收益最小,但免费)
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,但必须配可信代理链(nginxset_real_ip_from、real_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_open、ulimit -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.somaxconn、tcp_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 查表未命中时,直接用一致性哈希算出后端,结果和有表时一致。于是连接表退化成一个纯粹的性能缓存——丢了不影响正确性。这带来两个决定性好处:
- 状态量和用户数解耦,单台的上限重新由带宽/pps 决定
- 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
暂无评论,欢迎留下第一条评论。