# HTTP的代理服务
30 秒速记
- 代理 = 在客户端和源站之间插一个或多个中间人,直连变成一条链
- 起点客户端,终点源服务器(
origin server),中间是代理服务器 - 每一跳只跟相邻节点说话,不越级;响应原路倒着回来
- 每一跳是独立的
TCP连接,各有各的超时和重试 - 代理不是透明管道:能改头、能缓存、能重试,排查要一跳一跳看
代理就是在浏览器和源站之间加了中间人,原来一根线变成了一条接力链。 比如「浏览器 → 代理 A → 代理 B → 源站」,浏览器只跟 A 打交道,A 收到请求后自己再去连 B,响应按反方向一跳跳传回来。这里要分清的是,每一跳都是单独建的连接,各自有超时和重试配置,所以链上任何一跳出问题,浏览器看到的都只是「慢了」或者「502」。排查的时候我会让每一跳都打出同一个请求 ID,一段一段对时间。
引入 HTTP 代理后,原来简单的双方通信就变复杂了一些,加入了一个或者多个中间人,但整体上来看,还是一个有顺序关系的链条,而且链条里相邻的两个角色仍然是简单的一对一通信,不会出现越级的情况。

链条的起点还是客户端(也就是浏览器),中间的角色被称为代理服务器(proxy server),链条的终点被称为源服务器(origin server),意思是数据的“源头”“起源”
原理拆解: 代理链可以表示为“客户端 → 代理 A → 代理 B → 源服务器”,响应再沿原路径反向返回。这里的“不越级”描述的是单次连接关系:客户端把请求交给代理 A,代理 A 再建立到代理 B 的连接,而不是把同一条底层连接原封不动地延伸到源站。每一跳都可以拥有独立的 TCP 连接、超时和重试策略;应用层消息则由中间节点接收后继续转发。代理还可能通过 Via 等字段记录消息经过的中间环节。
最小验证: 输入是一条经过两个代理的请求及其响应,要验证各节点只和相邻角色交换消息,并且响应按链路逆序返回。
GET /products/42 HTTP/1.1
Host: origin.example
Via: 1.1 proxy-a, 1.1 proxy-b
HTTP/1.1 200 OK
Content-Type: application/json
Via: 1.1 proxy-b, 1.1 proxy-a
Content-Length: 24
{"id":42,"stock":true}
请求中的 Host 指向目标源站,Via 展示代理依次追加的转发轨迹;响应从源站返回后,先经过靠近源站的 proxy-b,再经过 proxy-a 到达客户端。真实产品是否添加或保留 Via 取决于代理配置,因此缺少该字段不能证明没有代理。
边界与排查: “有序链条”不等于所有代理都不会改变消息。代理可以增删首部、缓存响应,或因重试把一次下游请求转换成多次上游请求;负载均衡器也可能为不同请求选择不同源站。排查链路时应结合代理访问日志、请求标识、连接信息和抓包结果,分别确认每一跳的接收与发送时间,不能仅凭浏览器里看到的源站域名推断客户端进行了直连。
面试官追问
追问 1架构图上浏览器同时连着两个代理和源站,说这是一条两级代理链,对吗?
不对。代理链是串行的,浏览器只连第一跳,第一跳再连第二跳。如果浏览器还直接连了源站,那是另一条路,跟这条链没关系。
追问 2经过两级代理后偶尔超时,怎么定位卡在哪一跳?
入口生成一个 X-Request-Id 一路透传,每一跳日志都记下收到和转发的时间,拼起来就能看出时间耗在哪段。只看浏览器的总耗时是定位不了的。
追问 3一次下单,源站日志里出现了两条一模一样的请求,可能是什么原因?
很可能是中间某一跳超时重试了。Nginx 的 proxy_next_upstream 默认遇到 error 和 timeout 会换上游重试,1.9.13 之后默认不重试 POST,但配了 non_idempotent 或者其他网关、SDK 自带重试时照样会打两遍。下单接口要靠幂等键兜底,同时把链路上每一层的重试配置都翻一遍。
追问 4请求经过代理时,代理是把浏览器那条连接接到源站去吗?
普通的 HTTP 代理不是。它先把请求完整收下来,再自己开一条新连接发给下一跳,所以能改头、能换上游、能缓存。只有 CONNECT 隧道这种情况,代理才是原样搬运字节。
# 代理服务
30 秒速记
- 代理自己不生产内容,只转发上下游的请求和响应
- 双重身份:对下游(浏览器)是服务器,对上游(源站)是客户端
- 转发不是逐字节复制:会改请求目标、加
Via/X-Forwarded-For、复用上游连接 - 缓存命中时可以根本不访问源站
HTTPS走CONNECT隧道时,代理不解密就只能看到密文,改不了内容
代理就像楼下的便利店:对你来说它是卖家,对超市来说它是买家,自己不生产任何东西。 浏览器把请求发给代理,代理以服务器的身份接住,再以客户端的身份去请求源站,拿到响应再转回来。但它不是一根管子,正向代理收到的是 GET http://a.com/x 这种完整 URL,转给源站时会改成 GET /x,还会顺手加上 Via、X-Forwarded-For。如果开了缓存,命中时直接就回了,源站完全不知道有这次请求。
“代理”这个词听起来好像很神秘,有点“高大上”的感觉。
但其实 HTTP 协议里对它并没有什么特别的描述,它就是在客户端和服务器原本的通信链路中插入的一个中间环节,也是一台服务器,但提供的是“代理服务”。
所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求
打个比方。
之前你都是从超市里买东西,现在楼底下新开了一家 24 小时便利店,由超市直接供货,于是你就可以在便利店里买到原本必须去超市才能买到的商品。
这样超市就不直接和你打交道了,成了“源服务器”,便利店就成了超市的“代理服务器”
原理拆解: 代理的双重身份体现在两段独立交互中。下游连接里,代理监听端口、解析请求并向客户端写回响应,因此是服务器;上游连接里,它主动选择源站、建立连接并发送请求,因此是客户端。这里的“转发”也不要求逐字节复制:代理可以重写目标地址、补充转发首部或复用上游连接,但响应内容的最终来源仍是源服务器或代理中已有的缓存。
最小验证: 输入为客户端发给代理的请求,以及代理据此发给源站的请求;要验证同一代理在两段通信中使用不同身份。
GET http://origin.example/profile HTTP/1.1
Host: origin.example
User-Agent: demo-client
GET /profile HTTP/1.1
Host: origin.example
Via: 1.1 edge-proxy
X-Forwarded-For: 192.0.2.10
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5
Alice
第一段使用绝对形式的请求目标,常见于客户端访问显式正向代理;第二段是代理作为上游客户端向源站发送的请求。X-Forwarded-For 表示代理声明的原客户端地址,但它只是普通首部,只有来自可信代理时才可用于安全判断。最后的响应由代理接收,再转交下游客户端。
边界与排查: 代理不总能读取应用层内容。处理 HTTPS 隧道时,显式代理可通过 CONNECT 建立字节通道,此后若不终止 TLS,它只能转发密文。缓存命中时也可能完全不访问源站,所以“每个请求都有对应的上游请求”并不成立。工程验证应同时查看下游访问日志和上游日志,并用统一请求标识关联两段记录;若只有下游记录,应继续检查缓存、访问拒绝、连接失败和超时。
面试官追问
追问 1浏览器收到的 200 是代理回的,那内容就是代理生成的吗?
不一定。响应方是代理,内容来源要么是源站,要么是代理缓存的副本。看响应里的 Age、X-Cache 这类头就能判断是不是缓存来的。
追问 2代理日志里有请求,源站日志里没有,一定是上游挂了吗?
先查是不是缓存命中,命中了本来就不回源。然后再看是不是被代理的访问控制拦了,或者回源失败。这三种情况在代理日志里的状态和上游耗时字段是不一样的。
追问 3公司代理想按 URL 路径过滤 HTTPS 网站的内容,做得到吗?
普通 CONNECT 隧道做不到,代理只知道你要连哪个域名和端口,后面都是 TLS 密文。想看路径就得在代理上终止 TLS,给员工电脑装公司根证书,这就是常说的中间人解密,涉及隐私和合规问题。
追问 4前端本地开发用的 devServer.proxy 算哪种代理?
算反向代理。浏览器以为在请求 localhost:3000/api,其实是本地开发服务器作为客户端去请求后端,所以浏览器那边没有跨域问题。要注意后端看到的 Host 默认还是 localhost,有校验的话要配 changeOrigin: true。
# 代理的作用
30 秒速记
- 代理在中间,对客户端藏住后端有几台、在哪,对后端挡住外部直连,所有流量都过它
- 负载均衡:轮询、加权、最少连接、一致性哈希,把请求分到多台源站
- 健康检查:定期探活,挂掉的踢出去,恢复了再加回来
- 顺带能做:限流 / 封
IP、过滤改写、TLS卸载、内容缓存 - 代价是它成了单点:容量、配置、健康检查阈值出错,后端再好也对外不可用
