反向代理:不只是 proxy_pass
导言:被误解的“中间人”
我最开始接触反向代理的时候,对它的理解其实非常简单:Nginx 里面写一个 proxy_pass,把外面的请求转到后端服务,这就叫反向代理。
比如 Django 跑在本机的 8000 端口,我又不想让别人每次访问网站都在域名后面跟一个 :8000,于是让 Nginx 在前面监听 80 和 443,再把请求转发到 127.0.0.1:8000。
location / {
proxy_pass http://127.0.0.1:8000;
}
这么理解当然不能说错,问题是它只解释了反向代理最表面的那一层功能。后来自己部署的东西越来越多,再去看一些稍微复杂一点的服务器架构,我才发现反向代理真正好用的地方并不是“帮忙转发一下请求”,而是它可以在用户和真正提供服务的服务器之间,再加一层。
用户只需要知道自己访问的是 example.com。至于这个域名后面究竟是一台服务器还是十台服务器,后端跑的是 Django、Node.js 还是 Go,甚至服务器今天部署在这里、明天迁移到了另一台机器,其实都没必要让用户知道。
换句话说,反向代理真正藏起来的不是一个端口,而是后面整个服务到底是怎么实现的。
这也是我现在觉得反向代理最有意思的地方。
反向代理的原理
在继续往下讲之前,得先搞清楚反向代理到底是怎么工作的。它并不是把一个本来要发给后端服务器的请求“半路截走”,而是从一开始,客户端连接的对象就是反向代理。
比如我们在浏览器里输入 example.com,浏览器会先通过 DNS 查询这个域名对应的 IP 地址。如果这个网站使用了反向代理,那么 DNS 最终指向的公网入口通常就是反向代理,而不是真正运行 Django、Java 或其他业务程序的那台服务器。
所以第一次连接实际上是:
浏览器
│
│ 请求 example.com
▼
DNS 解析
│
│ 得到反向代理的 IP
▼
反向代理服务器
浏览器随后与这个 IP 建立 TCP 连接,如果使用 HTTPS,还会在这条连接上进行 TLS 握手。等 HTTP 请求真正发出去的时候,它首先到达的其实就是反向代理。
反向代理收到请求之后,会读取请求里的信息,比如访问的域名、URL 路径、请求头等,再按照提前配置好的规则决定这个请求应该交给谁处理。
假设服务器上同时运行了两个服务:
127.0.0.1:3000 前端
127.0.0.1:8000 Django
我们可以规定访问 /api/ 的请求交给 Django,其余请求交给前端。于是用户访问:
https://example.com/api/user
实际发生的事情更接近:
浏览器
│
│ HTTPS 请求
▼
反向代理 :443
│
│ 发现路径以 /api/ 开头
▼
Django :8000
│
│ 返回响应
▼
反向代理
│
▼
浏览器
这里有一个很容易忽略的地方:对客户端来说,它从头到尾都只是在和反向代理通信。
反向代理和后端之间实际上又是另外一次通信。也就是说,它并不是简单地把客户端原来的网络连接“接根管子延长到后端”,而是自己先作为服务器接收客户端请求,再作为客户端去请求真正的后端服务,拿到结果以后再把响应返回给用户。
这也是“代理”两个字真正体现出来的地方。
而所谓“反向”,主要是相对于我们更熟悉的正向代理来说的。使用正向代理时,是客户端知道自己正在通过代理访问外面的服务器:
我 → 代理 → Google
代理主要隐藏的是客户端。
反向代理则刚好站在服务器这一边:
用户 → 反向代理 → 真正的服务器
用户通常不需要知道后面到底是哪台服务器,所以它主要隐藏的是服务端的内部结构。
把这个原理想明白之后,后面的很多功能其实就顺理成章了。既然所有请求本来就先经过反向代理,那么它当然可以决定这个请求送给 A 还是 B,这就是负载均衡;它可以先和用户完成 TLS 握手,再把请求交给内部的 HTTP 服务,这就是 HTTPS/TLS 终止;它也可以看到请求的路径,再决定 /user 去用户服务、/order 去订单服务,这就是基于规则的请求路由。
所以反向代理那些看起来五花八门的能力,本质上都建立在同一个非常简单的前提上:
它是用户进入后端系统之前,真正连接到的第一站。
第一章:巨人的护城河 —— 企业级架构里的反向代理
先把场景放大一点。
一个刚上线的小网站可能确实没什么好代理的,一台服务器直接把前端、后端全跑起来,用户访问这台服务器就完事了。但假如网站突然有了很多用户,一台服务器扛不住了,最直接的办法当然就是加服务器。
问题也跟着来了。现在有 A、B、C 三台服务器,总不能让用户自己决定今天访问哪台,更不可能 A 快满了以后发个公告说“大家先别访问 A 了,麻烦改一下地址去 B”。
所以需要有一个东西站在这些服务器前面。
┌── Server A
│
用户 → Proxy ─┼── Server B
│
└── Server C
用户还是访问原来的地址,请求到了代理这里之后,再由它决定应该送到哪台服务器。这个时候反向代理干的事情就不再只是简单的“转发”了,它开始承担负载均衡的工作。
最简单的分配方式就是轮流来,一个请求给 A,下一个给 B,再下一个给 C。实际生产环境当然可以玩得复杂得多,比如哪台服务器性能更好就多分一点,哪台现在连接比较少就优先分给它,哪台已经挂了就暂时别往那里送。
再继续往后发展,事情会变得更有意思。
一个项目刚开始的时候,所有功能可能都塞在一个后端里。项目越来越大以后,用户、订单、支付、文件这些东西可能逐渐被拆成不同的服务。内部看起来已经变成了:
用户服务
订单服务
支付服务
文件服务
静态资源服务
……
但用户显然没必要知道这些。
他访问 /api/user 的时候,请求可以被送去用户服务;访问 /api/order 的时候送去订单服务;请求静态资源的时候又走另一台服务器。对用户来说,他从头到尾访问的可能都只是同一个 example.com。
这时候再看反向代理,就已经很像整个系统门口的一个交通枢纽了。
HTTPS 证书可以放在这里统一处理,静态资源可以在这里缓存,后面的服务器可以做负载均衡,某台机器出问题了可以暂时摘掉,新版本上线以后甚至可以先只放一部分流量过去试试,没有问题再慢慢扩大。
所以很多企业架构里都会有这样一层东西。具体实现不一定非得是我们最熟悉的 Nginx,但背后的思想其实差不多:外面只认一个稳定入口,至于里面到底怎么折腾,是系统内部自己的事情。
从这个角度来看,反向代理其实是在帮后端争取“随便改”的空间。服务器可以增加,可以减少,可以迁移,服务也可以拆分,只要外面的入口没变,客户端就不需要跟着后端一起折腾。
第二章:单机生产力提速 —— 为什么单台服务器也离不开它?
说到这里很容易产生一种错觉,好像反向代理是大公司服务器成百上千以后才需要考虑的东西。实际上对于个人开发者来说,它可能更加常见,只是规模小很多。
我就一台 VPS,没有什么集群,也没有每天几百万的请求,但这一台服务器上也不一定只跑一个东西。
今天跑一个 Django,占着 8000;明天又部署一个前端,占着 3000;后来又弄了一个 FastAPI,占着 8080;再过几天装个监控面板,又多出来一个 3001。
如果没有一个统一入口,理论上当然也能用:
example.com:3000
example.com:8000
example.com:8080
example.com:3001
就是怎么看都有点野生。
更麻烦的是,这些服务如果全部直接暴露在公网,每一个都得自己考虑公网访问和安全问题。HTTPS 也很烦,总不能每写一个小项目就重新让它自己处理一遍证书。
这时候 Nginx 或 Caddy 放在最前面就很舒服了。公网只需要和它打交道,它再根据域名或者路径把请求分给后面的程序。
比如:
www.example.com → 127.0.0.1:3000
api.example.com → 127.0.0.1:8000
admin.example.com → 127.0.0.1:8080
monitor.example.com → 127.0.0.1:3001
外面看起来是四个正常的网站或者服务,服务器里面其实还是那几个朴素的端口。
HTTPS 也可以直接在这一层解决。用户和 Nginx 之间走 HTTPS,Nginx 再去访问内部的 Django。这样 Django 根本不需要直接面对公网,老老实实监听 127.0.0.1:8000 就行。
我觉得这个场景反而比“大厂负载均衡”更容易理解反向代理为什么好用。因为你真的会看到自己那台服务器从“一个程序一个端口”慢慢变成一团乱麻,然后突然发现,只要在它们前面加一个统一入口,事情一下就清楚了。
而且内部端口不直接暴露公网之后,攻击面也跟着小了一些。真正需要对公网开放的可能就是 80 和 443,后面的 3000、8000、8080 全部留在服务器内部。
所以个人服务器用反向代理和企业用反向代理,本质上没有想象中那么大的区别。企业是在管理几十上百个服务,我是在管理自己 VPS 上那几个乱七八糟的小项目而已。
多少有点“同款架构,青春版”的意思。
第三章:折腾党白嫖指南 —— 零成本反代和内网穿透还能怎么玩?
如果只是为了把自己 VPS 上的几个服务整理好,到这里其实已经够用了。但我后来继续搜反向代理相关的东西,发现这玩意到了个人开发者手里,画风会迅速从“企业架构”变成“今天还能薅点什么”。
其中最绕不开的应该就是 Cloudflare。
Cloudflare 本身就在大量网站前面充当反向代理,用户访问一个接入 Cloudflare 的网站时,请求会先经过 Cloudflare 的网络,再到真正的源站。 但对个人开发者来说,它好玩的地方还不止 CDN 和隐藏源站 IP,Tunnel、Workers、Pages 这些东西组合起来以后,能折腾的事情非常多,而且不少功能确实存在免费额度。比如目前 Workers Free 每天包含 10 万次请求,不过免费计划同时存在 CPU 时间、子请求数量等限制,所以“白嫖”归白嫖,不能直接理解成无限用。
先说一个最实用的 Cloudflare Tunnel。
假如我在自己电脑上跑了一个博客,地址是 localhost:3000,正常情况下外面的人肯定访问不到。如果家里的网络又没有公网 IP,传统做法可能是先弄一台有公网 IP 的 VPS,再通过 FRP 之类的东西把流量转回来。
Tunnel 换了一个思路。不是让外面的人想办法主动找到我的电脑,而是让我电脑上的 cloudflared 主动连接 Cloudflare。Cloudflare 官方现在的说明也是这样:连接是从本地向外建立的,因此不要求源站拥有公网 IP,也不用为了 Tunnel 专门开放入站端口。然后可以把一个公开域名,比如 app.example.com,映射到本地的 localhost:8080。
于是整个访问过程大概就变成:
别人访问 app.example.com
↓
Cloudflare
↓
Tunnel
↓
我的 localhost:3000
这就很好玩了。原本只能自己打开的本地项目,可以直接丢一个地址给朋友看;家里的 NAS、测试环境或者一些自己部署的小服务,也可以用类似的方式访问。
甚至如果只是临时给别人看一下 Demo,Cloudflare 现在还有 Quick Tunnel,不需要 Cloudflare 账号就能创建临时 Tunnel,官方给它定位的场景本身就包括 Demo、Webhook 测试和分享本地开发服务器。
网上已经有人把这套玩法写得很细了,我觉得没必要在这里再复制一遍安装命令。
想实际折腾的话可以直接去看:
再往下还能折腾 Workers。
Workers 本身是 Cloudflare 的 Serverless 运行环境,简单理解就是我可以让一小段程序直接跑在 Cloudflare 的网络上,而不用专门为了这点代码养一台服务器。既然 Worker 能收到 HTTP 请求,又能通过 fetch() 请求其他地址,那自然就有人拿它做各种轻量的请求转发和反向代理。
这就开始有“赛博薅羊毛”的感觉了。
原本按照传统方式,想做一个简单的 API 中转或者资源反代,可能第一反应是买服务器、装 Nginx、配 HTTPS。现在一些很轻量的需求,直接用边缘函数就能处理。当然 Workers 免费计划并不是无限资源,目前每天有 10 万请求额度,每次调用的 CPU 时间等也有限制。
所以它特别适合个人项目和一些轻量服务,但真拿去扛一个高负载生产系统之前还是得先看清楚限制。
Tunnel 和 Workers 再加上 Pages,甚至还能继续套娃。Pages 放前端,Workers 处理一点动态逻辑或者转发请求,Tunnel 再把家里或者本地真正运行的某些服务接进来。以前脑子里的“网站上线”基本等于“先搞台服务器”,现在会发现有些项目从头到尾甚至都不一定需要自己维护一台传统 Web Server。
另外还有一点很容易混淆:反向代理和内网穿透不是一回事。
反向代理解决的是“请求已经来到入口了,接下来应该送给谁”;内网穿透解决的则是“外面的人怎么访问一个原本根本没有公网入口的服务”。Cloudflare Tunnel 属于后一类问题的解决方案,只不过请求进入 Cloudflare 以后,本身又会经过代理和路由,所以这几个概念在实际使用里经常缠在一起。
如果既不想依赖 Cloudflare,又比较喜欢自己掌控整个过程,那传统方案照样能玩。一台便宜 VPS 放在公网,Nginx 或 Caddy 负责入口,再配 FRP、rathole 一类隧道工具把流量送回家里的设备,也是很经典的一套组合。
如果我的需求压根不是“让全世界都能访问这个网站”,只是希望自己的电脑、手机、NAS 和服务器之间能互相访问,那又是另一回事了。这种情况下 Tailscale 一类虚拟组网工具可能更加合适,因为我要解决的已经不是公网发布,而是怎么让这些散落在不同网络里的设备像在同一个私人网络里一样互相通信。
所以折腾到这里之后,我反而觉得这些“白嫖教程”最大的价值不只是省了几十块服务器钱。
因为你一开始可能只是想把 localhost:3000 发给朋友看看,最后莫名其妙就会一路碰到 DNS、NAT、公网 IP、HTTPS、CDN、隧道、反向代理、端口和防火墙。
课本里分开出现的一堆名词,就这么被一个“我想让别人看看我网站”的朴素需求串起来了。
某种意义上,这可能才是折腾最大的羊毛。
第四章:总结 —— 反向代理的艺术:架构解耦
折腾一圈再回来看最开始那句:
proxy_pass http://127.0.0.1:8000;
感觉就完全不一样了。
从代码上来说,它确实只是在告诉 Nginx:“这个请求帮我送到 8000 端口。”
但从架构上来说,它其实是在告诉外面的访问者:“你不用知道 8000 端口的存在。”
今天 example.com 后面可能只有一台服务器,明天换了一台,后天变成三台做负载均衡,再过一段时间整个项目被拆成一堆服务。里面已经改得面目全非,用户却可能完全没有感觉。
因为他从头到尾只认识一个东西:
example.com。
所以我现在再理解反向代理,已经不会把重点放在“反向”这两个字到底反在哪里,也不会只把它理解成 Nginx 的一个功能。
我更愿意把它理解成系统里的一层边界。
边界外面的人只关心“我要什么”,边界里面再决定“到底让谁给你”。
反向代理当然没有消灭复杂性。服务器还是那些服务器,端口还是那些端口,该配的东西一个也跑不了。它只是把这些复杂性留在了系统内部,不再要求每一个访问者都理解它们。
说到底,好的架构很多时候也不是想办法把复杂性变没,而是想办法别让复杂性到处乱跑。
反向代理做的事情其实就这么简单:门外的人只需要知道门在哪,至于门后面今天坐了一个人还是一百个人,那是门里面自己的事。









