DNS 与 HTTP 入门

curl https://api.example.com/user/42。第一字节进 socket 之前,api.example.com 得先变成 IP —— 那是 DNS。 接着请求本身骑 3 种很不同的应用协议之一:HTTP/1.1(TCP 上的文本)、HTTP/2(单个 TCP 连接上的二进制 framing、多路复用流)、或 HTTP/3(UDP 上 QUIC 上的流)。 5 个 section 把画面搭出来:DNS 解析 —— resolver、记录类型、缓存;HTTP/1.1 —— keep-alive 和 HOL 阻塞;HTTP/2 —— 二进制 framing 和 HPACK + 可交互对比 demo;HTTP/3 / QUIC —— 0-RTT 和连接迁移; 最后是速查表

01

DNS —— 把 api.example.com 变成 IP

curl 能开 socket 之前,得把 api.example.com翻译成 203.0.113.42。那次翻译是横穿互联网的 5 跳走读, 每步都缓存 —— 也是大多数「时灵时不灵」故障真正住的地方。

curl 一跑,libc 第一件事就是getaddrinfo("api.example.com", "443", ...)。 这个函数先看 /etc/nsswitch.conf(通常写「files dns」—— 先试 /etc/hosts、再问 DNS resolver),然后 libc 里的stub resolver/etc/resolv.conf里的 IP 发个 UDP 包 —— 现代 systemd-resolved 主机上常是127.0.0.53,或你自己覆盖成 1.1.1.1 /8.8.8.8

递归走读

那个就近 IP 是 递归 resolver。它缓存里有答案的话, <1 ms 就给你回。没有的话,它替你走 DNS 层级:

  1. 根服务器(13 个标为 a.root-servers.netm.root-servers.net,每个其实是 anycast 到几百个物理实例) 告诉它哪里找 .com
  2. TLD 服务器(Verisign 跑的 .com) 告诉它哪些 nameserver 是 example.com 的权威服务器。
  3. 权威服务器(example.com 的) 返回 api.example.com 的 A 记录。

冷缓存下至少 3 次查询,每次有自己的 RTT。递归 resolver 拼命缓存来摊销这个 —— 所以你第二次 DNS 查询飞快,而新机器上第一次能 100 ms+。

$ dig +trace api.example.com
;; .                  86400 IN NS a.root-servers.net.   ; 根
;; com.               172800 IN NS a.gtld-servers.net.  ; TLD
;; example.com.        86400 IN NS ns1.example.com.     ; 权威
;; api.example.com.      300 IN A  203.0.113.42         ; 最终答案

要紧的记录类型

  • A —— IPv4 地址。最常见。
  • AAAA —— IPv6 地址。「quad-A」。
  • CNAME —— 规范名;另一个名字的别名。同名下不能与其他记录共存 (RFC 限制)。所以顶点记录(example.com 本身)不能是 CNAME —— 你需要 DNS 主机的 ALIAS / ANAME 或 flatten。
  • MX —— mail 交换器。带优先级字段做 fallback 排序。
  • TXT —— 任意文本。用于 SPF、DKIM、域名所有权验证 (「放这个 TXT 记录证明是你的」)。
  • NS —— nameserver 委托。说「example.com 下任何东西问 ns1.example.com」。
  • SOA —— start of authority。区元数据,包括负 TTL —— resolver 该缓存 NXDOMAIN 响应多久。
  • SRV —— 带端口的服务位置。SIP、XMPP、Kerberos、 Kubernetes 服务发现都用。
  • CAA —— Certification Authority Authorization。 列出哪些 CA 被允许给该域名签证书。CA/Browser Forum 要求 CA 发证前先查。

每层都的 TTL 和缓存

每个记录带 TTL —— 「缓存 N 秒」。每层都缓存:浏览器(~分钟)、 OS resolver(systemd-resolved 或 nscd)、递归 resolver(1.1.1.1 按 TTL 留)、 权威服务器本身。300 秒 TTL 意思是「改 DNS 后,世界可能在接下来 5 分钟看到你的旧 IP」。

SOA 记录里的负 TTL 同样要紧。你打错一个 hostname, NXDOMAIN 也会被缓存 —— 经常 5+ 分钟。配错 DNS 部署的第一征兆就是 「修复推上去后一小时里,部分用户看到错记录,其他人啥也看不到」。

UDP vs TCP、EDNS0、DoH / DoT

经典 DNS 是 53 端口的 UDP,按原 RFC 响应限 512 字节。比这大, 服务器就置截断标志(TC),客户端在 TCP 上重试 —— 直到EDNS0(RFC 6891),让客户端宣告支持更大 UDP 响应 (通常 4096 字节)。大多数现代部署用 EDNS0,普通查询从不回退到 TCP。

DNS over HTTPS(DoH)DNS over TLS(DoT)把 DNS 查询裹在 TLS 里,这样你 ISP 看不到也改不了。DoH 用 443 端口 (看着像普通 HTTPS 流量),Firefox 和 Chrome 原生就跟cloudflare-dns.comdns.google 这么说。 DoT 用 853 端口(专用端口 —— 公司防火墙容易封,这就是 DoH 赢的原因)。

Anycast —— 为啥 1.1.1.1 在哪都快

Cloudflare 的 1.1.1.1 和 Google 的 8.8.8.8 不住一个数据中心。同一个 IP 通过 BGP anycast 从几百个 PoP 宣告;BGP 拓扑上最近的 PoP 赢。 东京发到 1.1.1.1 的包到东京 PoP;伦敦发的到伦敦 PoP。同 IP、不同物理机。 这就是为啥「公共 resolver」能快到跟你 ISP 的较劲。

生产坑

  • JVM 默认永远缓存 DNS。Java 的networkaddress.cache.ttl 在设了 SecurityManager 时默认-1(无穷),否则 30 秒。长寿 JVM 解析到一个 round-robin DNS 的 负载均衡器,会钉死在一个 IP 上,后端死了它不知道。设成实际 TTL。
  • musl libc 不重载 /etc/resolv.confAlpine 容器启动时缓存 resolver IP。DNS 服务器 IP 改了(或容器在 /etc/resolv.conf 填好前就启动),你就卡住了。重启进程。
  • TTL 为 0 不等于「别缓存」。浏览器和某些库不管记录怎么写都按内部最小值缓存(经常 60 秒)。
  • 硬编码 resolver IP。把应用直接指 1.1.1.1 绕过本地缓存, 把你钉在一个外部供应商。2020 年 1.1.1.1 出故障时,死掉的就是硬编码栈。

要点。「DNS 是冷缓存回落路径:stub resolver → 递归 resolver → 根 → TLD → 权威,每层都拼命缓存。记录类型(A、AAAA、CNAME、MX、TXT、 NS、SOA、SRV、CAA)是词汇表。TTL 控制你对世界的视图能多陈。 普通查询 UDP,大查询 TCP,隐私 DoH / DoT。 经典生产 bug 就是某东西 —— JVM、musl、浏览器 —— 缓存比 TTL 说的久。」

02

HTTP/1.1 —— TCP 上的文本协议

一个 TCP 连接。一个请求,然后一个响应,然后或许再一个。线上是文本, 按行,人能解析。也是为啥你浏览器对同一 origin 开 6 个连接。

DNS 给我们 203.0.113.42 之后,curl 开个 TCP 连接到203.0.113.42:443、做 TLS 握手(下篇 primer 讲)、 然后说 HTTP。HTTP/1.1 下 TLS 之后线上走的就这个:

GET /user/42 HTTP/1.1
Host: api.example.com
User-Agent: curl/8.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cookie: session=abc123; csrf=xyz789

两个换行(\r\n\r\n)结束请求 headers。如果是 POST, body 跟在后面,Content-Length: N 告诉服务器读多少字节。 服务器随后用类似的文本帧消息回复:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 47
Cache-Control: private, max-age=60
ETag: "abc123"
Date: Wed, 27 May 2026 12:00:00 GMT

{"id":42,"name":"Ada","email":"ada@example.com"}

Keep-alive —— 复用 TCP 连接

HTTP/1.0 里,每个请求开新 TCP 连接、响应后关掉。这意味着每请求一次 TCP 握手 (1 RTT)+ 一次 TLS 握手(1-2 RTT)—— 延迟惩罚。HTTP/1.1 让keep-alive 成默认:响应后连接留着开,下一个请求复用。 服务器宣告 timeout(经常 60-75 秒)和每连接最大请求数。

所以 shell 里 curl https://a/x 然后 curl https://a/ycurl https://a/x https://a/y 慢很多 —— 前者开两个连接, 后者复用一个。

Pipelining —— 以及为啥没人用

HTTP/1.1 技术上允许 pipelining:响应 1 还没到就发请求 2、 响应 2 还没到就发请求 3,服务器必须按序回。实际上几乎没东西开着它出货, 因为两个问题:

  1. 队头(HOL)阻塞。如果响应 1 慢(200ms DB 查询), 响应 2 和 3 就卡在它后面,哪怕它们单独瞬时。客户端没改善, 连接在服务器思考时被浪费。
  2. 坏代理。野外许多透明代理对 pipelined 请求行为不端 —— 它们 buffer、重排、或丢响应。浏览器 2000 年代中期试过开 pipelining, 普遍回滚了。

为啥浏览器对每个 origin 开 6 个连接

Pipelining 死了,而一个 HTTP/1.1 连接一次只能扛一个未完成请求, 并发加载多个资源的唯一办法就是对同一 origin 开多个 TCP 连接。 所有主流浏览器执行的事实上限是每 origin 6 个并行连接。 那个数是「快加载资源」和「别 DDoS 服务器」之间的折中。

这也是为啥旧的性能技巧是「domain sharding」—— 从 img1.example.comimg2.example.com... 提供资源,这样浏览器开 6 × N 个连接。 现代 HTTP/2 让这个技巧主动有害(一个连接打败六个),但旧建议还潜伏在文章里。

Chunked transfer encoding —— 流式响应

有时服务器事先不知道响应大小 —— 它从数据库查询、long-poll、 或 SSE 流生成输出。服务器不用 Content-Length: N, 而是设 Transfer-Encoding: chunked 并发出带大小前缀的 chunk:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/event-stream

5\r\n
hello\r\n
6\r\n
 world\r\n
0\r\n
\r\n            ← 零长度 chunk = 「流结束」

没人逃得过的 headers

  • Host: —— HTTP/1.1 起必需;一个 IP 能服务多个虚拟主机的唯一办法。 没 Host,基于名字的虚拟主机(现代所有东西)就不工作。
  • Cookie: / Set-Cookie: —— 无状态协议上原版的有状态附加。每次发到匹配域名的请求都带。
  • Cache-Control:ETag:If-None-Match:—— 浏览器怎么问「我缓存的副本还好吗?」和服务器怎么答「304 Not Modified」。
  • Accept-Encoding: —— 「我能接 gzip / br / deflate」。 服务器挑一个(或没有)、用 Content-Encoding: 宣告。
  • Connection: keep-alive / Connection: close—— 退化但还在;keep-alive 是默认、现在很少显式发。

要点。「HTTP/1.1 是 TCP 上的文本,每连接一个未完成请求。 Keep-alive 摊销握手;pipelining 是注定失败的摊销请求-响应 RTT 尝试, 被 HOL 阻塞和坏代理杀死。浏览器靠对每个 origin 开 ~6 个并行连接补偿。 Cookie、Cache-Control、ETag、Accept-Encoding、Transfer-Encoding: chunked 是每次抓包都看到的 headers。」

03

HTTP/2 —— 二进制 framing 和多路复用

一个 TCP 连接。许多,在帧级交错。 headers 用共享动态表压缩。1997 到 2022 之间 HTTP 看到的最大协议改造 —— 但仍是 TCP 的囚徒。

HTTP/1.1 的「每 origin 6 个并行连接」一直是变通办法。HTTP/2(RFC 7540,2015) 用每 origin 一个连接替代,内部分成许多并发流。同样的 HTTP 语义 —— 方法、路径、状态码、headers —— 但线格式从文本变成二进制帧。

二进制 framing 层

每个 HTTP/2 消息是的序列。每帧有 9 字节头 (length、type、flags、stream id)和 payload。重要帧类型:

+----+--------------------------------------------------+
| 24 | length(帧 payload 大小,最大 16 MB)              |
|  8 | type(HEADERS、DATA、SETTINGS、WINDOW_UPDATE...)  |
|  8 | flags(END_STREAM、END_HEADERS、...)              |
|  1 | reserved                                          |
| 31 | stream id(0 = 连接、奇数 = 客户端流)            |
+----+--------------------------------------------------+
|    | payload                                          |
+----+--------------------------------------------------+
  • HEADERS —— 请求/响应 headers(HPACK 编码)。
  • DATA —— 请求/响应 body 字节。
  • SETTINGS —— 每连接旋钮(初始窗口大小、最大并发流、 最大帧大小、header table 大小)。
  • WINDOW_UPDATE —— 流控信用。按流和按连接都发。
  • RST_STREAM —— 取消一个流。
  • PING / GOAWAY —— keepalive 和优雅关停。

多路复用工作是因为每个帧带它所属的 stream id。服务器能自由交错来自流 1、3、 5、7 的字节 —— 接收者按 stream id 分流、按序重组每个流的帧。

HPACK —— 为啥 HTTP/2 headers 小

HTTP/1.1 每次请求都发同样的 Cookie:User-Agent:Accept: —— 经常几百字节,经常完全一样。HPACK(RFC 7541)用共享动态表压缩 headers。

两端维护一个同步的(名字、值)对表。常见 header 发一次后用 index 引用 —— 一个字节而不是 200。还有常见 header 的固定静态表 (:method GET:status 200)和字面值的 Huffman 编码。 cookie 重的请求上典型节省:80-95%。

请求 1:发 "cookie: session=abc123" → 加到表 index 62
请求 2:只发 "62" → 一字节代替 22
请求 3:只发 "62" → 还是一字节

HPACK 有个安全皱褶:TLS 压缩上的 CRIME 攻击在精神上适用,所以 HPACK 从不跨 混合了攻击者控制和秘密值的流边界压缩。规范小心,但实践实现要知道这个坑。

窗口的流控

HTTP/2 在 TCP 流控之上有自己的流控。每个流以 64 KB 接收窗口开始。字节被消费时,接收者发 WINDOW_UPDATE 帧给更多信用。这有两个意义:

  1. 对快响应(视频段),默认 64 KB 窗口在一个窗口量之后让发送者停 —— 你得等一个 RTT 拿 WINDOW_UPDATE。在意吞吐的客户端通过 SETTINGS 把初始窗口长到几 MB。
  2. 对慢接收者,流控让接收者节流快发送者而不丢数据 —— 流多路复用到一个 TCP 连接上时必需。

Server push —— 没人出货好的特性

HTTP/2 加了 server push:服务器能在响应 index.html的请求时主动发 app.css。实际上是失败。服务器不知道浏览器已经 缓存了什么,所以推了很多废物;浏览器支持不一致;取消的规则复杂。 Chrome 2022 年禁用 push。现代替代是 HTML 响应里的 Link: rel=preload提示,让浏览器决定。

没消失的 HOL 阻塞

HTTP/2 修了应用级 HOL 阻塞:流在 HTTP 层独立。但 HTTP/2 还跑在 TCP 上, TCP 交付单个有序字节流。一个丢包暂停整个连接等重传 —— 哪怕共享该连接的 6 个流 里 5 个数据已经在接收者上了。这是 TCP 级 HOL 阻塞, 是 HTTP/3 存在的最大原因。1% 丢包下,HTTP/2 能比 6 并行 HTTP/1.1 慢, 因为并行 HTTP/1.1 连接至少把一个流的丢隔离开。

页面加载 —— 1 HTML + 5 子资源,跨 HTTP 版本帧 0 —— 6 个请求等着(1 HTML + 5 子资源)req 1等待中req 2等待中req 3等待中req 4等待中req 5等待中req 6等待中
6 个逻辑请求:index.html、app.css、app.js、logo.png、hero.jpg、font.woff2。每个收到后 20 ms 完成。问题是底层传输怎么把它们渡过 50 ms RTT 链路。
1 / 7
每帧展示同一页面加载(1 HTML + 5 资源)在不同传输上的表现。RTT = 50 ms; 每资源处理 = 20 ms。看握手数和 HOL 阻塞随着我们从 HTTP/1.1(1995) 走到 HTTP/3(2022)而缩小。大跳跃不是 HTTP/2 → HTTP/3 带宽 —— 是丢包下的尾延迟,HTTP/3 的独立 QUIC 流不再互相流血。

要点。「HTTP/2 = 一个 TCP 连接、许多交错的二进制流。 framing 层(HEADERS、DATA、SETTINGS、WINDOW_UPDATE)让多路复用工作; HPACK 通过共享动态表在重复 headers 上省 80%+;流控窗口保护慢接收者。 两个损失:server push 失败、TCP 级 HOL 阻塞还在 —— 一个丢包卡住所有流。」

04

HTTP/3 —— UDP 上 QUIC 上的流

把 TCP 换成 QUIC,HTTP 对内核最后的共享依赖就消失。 流变成传输的一等公民。TLS 不再可选。 你手机从 Wi-Fi 跳到 LTE 也能保持同一会话。

HTTP/2 是 HTTP 在同样 TCP 上的伟大重写。HTTP/3(RFC 9114)走得更远: 保留应用层 framing 类似,但扔掉 TCP 和上面的 TLS 作为分离层。 用 QUIC(RFC 9000)替代 —— 一个端到端设计的传输协议, 多路复用、TLS 1.3、连接迁移都烤进同一握手里。

为啥 UDP、以及 QUIC 不是「只是」 UDP

QUIC 跑在 UDP 上,因为互联网上每个中间盒 —— 运营商 NAT 盒、 公司防火墙、强制门户 —— 都懂 UDP 和 TCP,别的不懂。 全新的传输协议会被静默丢掉。所以 QUIC 付「UDP 税」(小 UDP 头) 在用户态重建可靠性、拥塞控制、按序交付、和流多路复用。

关键的是,QUIC 的流在传输级独立。每个流有自己的序号。 流 3 上一个丢包只强制重传那个流的字节 —— 后续包一到达, 流 1、2、4、5、6 立刻继续流。这把 HTTP/2 的 TCP 级 HOL 阻塞彻底搞死。

TLS 1.3,烤进来

QUIC 不把 TLS 叠在上面 —— 它把 TLS 1.3 集成到握手里。 加密交换和传输参数交换在同一批包里发生。 净效果:对新服务器的新连接花1 RTT(vs TCP 的 1 RTT + TLS 的 1 RTT = HTTP/2 的 2 RTT)。 对之前说过话的服务器,0 RTT —— 第一个请求和握手在同一包里发出,resumption secret 由之前会话推导。

0-RTT 有微妙警告:那些早期字节易受重放攻击。 服务器必须把 0-RTT 请求当幂等(只 GET)或有应用级重放保护。 CDN 替你做了;裸 QUIC 服务器需要显式小心。

连接迁移

TCP 连接由 4 元组(源 IP、源端口、目的 IP、目的端口)标识。你 IP 一变 —— 从 Wi-Fi 到 LTE、NAT 重新绑定、漫游切换 —— 连接就死。 每个长寿 HTTP/2 连接都掉;浏览器开新的、用户看到卡。

QUIC 用每个包里带的Connection ID 标识连接。 你的包能从不同 IP 来,服务器还把它们和你会话关联。 你手机从咖啡厅 Wi-Fi 移到 LTE 时继续流式视频不重连。这是手机的杀手特性。

QPACK —— HPACK 的乱序表亲

HPACK 假设帧按序交付 —— HTTP/2 over TCP 上没事。QUIC 流乱序到达, 所以 HPACK 的动态表更新不能依赖前面帧已经处理。QPACK(RFC 9204)把 header 编码分成控制流(表更新) 和请求流(header 引用),带显式同步。同压缩率、为 QUIC 的乱序现实修正。

ALPN —— 客户端怎么最后说 h3

Curl 跑 curl https://api.example.com/user/42。 客户端怎么知道说 HTTP/1.1、h2、还是 h3?

  1. HTTP/1.1 vs h2:TLS 握手期间,客户端在 ALPN(Application-Layer Protocol Negotiation)TLS 扩展里列它支持的协议。服务器挑一个、放进 ServerHello。 TLS 完成时,双方知道说哪个 HTTP 版本。
  2. HTTP/3:客户端先用 TCP+TLS 连接、拿到Alt-Svc 响应 header: Alt-Svc: h3=":443"; ma=86400。 这告诉客户端「下次,试 443 端口上的 QUIC」。客户端按 max-age 缓存。 后续访问,它把 QUIC 握手和 TCP 赛跑,用先连上的那个。

采用 —— 你哪能看到它

Cloudflare 2024 数字:他们网络上 ~30% 的 HTTPS 流量是 HTTP/3。 Google、Meta、Akamai、Fastly 都提供。每个主流浏览器都支持。 你看不到的地方:大部分内部基础设施。 你自己负载均衡器后面的 backend 几乎必然是 h2。 你服务器之间的中间盒不懂 QUIC;你的监控工具可能解析不了; 你的追踪库假设 TCP。HTTP/3 是 CDN 边缘协议,h2 留给所有内部。

要点。「HTTP/3 跑在 QUIC 上、QUIC 跑在 UDP 上。 流在传输级独立 —— 无 TCP HOL 阻塞。TLS 1.3 集成到握手里: 新的 1 RTT、已知服务器 0 RTT。Connection ID 的连接迁移让手机 跨 Wi-Fi → LTE 保持会话。QPACK 替代 HPACK 处理乱序交付。 客户端通过 Alt-Svc header 挑 h3。互联网流量 ~30% 用,但只在 CDN 边缘 —— 你的内部服务还在 h2 上。」

05

速查表

6 道值得能冷讲清楚的核心问题、5 个 code review 时一眼看出来的红旗。

DNS 解析实际干什么、在哪缓存?

libc 里的 stub resolver 往 /etc/resolv.conf 里的递归 resolver 发 UDP 查询。缓存未命中时递归 resolver 走 根 → TLD → 权威服务器、 带 TTL 返回答案。每层都缓存:浏览器、OS(systemd-resolved / nscd)、 递归 resolver、甚至权威服务器自己。300 秒 TTL 意思是改完之后世界能看你旧记录 最多 5 分钟。负响应(NXDOMAIN)也缓存,按 SOA 的负 TTL。

队头阻塞是什么、哪些 HTTP 版本受害?

HOL 阻塞是队头慢或丢的消息阻止后面消息处理。HTTP/1.1 pipelining 受 应用级 HOL 影响(响应 1 慢卡住响应 2-N)。HTTP/2 用多路复用修了那个, 但还骑 TCP —— 所以一个丢包暂停共享该连接的每个流(TCP 级 HOL)。 HTTP/3 免疫,因为 QUIC 流在传输级独立。

HPACK 干啥让 HTTP/2 headers 小?

HPACK 在两端维护一个同步的(header 名、值)对动态表。 第一个请求后,cookie: session=abc... 进表; 后续请求按 1 字节 index 引用,而不是重发那 200 字节字符串。 配合常见 headers 的静态表(:method GET:status 200) 和字面值的 Huffman 编码,cookie 重的请求上省 80-95%。 QPACK 是 HTTP/3 的 QUIC 友好变体。

HTTP/3 在生产里啥时候明显赢过 HTTP/2?

3 种场景。(1)丢包网络:1% 丢包下 HTTP/3 显著更快,因为丢包只卡自己的流, 不卡所有六个。(2)对已知服务器的新连接:0-RTT 意思是第一请求跟握手一起发 —— 省整一个 RTT,移动冷启动大赢。(3)切网络的移动客户端: 连接迁移让会话跨 Wi-Fi → LTE 不重连。干净有线链路上收益不大。

为啥浏览器在 HTTP/1.1 下对一个 origin 开 6 个连接?

每个 HTTP/1.1 连接一次只能扛一个未完成请求(pipelining 死了)。 要并行加载一页 50+ 个资源,浏览器对同一 origin 开多个 TCP 连接。 事实上限是 6 —— 加载速度和不 DDoS 服务器之间的折中。 HTTP/2 下这个数掉到 1,因为多路复用让并行连接没必要(且主动有害 —— domain sharding 伤 h2)。

ALPN 是什么、客户端怎么最后说 h3?

ALPN(Application-Layer Protocol Negotiation)是个 TLS 扩展, 客户端列支持的协议(h2http/1.1)、 服务器在 ServerHello 里挑一个。那覆盖 HTTP/1.1 vs HTTP/2。 HTTP/3 的话,客户端先 TCP+TLS 连接,在响应里看到 Alt-Svc: h3=":443"; ma=86400 header, 缓存它,下次访问把 QUIC 握手和 TCP 赛跑。先连上的赢。

Code review 红旗

  • JVM 设了 networkaddress.cache.ttl=-1永远缓存 DNS。指向负载均衡器的 round-robin DNS 被钉死在一个 IP 上; 那个 backend 死了,JVM 不知道。设成实际记录 TTL(通常 30-300 秒)。
  • 硬编码 resolver IP。把应用直接指 1.1.1.1 或 8.8.8.8 绕过本地缓存、 把你钉在一个外部供应商。那个供应商出故障时(会出),你的栈陪它死。用系统 resolver。
  • 应用期待 HTTP/2 server push 工作。Chrome 2022 年禁用 push; 其他浏览器多年不一致。改用 HTML 响应里的 Link: rel=preload 提示 —— 让浏览器决定它实际要 fetch 什么。
  • 每次请求都带大 Cookie: header、走 HPACK 之前的代码路径或无 HTTP/2 backend。每个资源请求都发 4 KB cookie 累得快 —— 50 个资源 × 4 KB = HTTP/1.1 上每页加载 200 KB 开销。 HTTP/2 + HPACK 上同一 cookie 在第一次发后只花 ~1 字节。
  • HTTP/1.1 pipelining 上叠自定义协议。Pipelining 因为代理坏和 HOL 阻塞没部署。 一个「聪明」设计假设你能在一个连接上发 1000 个请求、乱序流响应回来, 会在你和服务器之间有坏代理的瞬间失败。用 HTTP/2 流或 WebSocket。