当一台 Apache 以反向代理的角色跑在业务入口时,它面对的流量往往是不均匀的:少数大文件下载、长轮询接口会占用大量带宽和连接,而普通的小请求被挤在队列后面,表现为用户打开页面卡顿、接口响应忽快忽慢。要解决这个问题,光靠 Apache 自身的配置还不够,需要把应用层的缓存策略与内核层的队列调度结合起来。FQ_Codel 正是内核层最值得使用的一种公平排队算法,它配合 Apache 的 mod_cache 代理缓存,可以从两个层面同时改善体验。

一、FQ_Codel 到底解决了什么问题
FQ_Codel 是 Fair Queue Controlled Delay 的缩写,由内核 3.5 引入,从 Linux 内核 4.20 开始成为默认的队列规则(qdisc)。它结合了两个思想:一是公平排队(FQ),内核会按五元组把流量拆分成几百条独立的流队列,每条流各自排队、各自发包,某一条流疯狂发包时,影响的只是它自己的队列,不会挤占其他流;二是 Codel(Controlled Delay),一种主动管理队列长度的算法,当检测到某个队列的滞留时间超过目标值(默认 5ms)时,就开始按概率丢弃或标记报文,促使发送方降速,从而把缓冲膨胀(bufferbloat)压下来。
对一台反向代理服务器来说,这一点非常关键。代理机器的上行链路通常是瓶颈,如果没有公平排队,内核会以先进先出的方式发送所有报文,一个正在下载大文件的客户端会让后续的小请求报文排在几千个报文后面,单个报文的排队延迟轻松超过几百毫秒。启用 FQ_Codel 之后,每个客户端连接独立成流,小请求的报文几乎不被大流量干扰,响应时间会立刻变得稳定。
先确认当前系统的队列规则:
# 查看当前 qdisc tc qdisc show # 如果输出中包含 fq_codel,说明已经是公平排队 # 典型输出:qdisc fq_codel 0: dev eth0 root refcnt 2 limit 10240p flows 1024
多数现代发行版默认已经是 fq_codel。如果是 pfifo_fast 之类的旧规则,切换方法在后文介绍。
二、配置 Apache 的反向代理与 mod_cache
应用层要做的第一件事,是让能缓存的响应别穿透到后端。Apache 提供了 mod_cache、mod_cache_disk 和 mod_proxy 三个模块,组合起来可以对后端返回的内容做磁盘缓存,命中后直接由代理本机响应,既减轻后端压力,也减少需要经过网卡队列的重复流量。
典型的配置如下:
# 启用模块(以 Ubuntu/Debian 为例,其他发行版类似)
# a2enmod proxy proxy_http cache cache_disk headers
<IfModule mod_cache.c>
CacheEnable disk "/"
CacheDefaultExpire 3600
CacheMaxFileSize 50000000
CacheDirLevels 2
CacheDirLength 1
</IfModule>
<IfModule mod_proxy.c>
ProxyPreserveHost On
ProxyPass "/" "http://127.0.0.1:8080/" retry=30
ProxyPassReverse "/" "http://127.0.0.1:8080/"
</IfModule>几个参数值得注意。CacheMaxFileSize 用来限制单文件缓存上限,避免巨大的文件把磁盘缓存目录塞满;retry=30 表示后端故障时 30 秒内不再重复尝试,防止雪崩。另外,如果后端返回的响应带有 Cache-Control 头,mod_cache 会遵循它,所以更推荐在后端或代理层用 Header set Cache-Control 明确控制缓存策略,例如静态资源设置较长的 max-age,动态接口设置 no-cache 或短时效。
还有一个容易被忽略的点:对确实无法缓存的大文件下载,建议在 ProxyPass 上单独拆一个路径,并配合 RateOutputBody 之类的方式限速(或交给 mod_ratelimit),从源头控制单连接占用的带宽,这与内核层的 FQ_Codel 是互补关系:应用层限的是总量,内核层保证的是分发的公平。
三、在出口网卡上启用并调优 FQ_Codel
如果系统默认没有启用,可以用 tc 命令手动切换,并针对代理服务器的上行带宽设置参数:
# 假设出口网卡为 eth0,上行带宽 500Mbit
tc qdisc replace dev eth0 root fq_codel \
limit 8192 \
target 5ms \
interval 100ms \
flows 2048 \
quantum 1514 \
ecn逐个解释这些参数。limit 是队列中报文总数上限,8192 对代理机器够用,内存紧张可以降到 4096;target 是 Codel 希望报文在队列中停留的目标时间,默认 5ms,一般不建议改;interval 是检测周期,保持 100ms 的默认值即可;flows 是最大并发流数量,默认 1024,反向代理上同时活跃的连接数可能更多,适当调大可以避免流被哈希到同一条队列;ecn 开启显式拥塞通知,让内核优先用标记代替丢包,对 TCP 吞吐更友好。
如果网卡本身支持硬件多队列,也可以更简单地用 fq(纯公平排队)配合服务器的整体拥塞控制算法。在 /etc/sysctl.conf 里设置 BBR 拥塞控制,与 fq 搭配是另一种常见组合:
# /etc/sysctl.conf net.core.default_qdisc = fq_codel net.ipv4.tcp_congestion_control = bbr # 生效 sysctl -p
需要说明的是,fq_codel 主要在瓶颈链路(通常是出口)起作用。如果瓶颈在服务器内部(比如 CPU 打满),单纯调队列参数收效有限,此时应回到 Apache 层做连接数限制和缓存命中率的优化。
四、验证效果:延迟、公平性与缓存命中率
配置完成后,用三类指标验证效果。第一是队列状态,用 tc -s qdisc show dev eth0 查看,重点看 ecn_mark 和 drop 的比例,如果 drop 很高说明链路确实拥塞且限流生效;第二是延迟稳定性,在一台客户端上持续 ping 代理服务器,同时另一台客户端发起大流量下载,对比启用前后 ping 延迟的变化,健康的 FQ_Codel 应该能把延迟稳定在几十毫秒以内而不是飙升到几百毫秒;第三是 Apache 侧的缓存命中率,开启 CacheDetailHeader on 后响应头会带上 hit 或 miss 标记,也可以查看 mod_cache_disk 的缓存目录大小变化来估算命中情况。
# 查看队列统计 tc -s qdisc show dev eth0 # 压测小请求延迟(配合另一端跑大流量) ab -n 20000 -c 50 https://your.proxy.ip.example/static/ # 观察 P99 延迟是否稳定
最后做个小结:Apache 的 mod_cache 解决的是重复流量的问题,让大量请求根本不需要走网络;FQ_Codel 解决的是剩余流量的调度问题,让每条连接公平共享带宽、排队延迟可控。两层配合使用,反向代理在高并发下的响应时间分布会明显收敛,大文件下载与普通页面请求互不干扰,这才是公平排队在实际业务里的价值。调优时记住一个原则:先做缓存削峰,再谈队列公平,顺序反了容易在错误的方向上浪费精力。