CDN回源慢报错504?可能是源站内核半连接队列与防火墙限频在“打架”

CDN回源时出现504超时,往往不是源站应用本身挂了,而是操作系统内核与防火墙在处理海量连接请求时“卡住”了。本文从TCP三次握手原理入手,用通俗语言解释半连接队列溢出和iptables限频导致丢SYN的机制,并给出具体的排查命令和调优方案。

CDN回源慢报错504?可能是源站内核半连接队列与防火墙限频在“打架”
封面图:ZuCDN · ZuCDN 原创

CDN TCP看似简单,真正落地时却很容易踩坑。打开网站,页面转圈半天,最后浏览器弹出“504 Gateway Time-out”——这大概是运维新手最怕看到的报错之一。很多人第一反应是去看源站的应用日志,但翻了一遍发现Nginx、PHP-FPM都正常,内存、CPU也还有富余,那问题到底出在哪?

如果你使用了CDN(Content Delivery Network,内容分发网络),504通常意味着“CDN节点向源站发起TCP连接时,源站在规定时间内没有完成握手”。换句话说,问题很可能不在应用层,而在更底层的网络堆栈里——最常见的是源站的内核TCP半连接队列被填满,或者防火墙因为连接频率过高直接丢掉了SYN包

先搞清楚基本的TCP握手与队列

要理解半连接队列,先要回忆一下TCP三次握手的过程。客户端(CDN节点)发送SYN包,源站内核接收到SYN后,会把这个连接标记为“半连接”状态,放入一个叫做半连接队列(SYN Queue)的数据结构里,然后回复SYN+ACK。客户端再发回ACK,连接进入“全连接”状态,应用程序(比如Nginx)才能从全连接队列(Accept Queue)里取出这个连接来处理。

半连接队列的大小由内核参数 net.ipv4.tcp_max_syn_backlog 控制,系统默认值通常只有128或256。当CDN在短时间内(比如秒杀活动或流量突增)向源站发起大量并发连接时,半连接队列会迅速占满。新来的SYN包无法入队,内核只能丢弃——于是CDN节点一直等不到SYN+ACK,最终触发超时,返回504。

全连接队列也同样重要

如果半连接队列没事,但全连接队列满了呢?同样会丢连接。全连接队列大小由应用程序的 backlog 参数(如Nginx的 listen backlog)和内核参数 net.core.somaxconn 共同决定。排查时这两个队列都需要检查。但根据我的经验,CDN回源场景下,半连接队列溢出的概率远高于全连接队列,因为CDN节点的分布特性会让源站同时收到大量来自不同IP的SYN请求。

防火墙限频:另一个常见的“隐形杀手”

除了内核队列,还有一个容易被忽视的因素:源站上的防火墙规则。很多系统管理员为了防CC攻击,会在iptables里通过 limithashlimit 模块限制单个IP的连接速率。但如果这个限速规则写得太死——比如 -m limit --limit 5/s——当CDN的多个节点(虽然IP不同)同时请求时,防火墙会按源IP独立计数,正常情况不会触发。但若规则错误地使用了 --limit-burst 或没有正确匹配连接状态,就可能把CDN的合法SYN包当成“超过限速”直接DROP。更糟糕的是,一些防火墙默认配置会丢弃超过 nf_conntrack 表容量的新连接,导致SYN包石沉大海。

如何区分问题出在内核队列还是防火墙?

排查方法很简单:登录源站服务器,执行以下命令:

ss -s

查看输出中的 TCP: listen overflowsTCP: listen drops 值。如果这些数值在不断增长,说明半连接队列或全连接队列发生了丢包。随后用 sar -n TCP,ETCP 1 实时观察 listen overflows 变化。

如果队列没有溢出,丢包依然发生,那就要检查防火墙。执行 iptables -L -n -v 查看每个规则的 pkts 计数,特别是INPUT链中针对SYN包的DROP规则。如果计数迅速增加,说明防火墙在“背锅”。

调优实战:解决半连接队列溢出的标准操作与CDN TCP

我的处理经验

确认问题后,按以下步骤操作:

  1. 增大半连接队列:编辑 /etc/sysctl.conf,添加或修改:
    net.ipv4.tcp_max_syn_backlog = 8192
    net.core.somaxconn = 2048
    注意 tcp_max_syn_backlog 不能超过 net.core.somaxconn,建议后者调整为前者一半或相等。
  2. 启用SYN Cookie:这是一种防御SYN Flood的机制,当半连接队列满时,内核会用Cookie代替队列来保存连接信息。虽然会略微增加CPU开销,但对现代服务器影响很小。添加:
    net.ipv4.tcp_syncookies = 1
  3. 调整Nginx的backlog:在server块中设置 listen 80 backlog=1024;listen 443 ssl backlog=1024;,使应用程序能够接收更多等待中的连接。
  4. 优化防火墙规则:如果使用了 connlimitlimit 模块,检查是否误限。对于CDN回源,建议在防火墙规则中放行CDN节点IP段(可以从CDN控制台获取),或者将限速阈值调高到 100/s 以上。

验证与监控:确保改动生效——CDN TCP

先看关键判断

执行 sysctl -p 使内核参数生效。重启Nginx。然后用压测工具(如wrk)模拟大量并发连接,观察 ss -s 中的 listen overflows 是否归零。同时用 tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn) != 0' 抓包,确认源站回复了SYN+ACK,没有丢包。

小结:别只盯着应用日志

先看关键判断

下次遇到CDN回源504,先别急着重启Nginx或找CDN客服。登录源站,先看内核的TCP半连接队列有没有溢出,再看防火墙有没有丢SYN。这两个问题在无状态的TCP层,应用日志通常没有任何记录。理解它们的工作原理,你就能在几分钟内定位根因,而不是靠运气盲猜。

当然,流量突增是诱因。如果调优后发现高并发场景下依然频繁超时,那就要考虑扩容源站带宽、增加CDN缓存命中率、甚至升级到多节点负载均衡了。但这是后话——先解决眼前的504再说。真正做好CDN TCP,靠的不是参数堆砌,而是持续验证。

延伸阅读