ITSWE博客

反向代理的十个深坑:一次讲完,个个真撞过

http2 弄死 WebSocket、add_header 继承陷阱、限速配置的静默失败……十个反代深坑,全部来自同一台服务器的真实事故。

同一台服务器上,反代相关的坑我们前前后后踩了十来个。这篇集中讲十个,每个都是真实事故,按“症状—根因—修法”写。同一套软件,坑的分布很有规律,提前看完能省很多深夜。

一、http2 弄死 WebSocket

症状:网页端的长连接功能在某个新环境里“静默死”——不报错,就是连不上。根因:连接升级机制只存在于 HTTP/1.1,升级到 http2 之后,升级请求头恒为空,升级变成普通请求,四十万八或者直接落空。最阴的是协商由默认服务块决定,只改单个站点无效,必须全站摘除。修法:全站 443 禁用 http2,并把“面板重建站点会偷偷加回来”写进检查清单。

二、add_header 的继承陷阱

在某个路径块里加一条缓存头,结果整个站点在该路径下的安全头(HSTS 等)集体消失——因为这类指令只要在子层级出现过,父级的全部声明就不再继承。修法是子层级把需要的头全部重新声明一遍,或者改用不参与继承规则的缓存指令(如 expires)。凡是“加一个头”的需求,先查继承规则再动手。

三、限速配置的静默失败

修改限速的键变量后重载,配置检查通过、回显正常,实际新配置没生效——某些指令的变量不允许跨重载更换,主进程会放弃重载,但检查工具是独立进程不报错,回显照常打印。判据是工作进程的实际运行时间:换了才是真换了。这条的教训超出了反代本身:一切配置变更,以运行时实测为准。

四、程序级缓存清不掉

应用自己提供的缓存清除接口,清不掉反向代理这一层的程序缓存。改完词条线上还是旧的十分钟。修法粗暴有效:进容器把缓存目录清空,再触发一次回源写入新条目。缓存要按层清,每层的工具不一样。

五、出口改写的失配

做响应内容替换时,替换规则对不上——上游返回的是压缩后的字节流,替换自然失败。修法两件套:显式清空发给上游的压缩协商头,并关掉“只替换第一处”。顺带一学:位置块里有自己的传递头声明时,不再继承父块的,漏一个头就出玄学。

六、挂载树里的软链接

数据目录用绑定挂载给容器用,目录里的软链接在宿主机正常、进容器就断——软链接只在宿主机的命名空间里解析,容器里没有那条路径。修法是把软链换成对两边都透明的绑定挂载。

七、默认服务块的“设计如此”

未知域名的请求握手即断,排查半天发现是空证书的默认兜底块,刻意为之——防止任意 SNI 探测。知道它是设计,才不会误判成故障。排查前先问:这个行为会不会是有意的。

八、两代架构的混淆

一台机器的 443 是流层按服务名分流,另一台是应用层直接按域名匹配。把 A 机器的排障思路套到 B 机器,找了半天不存在的分流配置。多台机器运维,架构差异要写进档案,换机器先看架构图。

手机浏览器陷入重定向死循环,桌面复现不了。根因是会话 Cookie 的同站策略设为最严——移动端从其他应用发起的导航不回传最严策略的 Cookie,服务器收不到登录态就踢重登,死循环。改为宽松策略后综合解。移动端 3xx 死循环但桌面正常,先查 Cookie 策略。

十、上传体积的隐形上限

大文件推送莫名失败,默认的请求体上限在作怪。对需要大流量的站点(比如代码托管)显式放开限制。小坑,但每次新建站点都会忘。

十一、真实地址的还原

有边缘层转发时,源站看到的全是边缘出口地址。还原真实客户端有两条路:协议层转发时透传原始地址,或应用层读转发头。前者架错层等于白防,后者要防伪造——实测证明边缘对转发头是追加式的,伪造者只能加在前面,真实地址永远在最右,所以取最右一个值,伪造无法绕过。这个“取最右”的规则写进所有频控与封禁的键设计里。

十二、出口压缩让替换失效

与第五坑同族的另一个:响应内容替换规则在部分路径生效部分不生效。根因是位置块的传递头声明不继承——有的块清了压缩协商头,有的没清,没清的上游返回压缩流,替换器对压缩流无能为力。修法是把“清压缩协商+关单次替换”做成标准模板块,凡开替换的位置块整块复制。替换功能必须配一条验证:伪造一个目标串上线,确认出口真的被改写。

十三、协议头传递的三件套

长连接代理还有一组易漏的声明:升级头、连接头、协议版本。三者缺一,升级请求就退化成普通请求。它们和第一坑(http2)是同一场事故的两面——协议选对了,头没传对,一样死。标准写法是四行连用,一字不能少。

总结

十三个坑里,静默失败占了一半——反代的很多错误不打日志,只是“行为和预期不一样”。所以这个领域的核心功夫不是背配置,是给每个关键行为建立实测基线:改之前量一次,改之后量一次,数字说话。以及,把每一条踩过的坑写进团队的检查清单——坑不会因为踩过就消失,只会因为写下来而不再咬人。

运维Nginx
返回全部文章