位于几乎每个 Web 服务前端的是什么
欢迎
当你在浏览器中输入 example.com 时,你几乎永远不会直接连接到实际运行应用程序的机器。你连接到的是一台转发请求到实际运行机器的机器。这台转发机器有一个名称:反向代理。
本课程将讲解反向代理的作用、为什么几乎每个公共 Web 服务都隐藏在反向代理之后,以及边缘层同时处理的三项职责。
到课程结束时,你将理解:
- 客户端、代理与源站之间的区别
- 为什么在源站前部署代理能够保护源站、实现扩展,并允许在无人察觉的情况下替换组件
- 反向代理同时承担的三项职责:隐藏源站、终止 TLS 以及跨副本分发负载
- 请求如何从浏览器到代理再到上游,逐跳传输
课程结束后,你将能够自信地推理代理的部署位置、边缘处的关注点分离,以及为什么无状态代理层可以低成本地倍增,而单一源站则不能。
正向代理与反向代理
两种代理方向
这两种代理都位于两方之间并中继流量。它们的区别在于代表哪一方。
正向代理:位于一组客户端的前端。客户端知道代理的存在;外部服务器看到的是代理的地址,而非客户端的地址。企业出口代理、内容过滤器及 SOCKS 代理均符合此模式。
反向代理:位于一组服务器的前端。外部世界(客户端)与代理地址进行通信;客户端完全不知道其背后隐藏着真实的服务器。几乎每个公共 Web 服务都使用反向代理。
助记法:正向代理向服务器隐藏客户端。反向代理向客户端隐藏服务器。
为什么方向很重要? 职责、故障模式及安全边界均有所不同。正向代理关注其用户正在联系谁;反向代理关注谁正在联系其服务器。
一个同时通过这两种类型代理发送流量的客户端,其路径为:client -> forward_proxy -> internet -> reverse_proxy -> origin。
为何如此多的功能位于边缘
边缘层物尽其用
反向代理同时承担三项职责。其中任何一项都足以证明该层的必要性;而在同一地址同时承担这三项职责,解释了为何几乎所有生产级 Web 架构的前端结构都呈现相同的形态。
职责 1:隐藏源站。 代理在公共 IP 上响应。后端位于互联网无法访问的私有 IP 上。想要攻击源站的攻击者必须首先攻破代理。
职责 2:终止 TLS。 代理持有 example.com 的证书,并解密传入的 HTTPS 请求。后端通过受信任的网络段以纯 HTTP(或更简单的内部 TLS)与代理通信。证书的轮换、续期以及密码策略都集中在一个地方管理。
职责 3:分发负载。 代理决定由哪个后端处理每个请求。位于同一个代理后的后端形成一个池;代理使用某种策略(轮询、最少连接数、基于请求头的哈希)为每个请求选择一个后端。增加容量意味着向池中添加一个后端,而不是告知每个客户端一个新的地址。
每个职责都是一个小程序。它们共同解释了为什么像“在 Python 应用前放置 Caddy”这样简单的层级,比 Python 应用本身承载了更多的设计权重。
为所有三项职责进行设计
你的团队在单台 VM 上运行一个小型 API,该 API 由一个监听 8000 端口的 Python 进程处理。流量增长到单台 VM 无法应对的程度,且安全审查指出该 VM 拥有公网 IP 并自行运行 TLS 证书。
你决定在前方放置一个反向代理。请草绘布局:DNS 指向哪里,证书存放在哪里,负载如何到达后端,以及原始 VM 会发生什么变化?
在无人察觉的情况下替换组件
间接层带来自由
计算机科学中有一句古老的格言:任何问题都可以通过增加一层间接性来解决(除了间接层太多的问题)。反向代理是分布式系统中最有用的间接层之一。
它为你带来的好处:
- 可替换的后端。 将应用从 Python 迁移到 Go?从一个数据中心迁移到另一个?零停机时间推出新版本?这一切都发生在一个稳定的公共地址之后。对用户而言,没有任何变化。
- 独立扩展。 代理层在 TLS 层根据带宽和 CPU 进行扩展。后端层根据应用负载进行扩展。由于它们运行在不同的机器上,因此可以各自独立地沿不同维度扩展。
- 故障隔离。 后端的错误部署不会导致公共地址失效。代理保持运行;你可以推送修复或回滚;当后端恢复时,世界重新连接。
- 横切关注点集中在一处。 速率限制、地理封锁、请求日志、头部重写、缓存、响应压缩:全部位于代理上。后端代码可以专注于应用程序本身。
没有代理时,上述每一个问题都必须存在于应用进程内部。有了代理,它们存在于由一个团队负责的一层中。
代价:多了一个需要运维的层。成熟的团队接受这一代价,因为代理层本身是无状态的,并且可以水平扩展;将一个代理替换为两个不需要任何协调。
通过反向代理进行蓝绿部署
你的团队在反向代理后方的三台后端虚拟机(蓝色池)上运行 API 版本 1。你希望部署版本 2,并且如果出现问题,能够在三十秒内回滚。
你启动了三台运行版本 2 的新后端虚拟机(绿色池),与蓝色池并行运行,但尚未将任何流量路由到它们。
从浏览器到后端再返回
端到端追踪一次请求
追踪一次针对 https://api.example.com/users/42 的 HTTPS GET 请求,该请求经过位于后端池前方的反向代理。
第 1 跳:DNS 解析。 浏览器向解析器查询 api.example.com。解析器返回代理的公网 IP(例如 203.0.113.10)。浏览器向 203.0.113.10:443 发起 TCP 连接。
第 2 跳:TLS 握手。 代理出示 api.example.com 的证书。浏览器验证证书,双方协商出会话密钥,加密通道随即建立。
第 3 跳:TLS 内的 HTTP 请求。 浏览器发送 GET /users/42 HTTP/1.1\nHost: api.example.com\n...。代理解密请求字节。
第 4 跳:后端选择。 代理检查 api.example.com 的上游池,并根据其负载均衡策略选择一个后端(例如 10.0.0.21:8000)。
第 5 跳:上游请求。 代理向 10.0.0.21:8000 打开(或复用)一个纯 HTTP 连接并转发请求。代理在此过程中可能会重写头部:添加 X-Forwarded-For: <client-ip>,正确设置 Host:,并移除诸如 Connection 等逐跳(hop-by-hop)头部。
第 6 跳:后端处理。 后端应用程序读取请求,查询其数据库,并构建 JSON 响应。
第 7 跳:上游响应。 后端将响应以纯 HTTP 形式发送回代理。
第 8 跳:边缘响应。 代理可能会重写或压缩响应,通过 TLS 会话将其重新加密,并发送给浏览器。
第 9 跳:连接生命周期。 TLS 会话通常保持打开状态以处理下一个请求(HTTP/2 在单个连接上复用多个请求)。代理到后端的连接通常会被池化以便复用。
每个公共 Web 服务都遵循这种形态的某种变体。了解这些跳数(hops)能让你推断出延迟在哪里累积、日志应该记录在哪里,以及故障可能隐藏在哪里。
时间都去哪儿了?
用户抱怨 API 响应缓慢。你进行测量后发现,请求的端到端耗时为 850 毫秒。后端服务器日志显示,应用程序处理该请求仅用了 40 毫秒。代理日志显示,代理在其一侧的工作(TLS 握手 + 路由 + 响应写入)耗时 50 毫秒。
为新服务设计一个极简边缘层
综合应用
你已经了解了正向代理与反向代理的区别、反向代理同时承担的三项职责、隐藏源站为何能在每次变更时带来回报,以及请求如何逐跳流经边缘层。
现在,请将其付诸实践。
一个小团队计划推出名为 notes.example.com 的新服务。用户将在此读写个人笔记。团队在上线时将运行两台后端虚拟机,并预计在未来一年内扩展至十台。他们希望为用户提供 HTTPS 支持、对新版本进行灰度发布,并且不向公众暴露后端 IP 地址。
本课程接下来的方向
本课程接下来的方向
本课时确立了边缘层的形态。本课程中还有四个课时将在此基础上展开:
- 无状态水平扩展:为什么代理层(及其背后的后端)可以低成本地倍增,以及在流量激增情况下计算副本数量的数学方法。
- 入站/出站分离:为什么同时处理入站和出站流量的单一代理服务器最终会以令人惊讶的方式失效,以及如何拆分这些层。
- 故障模式与爆炸半径:单个配置变更如何级联导致服务中断,以及如何撰写无责行动项以防止问题再次发生。
- 可观测性与容量:在边缘节点测量哪些指标,以便在用户察觉之前发现系统故障。
每节课都是独立的。综合起来,它们为你构建了一个关于大规模 Web 集群的实用心智模型。
配套课程:geometry_of_proxies_and_origins 将本课内容重新表述为有向图,并探讨图论对请求路径的启示。
干得漂亮。继续前进。