你在浏览器里访问一个网站时,地址栏里只有一个域名。页面打开得很正常,你也很容易以为:浏览器刚才就是连上了那台运行应用的服务器。
可现实往往完全不是这样。
浏览器连到的第一台机器,可能只负责接收连接和检查请求;它再把请求交给负载均衡器,负载均衡器从几十个应用实例里挑一个,应用又通过出口代理访问外部服务。最后真正生成响应的那台机器,可能从没和你的浏览器建立过连接,甚至不知道你的真实 IP 地址。
更反直觉的是,客户端通常也不需要知道后端到底有几台机器。今天后端有 3 台,明天扩到 30 台,只要公开入口没有变化,浏览器仍然访问同一个地址。
这不是障眼法,而是代理带来的连接分层:一个看似简单的 HTTP 请求,被拆成若干段独立连接,每一段都可以有自己的协议、超时、加密和故障。代理就在这些连接之间接收消息、做出决定,再把消息送往下一跳。

这一章我们不急着记“正向代理”和“反向代理”的定义。先顺着一次真实请求走完,再看每类代理究竟替谁解决问题、获得了什么能力,又付出了什么代价。
假设浏览器访问 /orders/42,请求路径是:
浏览器 → 边缘代理 → 内部网关 → 订单服务从用户感受上看,这只是一次请求。但从网络连接上看,至少可以拆成三段:浏览器到边缘代理、边缘代理到内部网关、内部网关到订单服务。每一段都有自己的连接端点,前一段断开,并不等于后一段一定同时断开;前一段使用 HTTP/2,后一段也可能使用 HTTP/1.1 或 gRPC。
这里有三个基础角色:
这三个词说的是中间节点此刻扮演的角色,不一定对应三套完全不同的软件。同一个代理进程,可以对普通 HTTP 请求做消息转发,对 HTTPS 请求建立隧道,也可以在另一个监听端口上充当反向代理。
判断一个中间节点时,先问两个问题:是谁选择了它?它是否终止并理解了 HTTP 或 TLS?这比只看产品名称更可靠。
HTTP 代理如果终止了这一段 HTTP 连接,就能读到请求方法、目标、头字段和状态码,因此可以改写路径、添加头字段、缓存响应、按 Cookie 路由。它处理的是 HTTP 消息。
隧道则更接近一根双向管道。建立完成后,它主要转发字节,不随意改写隧道里的消息。比如显式正向代理可以用 CONNECT 建立到目标主机 443 端口的通道,之后浏览器与目标站点在通道内部完成 TLS 握手。代理知道连接目标,却看不到加密后的具体 HTTP 路径和正文。
这个差别会直接决定能力边界:看不见 HTTP,就不能按 URL 做缓存和路由;能看见 HTTP,就意味着代理接触到了更多敏感数据,也承担了更大的安全责任。
想象一家公司规定:办公电脑不能直接访问互联网,所有外部请求都必须先交给公司的出口代理。目标网站看到的连接来源是公司代理,而不是每一台员工电脑。
这就是正向代理最典型的视角:代理站在一组客户端前面,代表它们访问外部资源。目标服务器可以知道有一个代理来过,却未必知道代理后面是哪台客户端。
显式代理需要客户端知道代理地址。这个配置可以手动写入浏览器或操作系统,也可以通过自动配置规则下发。发送普通 HTTP 请求时,客户端会把完整目标交给代理,让代理知道应该连接哪个站点;访问 HTTPS 站点时,客户端常先请求代理建立一条 CONNECT 隧道。
CONNECT shop.example.test:443 HTTP/1.1
Host: shop.example.test:443
Proxy-Authorization: Basic dXNlcjpwYXNz代理允许后返回成功状态,连接就切换为双向隧道。接下来流过的是 TLS 字节,普通隧道代理不会替浏览器解密站点内容。
显式代理的好处是边界清楚:客户端知道请求经过谁,代理也容易实施身份认证、访问控制和审计。代价是客户端需要配置;某些应用不遵守系统代理设置,某些协议也未必能通过 HTTP 代理。
所谓透明代理,通常是网络设备通过路由重定向,把原本要直接发往目标的流量送到代理。应用没有显式填写代理地址,所以“透明”说的是接入方式,不代表代理对所有参与者绝对不可见,更不代表它可以无条件理解 HTTPS 内容。
这点很容易被讲错。对明文 HTTP,拦截代理可以解析并转发消息;对 HTTPS,它如果只转发加密流量,就看不到里面的请求。如果要检查加密内容,就必须在代理处终止 TLS,并让客户端信任代理签发或使用的证书。这已经不是单纯的流量转发,而是一次明确的信任扩张。
透明与显式也不是正向与反向的另一组同义词。前者描述客户端如何接入代理,后者描述代理站在哪一侧、代表谁。服务网格同样可以用流量重定向,让应用不改代码就把服务间请求交给本机代理。

正向代理确实会让目标服务器直接看到代理的地址,但“目标看不到客户端 IP”不等于“客户端匿名”。代理本身知道客户端是谁,HTTP 头、登录账号、浏览器指纹和应用数据也可能继续暴露身份。如果代理主动传递客户端信息,目标服务仍可能获得原始地址。
因此正向代理的主要价值通常是统一出口、控制访问、复用连接和记录审计,而不是一句笼统的“隐藏 IP”。开放一个允许任意目标和任意端口的代理尤其危险:它可能被拿来扫描内网、转发攻击,或者借 CONNECT 绕过原本的网络限制。
现在把视角转到网站这一边。
用户访问 api.example.test,DNS 把它指向一个公开入口。入口收到请求后,可能把 /users 交给用户服务,把 /orders 交给订单服务;订单服务有多个实例时,入口还要从中挑一台。对浏览器来说,公开入口就是它正在访问的服务器。
这就是反向代理:它在客户端面前表现得像源站,真正的后端则藏在它身后。浏览器不需要配置代理,也不需要知道内部服务地址。

在 HTTP 的角色定义里,gateway 就是 reverse proxy:它面向客户端接收请求,再把请求翻译或转发给一个或多个后端。这里的“翻译”不一定很夸张,可能只是把外部 HTTPS 转成内部 HTTP,也可能把 HTTP 请求转换为后端使用的另一种应用协议。
现代工程里的 API 网关 是更具体的产品角色。它通常建立在反向代理能力之上,再集中处理认证、限流、配额、路由、协议转换和接口观测。于是一个组件既可以是 HTTP 意义上的 gateway,也可以承担团队语境里的 API 网关职责。
名称相近,关注点却不同:
它们可以由同一套软件完成,也可以拆成多层。真正需要弄清楚的是每一层拥有哪项策略,以及流量能否绕过它。
下面的 Nginx 配置把客户端请求转给两个应用实例,同时保留原始主机和入口协议,并用入口直接观察到的连接来源重建客户端地址字段。示例刻意只展示“公网客户端直接连接这一层代理”的情况,方便看清信任从哪里开始。
events {
worker_connections 1024;
}
http {
upstream app_pool {
least_conn;
server 10.20.0.11:8080 max_fails=3 fail_timeout=10s;
server 10.20.0.12:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
server_name api.example.test;
location
这段配置解决的是“请求如何转进去”,还没有自动解决身份信任、缓存污染、请求重试和后端加密。代理功能越集中,越不能把“配置能跑”误当成“边界已经安全”。
客户端不知道后端有几台机器,关键就在于选择后端的动作发生在代理内部。代理维护一个上游池,每次请求到来时,从健康目标中选一个。
最容易理解的是轮询:第一条请求给 A,第二条给 B,第三条给 C,然后重新开始。但“每台收到相同请求数”不等于“每台负载相同”。如果某些请求 10 毫秒结束,另一些要跑 10 秒,单纯轮询可能让慢请求碰巧挤在一台机器上。
没有一条策略在所有场景都最好。真正的选择依据是请求成本、实例差异、连接复用方式,以及应用是否把状态留在单机内存里。
代理不能把请求发给已经失效的实例,所以会做健康判断。被动检查根据真实请求的连接失败、超时或错误来降低目标可用性;主动检查则由代理定时访问专门端点。
问题是,/health 返回 200 只说明这个检查请求成功了。应用可能还能返回 200,却已经连不上数据库;也可能依赖服务短暂抖动,就被所有代理同时摘除。检查太浅会放过坏实例,检查太重又会把下游波动放大成大面积下线。
更稳妥的做法是区分“进程活着”和“当前可以接流量”,设置连续成功与失败阈值,并让摘除、恢复、优雅停机配合部署过程。
代理发现一次上游失败后,可以换一台重试。这对连接建立失败或幂等读取很有帮助,但对已经被后端处理的写请求很危险。代理看到的是连接中断,不一定知道订单是否已经创建;贸然重试可能生成两个订单。
即使都是读取,请求链上每一层都重试也会放大流量。边缘代理重试两次,内部网关再重试两次,一次用户请求最坏可能变成多次后端调用。重试必须受总超时、次数和幂等条件约束,还要配合熔断和容量保护。

反向代理拿到响应后,可以把副本存下来。下一次遇到等价请求时,它直接返回缓存,不再访问应用。因为这份副本可能服务多个用户,所以它属于共享缓存。
速度提升很好理解:少走一段网络,少执行一次应用逻辑,也少查一次数据库。但真正困难的从来不是“把响应存起来”,而是判断两次请求是否真的可以共用同一个响应。
共享缓存至少会用请求方法和目标 URI 选择响应,实际系统还可能把主机、查询参数或部分头字段加入缓存键。如果源站会根据 Accept-Encoding、语言或其他请求头返回不同内容,就需要让缓存知道这些差异。
最危险的情况是个性化内容被错误共享。用户 A 的账户页面如果只按 URL 缓存,用户 B 请求同一路径时就可能收到 A 的数据。带认证信息的请求、标记为私有的响应和禁止存储的响应,都需要按规则谨慎处理,而不是因为状态码是 200 就直接缓存。
缓存副本仍在新鲜期时,可以不联系源站直接使用。过期后不代表必须下载完整响应,它可以带验证器询问源站“这份内容变了吗”;没有变化时,源站只需确认原副本仍有效。
主动清除缓存解决的是另一类问题:内容刚发布就必须立刻更新,但旧副本的新鲜期还没结束。此时要通过版本化地址、清除接口或缩短缓存时间处理。缓存时间越长,命中率通常越好,发布后的陈旧窗口也越大。
代理缓存还改变了故障表现。有些系统在源站暂时不可用时允许返回陈旧副本,用户至少还能看到旧内容;代价是业务必须接受“可用但不最新”。这正是缓存的核心权衡:用一致性的确定程度,换延迟、带宽和可用性。

下一章会专门拆解缓存控制字段、验证器和缓存键。此处先记住一条:代理缓存不是一个自动加速开关,它是一套“哪些响应能被谁复用多久”的规则。
用户看到地址栏里的安全连接标志,很容易理解成“数据一直加密到应用进程”。但如果入口代理做了 TLS 终止,浏览器建立的加密连接其实到代理就结束了。代理解密 HTTP 请求,再决定怎样连接后端。
常见路径有三种:
TLS 直通:客户端 =====加密=====> 后端
边缘终止:客户端 =====加密=====> 代理 -----明文-----> 后端
重新加密:客户端 =====加密=====> 代理 =====新 TLS=====> 后端直通或隧道模式下,代理不解密应用数据,证书和 TLS 会话由后端处理。这样减少了中间节点接触明文的机会,但代理无法按 HTTP 路径做路由、缓存、头字段改写或 Web 应用防护。它通常只能根据连接层信息,或 TLS 握手阶段可见的有限信息做选择。
在入口终止 TLS,可以集中管理证书、协议版本和加密套件,并让代理理解 HTTP 后做路由、限流和观测。代价非常直接:代理拥有解密流量的能力,代理到后端之间如果使用明文,内网中的窃听或错误路由仍可能暴露数据。
“在内网所以可以明文”不是协议保证,只是一项风险判断。只要网络跨主机、跨租户或跨不可信区域,就应该认真评估重新加密。
代理到后端建立的是第二条 TLS 连接,不是把浏览器那条连接原样续上。代理既是前一段的服务器,也是后一段的客户端,因此它需要验证后端证书、正确发送目标名称,并管理自己的信任根。如果关闭后端证书验证,链路虽然有加密,代理却无法确认自己连的是不是正确后端。
服务间需要双向身份时,还可以使用 mTLS。它能让双方通过证书确认身份,但证书签发、轮换、撤销和时钟问题都会进入运维范围。安全能力变强的同时,系统也多了一套必须可靠运行的身份基础设施。

代理替客户端建立了到后端的新连接,所以后端在网络层直接看到的来源地址,通常是最后一跳代理。为了保留原始信息,代理会把它写进 HTTP 头字段。
常见字段各自回答不同问题:
Forwarded 可以记录原始客户端、代理入口、原始主机和协议,是标准化格式。X-Forwarded-For 常用来记录客户端与代理链地址,但它不是标准字段。X-Forwarded-Proto 告诉后端客户端访问入口时使用的是 HTTP 还是 HTTPS。X-Forwarded-Host 保留客户端原来请求的主机。Via 描述消息经过的 HTTP 中间节点与协议版本,主要用于转发能力和循环诊断,不等同于真实客户端身份字段。看起来只是几行元数据,安全问题却藏在“谁写的”里面。
任何客户端都可以发送下面这样的请求:
GET /admin HTTP/1.1
Host: api.example.test
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: https如果入口代理无条件保留它,后端又无条件相信最左边的地址,攻击者就可能把自己伪装成内网客户端。类似地,伪造协议字段可能影响重定向地址、安全 Cookie 判断或绝对链接生成。
正确思路不是“永远取第一个 IP”,而是先定义可信代理边界:公网入口应忽略或覆盖客户端提供的敏感转发信息,再由受信代理追加经过验证的连接来源;后端从最靠近自己的方向跳过已知代理,找到第一个不受信的上游地址。
还有一个常被漏掉的条件:后端必须只接受来自受信代理的连接。如果攻击者可以绕过入口直接访问后端,他就能自己构造整条头字段链,前面的清洗规则全部失去意义。

后端可能根据 Host 选择虚拟站点,根据原始协议生成跳转地址,根据客户端地址限流。代理如果写错这些值,结果不一定是明显报错,也可能是把用户跳转到内部域名、生成错误回调地址,或者让限流把所有人都当成同一个代理 IP。
所以转发头是一份跨边界的数据协议。代理和应用必须约定字段由谁清洗、谁追加、信任几跳,并通过测试验证多层代理时的行为。
现代请求很少只经过一层代理。一条常见链路可能是:
浏览器
→ CDN 或边缘代理
→ 云负载均衡器
→ API 网关
→ 服务网格入口
→ 服务旁代理
→ 应用进程每一层都有存在理由:边缘层吸收公网流量,负载均衡器分配连接,API 网关执行接口策略,服务网格管理服务间通信。问题是,每增加一跳,就增加一份配置、一个超时、一组缓冲区和一个可能失败的进程。
假设用户最多愿意等待 3 秒,入口却给内部网关 5 秒,内部网关又给应用 10 秒。用户连接早已断开,后面的工作还在继续,占用连接池和数据库资源。高峰期这些“没人再等待的请求”会拖慢真正有用的请求。
更合理的做法是先确定总时间预算,再为排队、连接、首字节和重试分配子预算。越靠近后端,剩余时间通常越少。请求上下文还应能传播取消信号,避免客户端离开后继续执行昂贵工作。
每层代理各写一份日志并不自动等于可观测。没有稳定的请求标识和时间字段时,你只会得到几堆互相对不上的记录。
入口可以生成请求 ID,并由受信链路向后传递;每一跳记录上游目标、等待时间、响应状态和重试次数。分布式追踪进一步把一次请求拆成多个时间片,让你看见延迟究竟发生在连接、代理排队、应用还是数据库。
但头字段同样可能由外部伪造。外来请求 ID 可以保留用于关联,也可以重建内部 ID;无论哪种方式,都应避免让攻击者用超长或恶意格式污染日志。
代理可以先把请求体收完再交给后端,也可以边收边转发。缓冲能保护慢后端、方便重试,并让客户端连接更快释放;大文件会占用代理内存或磁盘,也可能让应用迟迟收不到第一段数据。流式转发延迟更低,却会把慢客户端和后端连接更紧地绑在一起。
这类配置没有通用答案。上传接口、事件流和普通 JSON API 对缓冲的要求完全不同,应该按请求体大小、流式需求和故障恢复方式分别配置。
把后端藏在反向代理后面,确实减少了直接暴露面。代理可以限制请求大小、规范化头字段、执行认证与限流,也可以在到达应用前拒绝明显恶意流量。但这不等于后端自动安全了。
代理和后端可能使用不同的 HTTP 解析器。如果它们对消息边界、重复头字段或非法格式的处理不一致,攻击者构造的一段字节可能被代理理解成一个请求,却被后端理解成两个。安全检查只看到了前一个,藏在后面的请求仍可能进入应用。
因此代理链应尽量统一协议规则,拒绝模糊格式,及时更新实现,并避免随意转发连接级头字段。安全不是“前面放一个 WAF”就结束了,代理与后端对同一消息的理解必须一致。
API 网关集中验证令牌,可以避免每个服务重复实现一遍认证。反过来,一条错误的放行规则也可能影响所有服务;如果内部服务可以被绕过网关直接访问,它仍需要判断请求来自何处,敏感操作甚至要在服务内再次校验权限。
限流也有类似权衡。按客户端 IP 限流实现简单,但共享出口会让许多用户挤在同一地址后;只在单台代理内计数速度快,多实例之间却可能各放行一份额度。策略必须和真实身份、代理规模与一致性需求一起设计。
边缘代理处理的是最不可信的输入,又持有证书、路由规则和后端访问能力。它需要最小权限、及时更新、资源上限、受控管理接口和完整审计。源站网络还应限制为只接受预期代理,避免攻击者绕过访问控制与头字段清洗。
正向代理则要严格限制允许的客户端、目标和端口。尤其是能建立任意隧道或访问内网地址的代理,很容易变成 SSRF 跳板或开放转发器。
代理没有停留在网站入口。微服务把一次业务拆成许多内部调用之后,服务之间也出现了同样的问题:发现目标、负载均衡、加密、重试、限流和观测。如果每种语言的每个服务都自己实现,策略很快会不一致。
服务网格的思路,是把这些通用网络能力放进服务代理。代理可以在每个工作负载旁运行,也可以由节点级组件承担;应用仍然访问一个本地或虚拟目标,代理负责发现真正端点并执行策略。集中控制面向代理下发路由、证书和策略,实际承载请求的代理组成数据面。

边缘代理位于系统与外部网络之间,重点是公网 TLS、外部身份、攻击面控制、全局路由和静态内容分发。它面对的客户端类型复杂,请求默认不可信。
服务代理位于内部服务之间,重点是服务发现、实例级负载均衡、mTLS、重试、熔断和调用观测。它不应该因为流量来自“内网”就自动信任,而应使用工作负载身份和明确策略判断谁能调用谁。
服务网格让不同语言共享同一套流量治理能力,应用升级网络策略时也不必逐个改库。但请求路径会增加代理进程,资源消耗、控制面可靠性、调试难度和版本兼容都随之增加。应用日志显示“请求超时”时,问题可能在本机代理、远端代理、策略配置或证书轮换,而不在应用代码。
小规模系统如果只有几个服务,简单的反向代理和应用内客户端库往往更容易维护。只有当服务数量、语言种类和治理需求真的增长时,网格带来的统一能力才可能覆盖它的复杂度成本。
现在回到开头:客户端为什么不知道后端有几台机器?因为客户端只需要信任并连接公开入口,入口后面的代理链负责选择、转发和隔离变化。
这个答案听起来简单,真正落地时应把下面的问题写清楚:
代理真正解决的不是“把 A 转发到 B”这么一个动作,而是把客户端与后端的变化隔开:客户端不必追着实例变化,后端也不必直接面对所有外部连接。
代价同样明确。你每增加一层代理,就增加一个拥有流量控制权的组件,也增加一处信任、延迟和故障判断。好的代理架构不是层数最多、功能最全,而是每一层的职责都说得清,绕过路径被关住,失败时的后果也在预期之内。
接下来再看 HTTP 缓存时,你会发现它并不是独立技巧,而是代理在“我能否用过去的响应回答现在的请求”这个问题上做出的另一层判断。