跳转至

01 一次请求经过了什么

本章目标:能沿着“客户端 → 网络 → 服务端”的方向讲述 HTTP 请求,并区分地址、端口、域名、协议和超时。先不要求 Kubernetes 或 Go 知识。后面出现的大多数代理故障,都是这几个环节的组合。

从地址开始

在浏览器打开 https://shop.example.com:443/products?id=7,实际上给出了几类信息。https 指定使用 TLS 保护的 HTTP;shop.example.com 是域名;443 是服务端监听的 TCP 端口;/products 是路径,id=7 是查询参数。域名面向人,IP 地址面向网络,端口帮助同一台机器上的操作系统找到接收连接的程序。一个 IP 上可以同时运行 Web 服务、SSH 和数据库,因为它们监听不同端口。

DNS 把域名解析成地址。解析出的地址可能是一台服务器,也可能是负载均衡入口。DNS 并不告诉你业务是否正常,也不保证响应请求的进程有正确路由。ping 通也不等于 HTTP 通:ICMP 探测、TCP 建连和 HTTP 处理是不同层面的事情。

在 HTTP/1.1 over TCP 的情形下,客户端先建 TCP 连接。如果使用 HTTPS,再做 TLS 握手,验证证书并协商加密参数,之后才交换 HTTP 内容。HTTP/2 通常在一条连接里复用多个流;HTTP/3 基于 QUIC,不能简单套用 TCP 的全部描述。本书最初的实验使用明文 HTTP/1.1,先把转发关系弄清,再学习 TLS。

读一段 HTTP

GET /products?id=7 HTTP/1.1
Host: shop.example.com
Accept: application/json

第一行是方法、请求目标和版本。头部为服务端提供附加信息。空行结束头部;某些请求还有请求体。响应可能是:

HTTP/1.1 200 OK
Content-Type: application/json

{"id":7,"name":"book"}

200 是 HTTP 层的结果。收到 404 表示某个 HTTP 服务确实回答了,但它没有找到所需资源;它不能单独证明请求到达了你期望的业务服务。代理也能生成响应。没有收到 HTTP 状态码而是“连接被拒绝”,则问题发生得更早:可能没有进程监听目标端口。理解“谁生成了响应”是之后排障的关键。

Host 尤其重要。多个网站可以共享一个监听地址,代理通过 Host 决定选哪个虚拟主机。HTTP/2 对应的信息通常放在 :authority 伪头里。直接请求 http://127.0.0.1:18000/ 时,Host 通常是 127.0.0.1:18000;而实验路由可能要求 book.example。这时应使用:

curl -v -H 'Host: book.example' http://127.0.0.1:18000/

-H 设置头部,-v 把建连和收发头部的细节输出到终端。它们改变的是不同事情:连接仍然去本机 18000 端口,只是 HTTP 请求中声明目标网站为 book.example。HTTPS 还涉及 TLS SNI,单独修改 Host 不一定能完成正确证书验证;第 20 章会继续展开。

一条业务调用可以消耗多个时间预算

假设客户端总超时 2 秒,代理连接后端耗费 100 毫秒,后端处理耗费 1.5 秒,再返回响应。网络、排队、业务计算都消耗同一段墙钟时间。代理的 connect timeout 只限制连接建立,route timeout 通常约束请求整体的一段生命周期,二者含义不同。流式请求、重试和每次尝试超时还有额外规则,必须看具体协议和配置。

重试也不是免费补救。如果一个请求把库存减一,第一次其实成功了,只是响应丢失,盲目重试就可能重复扣减。可安全重复执行称为幂等;幂等键可以帮助业务识别重复。网关能发送第二次尝试,却不能凭空判断业务副作用是否重复。后面配置重试时,始终同时考虑请求语义和总时间预算。

用一个小实验建立观察习惯

下面命令只访问本机一个特意选择的端口,不要求网络服务已启动:

curl -v --connect-timeout 2 --max-time 3 http://127.0.0.1:18000/

--connect-timeout 2 限制连接阶段,--max-time 3 限制整个命令。服务尚未启动时可能看到 connection refused;已经启动实验一时会得到它的响应。请记录真实结果,先不要把它判为“配置对/错”,而是问:是否建连、是否收到 HTTP、响应来自谁?这三条记录能避免把网络故障误诊为路由故障。

常见混淆

localhost 指当前网络环境中的本机。在笔记本终端里是笔记本,在容器里是那个容器的网络命名空间。代理容器配置后端为 127.0.0.1,通常会连接到它自己,而不是另一个后端容器。实验一的独立 Compose 容器通过服务名访问彼此;Kubernetes 则通常使用 Service 与实际端点。实验二特意让玩具后端与 Envoy 共享网络命名空间,因此可以使用数值 loopback 地址,这是显式配置后的例外。

连接与请求也不相同。一条连接可能承载多个请求,HTTP/2 还可并发多个流。因此“按请求分配流量”和“按连接分配流量”得到的效果可能不同。长连接本身不会保证路由永远固定;实际行为由协议和代理的路由时机共同决定。

练习与参考答案

  1. DNS 正常、TCP 连接成功、收到 404,下一步优先检查什么?
  2. 为什么在容器里把后端设为 localhost 经常失败?
  3. 请求超时后,为什么不能一律重试三次?
参考答案
  1. 检查响应来自代理还是后端,并核对 Host、路径和路由匹配。此时不能仅凭 404 断言 DNS 或后端进程坏了。
  2. localhost 指代理容器自身;另一个容器应通过共同网络上的服务名和内部端口访问。
  3. 需要考虑总超时、服务压力、请求幂等性和已发生但未收到确认的副作用。重试可能放大故障和重复写入。

继续:为什么需要代理和网关。来源:HTTP 语义 RFC 9110、HTTP/1.1 RFC 9112、TLS 1.3 RFC 8446。