Showing Posts From
Nginx
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 从源站本机开始逐层排查。
让 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 反向代理 + 全局跨域:那些看着通了但其实拦了的坑
前后端分离项目上线,前端一直报跨域,可 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。
