Showing Posts From
Nginx
Nginx upstream 负载均衡配置:轮询、ip_hash、权重、least_conn 与 PHPStudy 位置问题
基本结构 upstream 定义后端服务器组,proxy_pass 引用该组名: upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; server 192.168.1.12:8000; }server { location / { proxy_pass http://backend; # ... 其他 proxy 配置 } }四种均衡策略 轮询(默认):依次分配请求,适合无状态服务: upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; }ip_hash:同一客户端 IP 固定打到同一台服务器,适合有 Session 的服务: upstream backend { ip_hash; server 192.168.1.10:8000; server 192.168.1.11:8000; }weight(加权轮询):按权重比例分配流量: upstream backend { server 192.168.1.10:8000 weight=4; server 192.168.1.11:8000 weight=2; server 192.168.1.12:8000 weight=1; }least_conn(最少连接):新请求优先分配给当前连接数最少的服务器,适合耗时不均匀的服务(如 AI 推理、文件处理): upstream backend { least_conn; server 192.168.1.10:8000; server 192.168.1.11:8000; }健康检查 max_fails 次失败后暂时摘除节点,fail_timeout 秒后重试: upstream backend { server 192.168.1.10:8000 max_fails=3 fail_timeout=30s; server 192.168.1.11:8000 max_fails=3 fail_timeout=30s; }错误:upstream directive is not allowed here [emerg] "upstream" directive is not allowed here in vhosts/xxx.conf:24原因:upstream 只能放在 http {} 块内,不能放在 server {} 或 location {} 内。 PHPStudy 等面板生成的 vhosts 文件是 server {} 块,不能在里面定义 upstream。 解决方案:在主 nginx.conf 的 http {} 块中定义: # nginx.conf http { upstream backend { server 192.168.1.10:8000; server 192.168.1.11:8000; } include vhosts/*.conf; # vhosts 文件只包含 server{} 块 }配置验证与重载 nginx -t # 检查配置语法 nginx -s reload # 平滑重载(不中断现有连接)
Nginx 反向代理改造成负载均衡:踩坑与正确写法
原本站点是这样的反向代理: location / { proxy_pass http://192.168.5.31:8000; proxy_set_header Host $host:$server_port; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }后端加机器了,要改成负载均衡。看着简单,但一路踩了两个坑。 步骤一:定义 upstream 轮询模式最简单: upstream backend { server 192.168.5.31:8000; server 192.168.5.32:8000; server 192.168.5.33:8000; }location 里的 proxy_pass 改成: proxy_pass http://backend;三种策略怎么选 WebSocket / 长连接:ip_hash 同一个客户端 IP 固定打到同一台机器,session 不会乱: upstream backend { ip_hash; server 192.168.5.31:8000; server 192.168.5.32:8000; }机器性能不同:weight upstream backend { server 192.168.5.31:8000 weight=4; server 192.168.5.32:8000 weight=2; server 192.168.5.33:8000 weight=1; }流量大致按 57% / 29% / 14% 分。 AI 推理、长请求:least_conn 轮询在这种场景很糟糕——某台机器可能同时挤了 5 个几十秒的推理请求,其他机器闲着。least_conn 会挑当前连接数最少的: upstream backend { least_conn; server 192.168.5.31:8000; server 192.168.5.32:8000; server 192.168.5.33:8000; }顺手加个健康检查,挂了的自动摘除: server 192.168.5.31:8000 max_fails=3 fail_timeout=30s;坑一:upstream directive is not allowed here 改完 reload,直接报: nginx: [emerg] "upstream" directive is not allowed here in D:\phpstudy_pro\Extensions\Nginx1.15.11/conf/vhosts/127.0.0.1_8000.conf:24upstream 只能写在 http {} 块里,不能写进 server {} 或 location {}。而 PHPStudy 类面板的 vhost 文件通常等价于一个 server 配置片段,里面就不能定义 upstream。 正确做法:把 upstream backend {...} 挪进主 nginx.conf 的 http {} 里,vhost 文件里只放 location。 # nginx.conf http { upstream backend { least_conn; server 192.168.5.31:8000; server 192.168.5.32:8000; } include vhosts/*.conf; }坑二:proxy_pass 少了协议头 写成: proxy_pass backend;Nginx 会直接报语法错误。proxy_pass 必须带上 http:// 或 https://: proxy_pass http://backend;检查与重载 nginx -t # 语法检查 nginx -s reload # 平滑重载一句话总结 upstream 放 http {}、proxy_pass 别忘 http://、AI/长任务优先 least_conn。三点一起做好,反代改负载均衡基本一次成。
Cloudflare 525 SSL handshake failed 排查全流程
某天访问站点,Cloudflare 直接抛了个 525: { "title": "Error 525: SSL handshake failed", "status": 525, "detail": "The SSL/TLS handshake between Cloudflare and the origin server failed." }意思就是——Cloudflare 收到了请求,但它和你的源站之间的 TLS 握手没建起来。不是 Cloudflare 挂了,是源站 SSL 有问题。 常见 8 种原因 1. 源站证书失效 在源站本机测: echo | openssl s_client -servername example.com -connect example.com:443看输出里有没有: Verify return code: 0 (ok)如果出现 certificate has expired,证书过期了,续签或换证书。 2. Nginx 证书和私钥不匹配 分别算 MD5,两边必须一致: openssl x509 -noout -modulus -in cert.pem | md5sum openssl rsa -noout -modulus -in key.pem | md5sum不一致就是配错了。 3. TLS 版本太老 Cloudflare 现在只接受 TLS 1.2 / 1.3。Nginx 配置里明确写清: ssl_protocols TLSv1.2 TLSv1.3;4. Cipher Suite 不兼容 某些老配置写得太激进(HIGH:!aNULL:!MD5 只留少数几个),Cloudflare 找不到共同 cipher。用 nmap 探一下源站支持哪些: nmap --script ssl-enum-ciphers -p 443 <origin-ip>5. Cloudflare 用了 Full (strict) 模式 Dashboard → SSL/TLS → Overview 里检查模式:Flexible:Cloudflare 到源站走 HTTP Full:源站要有证书(不校验) Full (strict):源站必须有效证书、链完整、域名匹配如果是 strict,用 Cloudflare Origin CA 签的证书最省事。 6. 证书链不完整(最常见的坑) 如果 openssl s_client 里出现: verify error:num=20:unable to get local issuer certificate多半是 Nginx 只配了 cert.pem 而不是 fullchain.pem。 正确写法: ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;7. 源站 443 没监听 直接测: curl -vk https://<origin-ip>如果 Connection refused,Nginx 没在这个 IP 上监听 443。防火墙、云安全组同样要放行。 8. IPv6 配置错误 Cloudflare 可能优先走 IPv6,如果 Nginx 没监听或者防火墙漏放,就 525。 dig AAAA example.com出现 AAAA 记录时,Nginx 记得写: listen 443 ssl; listen [::]:443 ssl;快速定位思路 先在源站本机执行: openssl s_client -connect localhost:443 -servername example.com本机就 handshake failure:问题在 Nginx 或证书 本机 OK 但外网 525:问题在 IP 路由 / 防火墙 / IPv6 / Cloudflare 连的不是这台机器再看 Cloudflare 实际解析到哪个 IP: dig A example.com dig AAAA example.com用 --resolve 强制走特定 IP 复现: curl -I https://example.com --resolve example.com:443:<origin-ip>一句话总结 525 = Cloudflare 和源站的 TLS 握手失败。八成落在 证书链不完整 或 Full (strict) 模式下证书不合规,用 openssl s_client 从源站本机开始逐层排查。
Cloudflare 525 SSL Handshake Failed:原因排查与修复方法
Cloudflare 525 错误发生在 Cloudflare 与源站之间,不是用户与 Cloudflare 之间。含义:Cloudflare 已经接收到请求,但无法与源站完成 TLS 握手。 快速检测源站 TLS openssl s_client -connect 源站IP:443 -servername 你的域名正常握手会看到: New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384如果出现 handshake failure,问题在 Nginx 或证书配置。 原因一:Nginx 未使用 fullchain 证书(最常见) Let's Encrypt 证书必须用 fullchain.pem,不能只用叶证书: # 正确 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;检查: nginx -T | grep ssl_certificate如果配置的是 cert.pem 而非 fullchain.pem,Cloudflare 会因为证书链不完整而报 525。 openssl 验证时看到这行也是同一问题: verify error:num=20:unable to get local issuer certificate原因二:TLS 版本过低 Cloudflare 要求至少 TLS 1.2。Nginx 配置: ssl_protocols TLSv1.2 TLSv1.3;原因三:Cipher Suite 过于严格 ssl_ciphers HIGH:!aNULL:!MD5;检查源站支持的 cipher: nmap --script ssl-enum-ciphers -p 443 源站IP原因四:开启了客户端证书验证 ssl_verify_client on;Cloudflare 无法提供匹配的客户端证书,直接导致 525。 检查: nginx -T | grep verify_client原因五:443 端口未监听 telnet 源站IP 443或: curl -vk https://源站IP如果 Connection refused 说明 Nginx 没有监听 443。 原因六:IPv6 配置问题 Cloudflare 可能优先走 IPv6: dig AAAA 你的域名如果有 AAAA 记录,但 Nginx 的 IPv6 监听或防火墙有问题: openssl s_client -connect [IPv6地址]:443 -servername 你的域名测试是否成功。 Cloudflare SSL 模式 Cloudflare Dashboard → SSL/TLS → Overview 查看当前模式:模式 要求Flexible 源站无需 HTTPSFull 源站需要 HTTPS,证书可自签Full (strict) 源站需要有效证书,证书链完整使用 Full (strict) 时,源站必须配置 fullchain.pem。 完整排查步骤 # 1. 检查证书配置 nginx -T | grep ssl_certificate# 2. 测试握手(替换为实际源站 IP) openssl s_client -connect 源站IP:443 -servername 你的域名# 3. 检查 IPv6 dig AAAA 你的域名# 4. 检查客户端验证 nginx -T | grep verify_client# 5. 直接用 curl 测试 curl -Iv https://你的域名
让 Nginx 直接返回固定 JSON:不走后端的假接口
前端联调时想要一个"始终返回 VIP=1"的假接口,去写后端太重,Nginx 直接顶上就行。 最简写法 location /is_vip { default_type application/json; return 200 '{"code":200,"msg":"操作成功","data":1}'; }三行做完三件事:default_type application/json;——告诉浏览器返回的是 JSON,不是 text/plain return 200 '...'——直接给 HTTP 200 + 固定 body,不用 upstream 外层用单引号包住整个 JSON,内层用双引号,省得转义防止被前端缓存 浏览器可能把这个 200 响应缓存下来,联调时容易被误导。加个 no-store: location /is_vip { default_type application/json; add_header Cache-Control no-store; return 200 '{"code":200,"msg":"操作成功","data":1}'; }跟根据参数返回不同内容 要根据 query string 分支返回,可以用 if + map: map $arg_uid $vip_result { default '{"code":200,"msg":"操作成功","data":0}'; "1" '{"code":200,"msg":"操作成功","data":1}'; "2" '{"code":200,"msg":"操作成功","data":1}'; }location /is_vip { default_type application/json; return 200 $vip_result; }访问 /is_vip?uid=1 返回 VIP=1,其它返回 VIP=0。 需要更复杂的 mock 逻辑就该上 OpenResty 或者写真后端了——Nginx 原生更适合"固定返回"这类死数据。 一句话总结 default_type application/json + return 200 '{...}'。联调阶段最快的 mock 接口,比启 Node/Python 都省事。
Nginx 直接返回固定 JSON:return 指令与 default_type 配置
Nginx 可以通过 return 指令直接返回固定响应,无需经过后端服务,适合 mock 接口、健康检查、维护模式等场景。 基本写法 location /api/status { default_type application/json; return 200 '{"code":200,"msg":"ok","data":null}'; }default_type application/json 设置响应的 Content-Type,不设置则默认为 text/plain return 200 '...' 返回 HTTP 200 和固定响应体 响应体用单引号包裹,内部 JSON 用双引号禁用缓存 接口类响应通常不应该被浏览器缓存: location /api/health { default_type application/json; add_header Cache-Control "no-store, no-cache"; return 200 '{"status":"healthy","timestamp":"dynamic"}'; }返回不同 HTTP 状态码 # 404 错误响应 location /api/v1/ { default_type application/json; return 404 '{"code":404,"msg":"接口不存在"}'; }# 维护模式 503 location / { default_type application/json; return 503 '{"code":503,"msg":"系统维护中,请稍后再试"}'; }多行 JSON:用变量 return 本身不支持换行,多行 JSON 用变量存储: location /api/config { default_type application/json; set $json_body '{"version":"1.0","debug":false,"features":{"pay":true,"upload":true}}'; return 200 $json_body; }根据请求方法区分 location /api/mock { default_type application/json; add_header Access-Control-Allow-Origin *; if ($request_method = OPTIONS) { add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; return 204; } return 200 '{"code":200,"data":[]}'; }与 echo 模块对比 Nginx 标准版不支持 echo 指令,return 是不需要额外模块的内置方案。OpenResty(含 ngx_http_echo_module)可以用: location /api/test { default_type application/json; echo '{"msg":"hello"}'; }标准 Nginx 统一使用 return 即可满足固定响应需求。
Nginx 反向代理开启 CORS:Access-Control 配置与 OPTIONS 预检处理
前后端分离时,在 Nginx 反向代理层统一添加 CORS 头,比在每个后端框架里单独配置更集中可控。 基础配置 server { listen 80; server_name api.example.com; location / { proxy_pass http://backend-server:8002; 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; # CORS 响应头 add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, X-Requested-With" always; # OPTIONS 预检请求直接返回 204 if ($request_method = OPTIONS) { return 204; } } }always 参数确保 CORS 头在 4xx/5xx 错误响应中也会附加。 带 Credentials 时不能用通配符 Access-Control-Allow-Origin: * 不能与 Access-Control-Allow-Credentials: true 同时使用,浏览器会拒绝: # 错误:浏览器拒绝 add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Credentials true always;需要改为动态匹配请求来源: add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; add_header Vary Origin always;Vary: Origin 告知缓存层不同 Origin 的响应不同,防止缓存污染。 多域名白名单:map 方案 允许特定域名列表访问(带 Credentials): map $http_origin $cors_origin { default ""; "~^https?://localhost(:\d+)?$" $http_origin; "~^https://app\.example\.com$" $http_origin; "~^https://admin\.example\.com$" $http_origin; }server { location /api/ { proxy_pass http://backend-server:8002; add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Vary Origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; if ($request_method = OPTIONS) { return 204; } } }非白名单来源 $cors_origin 为空字符串,不会添加 CORS 头,浏览器拒绝跨域请求。 常见坑 proxy_pass 末尾斜杠影响路径 proxy_pass http://backend:8002; # /api/users → /api/users proxy_pass http://backend:8002/; # /api/users → //users(多了斜杠)后端已有 CORS 头导致重复 如果后端也返回了 CORS 头,Nginx 再叠加会导致响应中出现两个 Access-Control-Allow-Origin,浏览器拒绝。需要在后端关闭 CORS,统一由 Nginx 管理: proxy_hide_header Access-Control-Allow-Origin; add_header Access-Control-Allow-Origin $cors_origin always;
Nginx 反向代理 + 全局跨域:那些看着通了但其实拦了的坑
前后端分离项目上线,前端一直报跨域,可 Nginx 里明明写了 Access-Control-Allow-Origin *。折腾一圈才发现,跨域配置里有几个"看着通了但其实拦了"的隐蔽坑。 最小可用配置 先把最小骨架搭起来——反代到后端 + 全局放开 CORS: server { listen 80; server_name your-domain.com; location / { proxy_pass http://backend.example.com:8002; 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; add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always; if ($request_method = OPTIONS) { return 204; } } }这份能应付纯 API、无登录态的场景。要带 cookie 的话继续往下看,不然会直接被浏览器拦。 坑一:* 和 Allow-Credentials: true 不能共存 很多人从两份教程各抄一段,凑成: add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Credentials true always;浏览器规范明确禁止这种组合,直接跨域失败。 正确做法:把 * 换成 $http_origin 或白名单: add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; add_header Vary Origin always;坑二:Host 被写死 见过这种写法: proxy_set_header Host backend.example.com;后端如果基于 Host 做路由(Spring、Django 之类)或者生成回调 URL,就全乱套。改回: proxy_set_header Host $host;坑三:OPTIONS 只返回 204,没带 CORS 头 写了: if ($request_method = OPTIONS) { return 204; }有些浏览器要求预检响应本身也带跨域头,直接 return 204 就少一批头。解法是让 add_header 放在 if 之前——Nginx 的 add_header 会自动挂到最终响应上,只要不用 error_page 覆盖响应就行。 生产版本(推荐) 同时兼顾 cookie、白名单和预检: map $http_origin $cors_origin { default ""; "~^http://localhost:.*" $http_origin; "~^https://your-frontend\.com$" $http_origin; }server { listen 80; server_name your-domain.com; location / { proxy_pass http://backend.example.com:8002; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, PATCH" always; add_header Access-Control-Allow-Headers "Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Requested-With,Token" always; add_header Vary Origin always; if ($request_method = OPTIONS) { return 204; } } }map 是精髓:预定义的域名才回响原 Origin,其它的会拿到空字符串,浏览器就自然拒绝。 一张表决定怎么选场景 推荐写法纯 API,无 cookie Allow-Origin *带 cookie / token map + $http_origin 白名单本地开发临时放开 Allow-Origin $http_origin一句话总结 CORS 卡住 90% 的时候都是这三件事:* 撞 Credentials、Host 写死、OPTIONS 没配好。抄配置前先想清楚要不要带 cookie。
