做运维这几年,被问得最多的一个问题就是:CDN、负载均衡、反向代理,这三个到底有什么区别。每次都有朋友拿着 Nginx 配置来问,说"我明明加了反向代理,为什么还是很卡""我套了 CDN,为什么源站 IP 还是暴露的"。说实话,这三个词经常被放在一起,是因为它们都站在用户和后端服务器之间,都涉及"转发""加速""分发"这些动作,但各自的职责完全不同。
今天我就不按教科书那套讲了,准备用一台服务器、一个域名,把三者的原理和实战串起来讲清楚。文章里会有可以直接抄走的 Nginx 配置、验证 CDN 是否生效的命令、以及我在排障时踩过的坑。适合刚接触 Web 架构、在 1Panel 或纯 Nginx 里配过反代但没搞懂背后逻辑的同学,也适合想搞清楚线上链路每一层在干什么的人。
1. 三个高频词,到底分别解决什么问题
1.1 反向代理:站在用户和服务器之间的"前台接待"
反向代理(Reverse Proxy)可以理解成一个公司的前台接待。用户不直接找后端员工(应用服务器),而是把所有请求统一递到前台,由前台根据请求内容转交给对应的人。这个"前台"不生产内容,但它决定了谁能见到后端、以什么方式见。
为什么需要这个前台?真实场景里理由很多:
- 隐藏后端细节:后端可能是内网地址,比如 192.168.x.x,不能直接暴露公网。反代作为唯一入口,替后端挡住所有直连尝试。
- 统一入口与域名管理:多个应用跑在不同端口(8080、3000、11434),用户不可能每个端口都记。反代可以把子域名或路径映射到不同端口,外面看起来就是一个站点。
- 附加能力集中下沉:HTTPS 证书终止、访问控制、限流、日志、请求头改写,这些都可以在反代层统一做,不用塞进每个应用代码里。
反向代理里最重要的配置是 proxy_pass,也是最容易写错的一行。它的核心规则是:location 匹配到的 URI 是否被保留,取决于 proxy_pass 后面是否带了路径。
举个例子。用户请求 https://example.com/api/v1/users:
# 带路径写法:location 中的 /api 会被替换成 /v1 location /api { proxy_pass http://backend:8080/v1; } # 实际转发到 http://backend:8080/v1/users # 不带路径写法:location 匹配到的完整 URI 原样转发 location /api { proxy_pass http://backend:8080; } # 实际转发到 http://backend:8080/api/v1/users这个细节我踩过太多次。之前帮同事排查接口 404,查半天发现就是 proxy_pass 末尾多了一个斜杠,导致路径前缀被吞掉,所有接口都从 /api 变成了 /,路由全乱。后来我养成了个习惯:每次改完 proxy_pass,先自己默念一遍"带不带斜杠、带不带 URI",再去 reload。
1.2 负载均衡:把流量分给多个后端
那如果后端不是一台,而是三台机器跑同一个服务呢?这时候"前台接待"一个人忙不过来,就需要一组"分诊台"——这就是负载均衡(Load Balancing)。
负载均衡的核心逻辑很简单:同一个入口地址,背后的请求被按照某种策略分发到多台后端服务器上。每台后端分摊一部分流量,单台挂了也有其他机器顶上,这就是"高可用"最朴素的理解。
注意,负载均衡不一定是一个独立软件。Nginx 的反向代理模块本身就自带负载均衡能力,你只要在 upstream 里写多个后端,轮流转发,这就已经是负载均衡了。所以很多人以为"负载均衡 = 云厂商的 SLB/ELB",其实 Nginx 配置里早就内嵌了这个功能,而且对于中小流量场景完全够用,不额外花钱。
简单总结分工:如果请求固定只去一台服务器,那是反代;如果请求按规则在多台服务器之间分发,那就是在反代基础上叠加了负载均衡。
1.3 CDN:把内容搬到离用户更近的地方
第三个是 CDN(Content Delivery Network,内容分发网络)。它解决的是另一个维度的痛:用户离服务器太远。
打个比方,你的源站在广州,一个黑龙江的用户直接访问源站,一个请求要跨大半个中国。就算链路很顺,物理延迟也摆在那里。CDN 的思路是:把静态资源(图片、CSS、JS、视频)提前"复制"到各地机房,让用户从最近的节点拿数据,而不是每次千里迢迢回源。
关键动作是两个:"复制"和"就近"。CDN 节点本质上是分布在全国甚至全球的缓存服务器,用户请求会被 DNS 调度到离自己最近的节点,节点上有缓存就直接返回,没有缓存才回源站取一次,然后缓存下来供后续请求用。
所以 CDN 和负载均衡不是一回事:负载均衡解决的是"谁来处理",CDN 解决的是"从哪儿拿"。生产环境里两者经常一起出现——静态资源走 CDN,动态接口走负载均衡加反代,各管各的。
| 角色 | 核心解决什么问题 | 典型工具 | 生活类比 |
|---|---|---|---|
| 反向代理 | 统一入口、隐藏后端、附加能力 | Nginx、Caddy、HAProxy | 公司前台 |
| 负载均衡 | 多后端分摊流量、故障转移 | Nginx upstream、云 SLB、LVS | 医院分诊台 |
| CDN | 内容就近分发、缓存加速 | Cloudflare、阿里云 CDN、腾讯云 CDN | 小区门口的便利店 |
2. Nginx 反向代理实战:从单站点到多站点
2.1 最简配置:看懂一个 server 块
先来一个能直接跑起来的最简反代。假设我有一台服务器,IP 是 1.2.3.4,上面跑了两个服务:一个 Node.js 应用监听 3000 端口,一个静态博客放在 /var/www/blog。我只想给用户暴露一个入口 https://example.com,并按路径区分:/ 是博客,/api 是 Node 服务。
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/certs/example.com.crt; ssl_certificate_key /etc/nginx/certs/example.com.key; # 静态博客:Nginx 直接处理 location / { root /var/www/blog; index index.html; } # 动态接口:转发给 Node location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }三行请求头设置,每一行都有说法:
- Host $host:把用户请求的域名原样传给后端。很多框架会基于 Host 做路由或生成链接,不传的话,后端拿到的是 127.0.0.1,可能触发重定向到错误地址。
- X-Real-IP $remote_addr:记录直连 Nginx 的真实客户端 IP,后端日志里能直接看到用户 IP。
- X-Forwarded-For:标准链路透传字段。$proxy_add_x_forwarded_for 会自动把现有 XFF 值和当前 remote_addr 拼接,多层代理时也不会丢前面的 IP。
注意:反代之后,后端看到的所有请求都来自 Nginx 的 IP。如果不透传这些头,应用的访问日志、风控、限流全会失效。这是我过去几年见过最多的问题,没有之一。
如果你在 Windows 上装 Nginx,配置思路完全一样,只是启动方式不同。去官网下载 Windows 二进制包,解压后修改 conf/nginx.conf,然后用nginx.exe -t检查语法,nginx.exe -s reload重载。生产环境不建议 Windows 长期跑 Nginx,问题排查和性能调优都不如 Linux 顺手,但本地开发练手完全没问题。
2.2 多站点场景:1Panel 与纯 Nginx 的两种做法
现在很多人用 1Panel 管理服务器。它自带一个"网站"功能,可以可视化创建站点、申请 SSL 证书、配置反向代理,底层依然是帮你生成 Nginx 配置文件。它的逻辑很简单:一个站点对应一个 server 块。
在 1Panel 里给"多个网站"做反代,最常见套路就是按域名拆站点:
- 在"网站"里分别创建 example.com 和 blog.example.com 两个站点。
- 每个站点单独指定"反向代理"类型,填入目标地址,比如 http://127.0.0.1:3000。
- 面板会自动帮你生成 server 块,证书也在同一个页面申请和续期。
按域名拆是最直观的方式,多个站点互不影响,出错也好定位。如果是纯手写 Nginx,我强烈建议用 sites-available / sites-enabled 目录,按站点拆分文件,而不是把所有域名堆在一个 nginx.conf 里:
/etc/nginx/ ├── nginx.conf # 主配置,只做全局设置 ├── sites-available/ │ ├── example.com │ └── blog.example.com └── sites-enabled/ # 里面放指向 sites-available 的软链接 ├── example.com -> ../sites-available/example.com └── blog.example.com -> ../sites-available/blog.example.com新增一个站点就是"写配置 + 建软链 + reload",删除站点也不用小心翼翼去大文件里删块。我维护过十几个站点的机器,单文件方案到后面会让你崩溃,这个目录拆分法能救命。
2.3 给本机 AI 服务做反代:Ollama 实测案例
Ollama 是现在很流行的本地大模型运行工具,默认监听 11434 端口。如果你有台带 GPU 的服务器,想给团队统一提供一个入口,反代就很合适。配置如下:
server { listen 443 ssl; server_name ollama.example.com; ssl_certificate /etc/nginx/certs/example.com.crt; ssl_certificate_key /etc/nginx/certs/example.com.key; location / { proxy_pass http://127.0.0.1:11434; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # Ollama API 是流式输出,必须关掉缓冲 proxy_buffering off; proxy_read_timeout 300s; } }两个关键点需要特别说一下:
一是 proxy_buffering off。大模型接口走流式生成(SSE),如果 Nginx 默认开启缓冲,它会攒一批数据再一次性返回,前端看到的就不是逐字输出,而是卡一下、刷一片,体验非常糟糕。开了 off 之后,数据能第一时间流向客户端。
二是超时时间。大模型生成可能要几十秒甚至几分钟,Nginx 默认的 proxy_read_timeout 是 60 秒,很容易触发 504。我一般调到 300 秒以上,具体看模型大小和显存情况。
如果是给公网访问,记得在 Nginx 层加鉴权,最简单的做法是 HTTP Basic Auth(htpasswd)。别把本地 Ollama 裸暴露到公网,别人可以白嫖你的 GPU 算力,这个坑我见过不止一次。
3. 负载均衡实战:从单点故障到高可用
3.1 常用负载策略:轮询、最少连接、IP 哈希
Nginx upstream 支持多种策略,日常够用的核心就三个:
- 轮询(默认,round-robin):请求按顺序轮流分给每台后端。适合各后端性能相近、请求处理时间差不多的场景,配置最简单。
- least_conn:谁当前连接数少就分给谁。适合请求耗时差异大的场景,比如有的接口 10ms,有的要 2 秒,轮询会导致慢请求堆积在某台机器上。
- ip_hash:按客户端 IP 做哈希,同一个 IP 固定到同一台后端。适合需要会话保持、后端又无法共享会话的场景。
还有 weight 权重参数,可以在任意策略基础上给不同机器不同比重。比如新机器性能好,权重给 3,老机器给 1,流量就大致按 3:1 分配。
upstream backend_app { least_conn; server 10.0.0.11:8080 weight=3; server 10.0.0.12:8080 weight=1; server 10.0.0.13:8080 backup; }上面这段里 backup 表示备用节点:只有前两台都不可用时,流量才会切到 backup。这在同城容灾、灰度发布场景里很实用——平时它不接流量,关键时刻顶上去。
补充一点:least_conn 判断的是连接数,不是 CPU 负载或响应时间,所以它只是一个"更好一点的默认选择",不是银弹。如果各后端处理能力差异很大,更稳的方案是用云厂商的负载均衡配合健康检查和权重动态调整,毕竟 Nginx 原生策略在复杂场景下确实有些单调。
所谓"低开销负载均衡",对于中小站点来说,Nginx upstream 就是性价比最高的方案:零额外组件、零额外费用,几十行配置就能拿到基本的高可用能力。我见过很多一天几千请求的小站,架一台 SLB 花了钱却发挥不出价值,真不如先在 Nginx 层用起来。
3.2 健康检查与故障转移是怎么工作的
负载均衡能实现高可用,靠的是自动剔除故障后端。Nginx 原生的检查是"被动式"的:它不主动去探测,而是根据转发请求后的表现来判断。
关键参数就两个:
upstream backend_app { server 10.0.0.11:8080 max_fails=2 fail_timeout=10s; server 10.0.0.12:8080 max_fails=2 fail_timeout=10s; }含义是:在 10 秒内,如果这台后端有 2 次转发失败(连接超时、返回 502/504 等),Nginx 就认为它挂了,接下来 10 秒内不再向它转发;10 秒后重新试探,请求成功则慢慢恢复。
被动检查的优点是零额外请求成本,缺点是发现问题时,已经有部分用户踩到了失败请求。主动健康检查需要 Nginx Plus 或额外编译模块(比如 nginx_upstream_check_module),云厂商的负载均衡一般是主动探测,这也是很多人最终换云产品的原因。
如果是自己维护,我建议被动检查参数配合应用日志告警一起用:负载均衡负责在阈值内快速摘除故障节点,告警负责通知你去修机器。两者结合,比单纯依赖 Nginx 的检查机制靠谱得多。
3.3 会话保持与真实 IP 透传
负载均衡之后,最大的两个工程问题就是会话和 IP。
会话问题。如果后端是 PHP 原生 session 或某些老框架,用户第一次请求落在 A 机,第二次落在 B 机,B 机没有这份 session,用户就被踢下线了。解决思路有三个方向:
- 让同一用户固定落在同一台机器(ip_hash 或 sticky cookie),这是最简单的"治标";
- 把 session 外置到 Redis,所有后端共享,这是更彻底的"治本";
- 改造为无状态服务(JWT 之类),从根上消灭服务端会话。
我的建议是:新项目直接走第三条路,老项目如果改不动,优先把 session 挪到 Redis,ip_hash 只作为临时止血手段。因为它会导致某台机器热度不均,一台挂了会集中甩掉一批用户。
IP 问题。后端需要知道真实客户端 IP,不能只看到负载均衡的 IP。Nginx 做负载均衡时同样要透传 X-Forwarded-For,而且我们还能让 Nginx 把真实 IP 直接替换进 $remote_addr:
set_real_ip_from 10.0.0.1; # 上行设备的地址,按需调整 real_ip_header X-Forwarded-For;set_real_ip_from 的含义是:来自这个地址的请求,XFF 里的第一个 IP 可以被信任为真实客户端。这个配置在多层代理、WAF + 负载均衡的架构里几乎是必需的,否则应用看到的全是一个奇怪的代理 IP,限流和日志分析都会乱套。我排查过一次线上限流误伤,最后发现就是没配 real_ip,所有用户被算成同一个入口 IP,触发了几千次误拦。
4. CDN 加速原理与验证手段
4.1 DNS 调度 + 边缘缓存:CDN 为什么"快"
CDN 的核心机制就两步。
第一步是 DNS 调度。普通域名解析时,DNS 服务器直接返回源站 IP;接入 CDN 后,域名 CNAME 到 CDN 厂商的调度域名,调度系统根据用户出口 IP、位置、运营商,返回一个"最优节点"的 IP。这就是为什么不同地区的用户 ping 同一个 CDN 域名,得到的 IP 可能完全不一样。
第二步是边缘缓存。节点收到请求后,如果本地有对应缓存副本且没过期,直接返回,这叫缓存命中;如果没有,就回源站取一次,拿回来缓存,同时按响应头里的 Cache-Control 决定缓存多久。图片、CSS、JS 这类静态资源非常适合缓存,命中率上去之后,源站负载可以下降一个数量级。
所以判断一个服务适不适合套 CDN,核心就看两点:内容是否可缓存(静态还是动态)、用户分布是否很广。如果用户都在公司局域网,套 CDN 意义不大;如果用户遍布全国,CDN 带来的体感提升会非常明显,尤其图片和视频类站点。
4.2 怎么查一个域名到底有没有走 CDN
"如何查询本地 CDN"这个场景我遇到过很多次。想确认的通常是两件事:这个域名接入 CDN 了吗?本地解析到的 IP 到底是不是 CDN 节点?
第一招,看 DNS 解析结果:
dig example.com nslookup example.com如果解析出来的是多个不同 IP,或者解析速度极快,大概率是 CDN。更明显的是,同一个 CDN 域名,你在手机流量和家里宽带的网络下分别解析,拿到不同 IP,这就是本地调度在起作用。
第二招,看响应头。用 curl 拉一次请求,检查返回头:
curl -I https://example.com/static/app.js常见的 CDN 特征字段有:Via、X-Cache、X-Cache-Lookup、Age、X-Via、X-Served-By 等。比如有些 CDN 会返回 Via: cache,某些厂商直接标 X-Cache: HIT 或 MISS。Age 字段表示缓存已经存在了多少秒,也是重要参考。
第三招,看节点归属。把解析出来的 IP 拿到 IP 库(比如 ipip.net 或本地 whois)查归属,如果厂商名明显是 CDN 服务商、或者 AS 号是云厂商的,基本可以确认。还可以对比 TTL:CDN 域名的 DNS TTL 一般不长(几十秒到几分钟),因为节点调度的 IP 可能随时变;源站 A 记录的 TTL 动辄几小时甚至一天。
提醒一句:CDN 调度结果和你所在的网络环境强相关。在公司查到的节点,和你家里手机 4G 网络查到的未必一致,别因为 IP 不同就怀疑配置有问题。
4.3 回源、缓存刷新与版本更新策略
CDN 最能坑人的地方是"更新了但用户看不到"。原因很简单:旧文件还在节点缓存里,用户请求被缓存直接命中,压根没到源站。
我的习惯是双管齐下。
第一,从源头控制缓存时间。静态资源按类型区分设置 Cache-Control:
- 带 hash 的文件(app.abc123.js):可以设一年,因为文件名变了就是新文件,天然防缓存;
- 不带 hash 的入口文件(index.html):设 no-cache 或很短的 max-age,保证每次都回源确认;
- 图片按内容性质设置,比如 7 天到 30 天,频繁换图的运营位单独处理。
第二,版本更新后主动刷新 CDN 缓存。各家 CDN 控制台都有"刷新缓存 / URL 预热"功能,把更新过的 URL 提交刷掉。批量刷新时注意配额,我有一回误操作把整个目录刷新了,结果高峰期回源流量直接把源站打满,源站 Nginx 报警刷屏,这个教训很深刻。
排查"更新为什么没生效"时,别只盯着源站。按顺序查:先用 curl 本地直连源站确认新内容没问题,再看 CDN 控制台有没有缓存记录,最后看响应头里的 Age 字段。如果 Age 很大,就是缓存没刷或者 TTL 太长,跟你的源站代码一点关系都没有。
5. 高频坑位排查实录:证书、WebSocket 与缓存
5.1 浏览器报 err_cert_common_name_invalid 怎么办
这个报错在反代场景里非常常见,尤其是自己签证书或套了多层代理时。英文直译是"证书通用名无效",意思是:浏览器请求的域名,和服务器返回证书里写的域名对不上。
最常见的三种成因:
- 证书只签了 www.example.com,用户却用 example.com 访问。现在主流 CA 一般同时签两个域名,但老证书或自己签的很容易漏。
- 反代转发到 https 后端,而后端证书是自签的或域名对不上。浏览器到 Nginx 这段证书没问题,但 Nginx 转发时默认会校验上游证书,校验不过,报错就会在浏览器侧暴露出来。
- 直接拿 IP 访问一个只签了域名的证书。
修复思路按层来:
先查入口证书的 SAN 是否覆盖所有对外域名:
openssl x509 -in example.com.crt -noout -text | grep -A1 "Subject Alternative Name"如果缺域名,找 CA 重新签或者补全证书。再看后端是 https 且证书不可信、域名不一致的情况。内网环境如果确认可信,可以让 Nginx 转发时不校验上游证书:
location / { proxy_pass https://10.0.0.11:8443; proxy_ssl_verify off; proxy_ssl_server_name on; proxy_ssl_name 10.0.0.11; # 指向上游证书实际签发的域名或 IP }注意:proxy_ssl_verify off 只解决"转发链路"的校验问题,浏览器到你 Nginx 这一段仍然必须持有有效证书。生产环境尽量给上游也用受信任证书,别长期挂着关闭校验,否则哪天上游证书过期,你会在半夜被报警电话叫醒。
我在 Windows 上练手反代时被这个报错卡过一晚上,最后发现就是后端自签证书没加 SAN,和 Nginx 配置半毛钱关系都没有。所以遇到证书报错,先分清楚是浏览器到 Nginx 这一段,还是 Nginx 到上游这一段,再动手改配置。
5.2 WebSocket 反代:必须补的三行配置
给 WebSocket 服务做反代时,很多人把普通 HTTP 配置直接搬过来,结果前端一直连接失败。原因是 WebSocket 握手需要 Upgrade 请求头,而 Nginx 默认不传递这个头。
标准做法是配合 map 指令实现:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { # ... location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s; } }解释一下原理:$http_upgrade 是客户端发来的 Upgrade 头的值,如果是 WebSocket,就是 websocket。map 块把它的值转换到 Connection 头,没有升级请求时保持 close,避免污染普通 HTTP 请求。proxy_read_timeout 也要拉长,因为 WebSocket 长连接可能好几分钟不说一句话,默认 60 秒内没有任何数据交换就会断开,前端就会频繁掉线重连。
这个坑隐蔽在:普通请求看起来都正常,只有 WebSocket 用户反映"一会儿就断"。如果你加了上面三行配置还断,就抓包看是不是中间还有一层防火墙或四层负载均衡在掐长连接。
5.3 更新不生效、偶发超时的通用排查顺序
最后分享一个通用的排查习惯,在 CDN、负载均衡、反向代理三类场景里都适用。不管现象是"更新不生效"还是"一会儿通一会儿不通",我都按这个顺序来:
- 先确认问题在哪一层。本地直接 curl 源站 IP(绕过 DNS)测试,如果也有问题,先修应用;如果正常,问题在链路。
- 再确认 DNS 解析的最终结果。dig 看是不是被 CDN 接管,解析到的 IP 是不是预期的节点。
- 然后看中间层日志。Nginx 的 access log 能看到请求是否到达、上游是哪台、返回什么状态码;CDN 控制台的日志能看到命中与回源情况。
- 最后才动配置。改配置前先备份,改完 reload 并保留旧配置目录,方便随时回滚。
这个习惯帮我避免了很多次"瞎改一通、越弄越糟"的情况。尤其是三层都配了的时候,直接在源站和浏览器之间来回猜,效率极低;按层切分,问题通常能在十几分钟内定位。有一次线上偶发 502,我按这个顺序查到是某台后端机器的 keepalive 连接被防火墙静默断开,Nginx 复用连接失败导致,定位过程不超过十分钟,而之前同事在应用代码里找了两天都没头绪。
我个人还有一个小但很值钱的习惯:给每个域名建一份"链路档案",记清楚从 DNS 到 CDN、到负载均衡、到 Nginx、再到后端应用,每一层用的什么产品、证书什么时候到期、上次变更是什么时候。平时不觉得有用,一旦线上出问题,这份档案能帮你直接砍掉一半排查时间。CDN、负载均衡、反向代理这三个词,分开看原理都不复杂,复杂的是它们叠加在真实链路里之后的状态。把每一层都想成一个人、一件事,问题来了按层拆,你就不会慌。