基于Nginx + Apache mod_qos的多层流量整形架构设计:云服务器上的高并发保命方案

1. 背景与痛点

云服务器上的 Web 服务,最怕的不是正常流量,而是「突发毛刺」和「慢连接拖死」。Nginx 处理静态文件是一把好手,但一旦动态请求打到 Apache,Apache 的线程池就会成为阿喀琉斯之踵。肉眼看 Apache 状态页,W (Waiting) 状态堆积上千,机器负载不高,但用户请求就是转圈。原因是每个请求占用了后端连接,而 Nginx 的错误重试机制又放大了流量。

传统方案只做前端限流,却忽略了后端的连接级防护。本文给出一个真正的多层流量整形架构:Nginx 做边缘限流,Apache 用 mod_qos 做后端深度整形,同时用 mod_remoteip 解决真实 IP 穿透问题。这套方案在轻云互联的云服务器上实测,能扛住 30 倍突发流量而不打满后端连接。

2. 架构设计

  • 入口层:Nginx(主)——静态资源直接返回,动态请求反向代理到 Apache,并承担 TLS 终结。
  • 后端层:Apache(event MPM)——只处理动态应用,启用 mod_qos、mod_remoteip。
  • 流量路径:客户端 → Nginx(limit_req/limit_conn) → Apache(mod_qos 二次整形) → 应用。

为什么要双重限流?Nginx 的 limit_req 基于 token bucket,只能做“频率”限制;Apache mod_qos 能针对并发连接、每 IP 带宽、慢客户端超时做精细控制。前者防止攻击者打满 Nginx,后者防止慢连接拖死 Apache 线程。

3. Nginx 层配置

http {
    # 每个 IP 每秒 10 个请求,突发 20 个
    limit_req_zone $binary_remote_addr zone=flood:10m rate=10r/s;
    # 每个 IP 最多 10 个并发连接
    limit_conn_zone $binary_remote_addr zone=perip:10m;

    upstream apache_backend {
        server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
        keepalive 32;
    }

    server {
        listen 443 ssl;
        server_name example.com;

        # 静态资源直接处理
        location ~* \.(css|js|png|jpg|webp|svg)$ {
            root /var/www/static;
            expires 7d;
            access_log off;
        }

        location / {
            limit_req zone=flood burst=20 nodelay;
            limit_conn perip 10;

            proxy_pass http://apache_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;

            # 关键:连接超时和半关闭处理
            proxy_connect_timeout 5s;
            proxy_read_timeout 60s;
            proxy_buffering off;
        }
    }
}

注意 proxy_buffering off——对于实时性要求高的接口,关闭缓冲能让 Apache 快速释放连接;但如果静态压测吞吐,建议开启 buffering。

4. Apache mod_qos 深度配置

先安装和启用模块(Debian/Ubuntu):

apt install libapache2-mod-qos
a2enmod qos
a2enmod remoteip

Apache 配置(mod_qos 部分):

QS_SrvRequestLimit 500
# 每个 IP 同时只允许 10 个连接
QS_ConnLimitPerIP      10
# 每个主机(相对于 IP)最大连接 100
QS_ConnLimitMaxPerHost 100
# 服务端总最大连接数(线程池上限)
QS_SrvMaxConn          600
# 允许 60 秒内关闭的并发连接数
QS_SrvMaxConnClose     120

# 关键:慢连接防护
# 当客户端超过 10 秒没发送数据,强制断开
QS_ClientEventLimit    10
QS_ClientEventBlock    MSG

# 带宽整形:每个 IP 最大 1MB/s
QS_SrvMaxBandwidthPerIP 1M

这些指令不是随意的。注意 QS_SrvMaxConn 必须小于 Apache 的实际线程上限(event MPM 的 ThreadsPerChild x ServerLimit),否则 mod_qos 会报错。而 QS_ClientEventLimit 可以识别空闲连接——即使请求没有结束,只要不读写数据,10 秒后就会被踢掉。

再配合 event MPM 调优:

MPM event:
    ServerLimit 100
    ThreadsPerChild 25
    MaxRequestWorkers 250
    MinSpareThreads 25
    MaxSpareThreads 75
    ThreadStackSize 65536

这样 Apache 最多 250 个并发,mod_qos 的 QS_SrvMaxConn 设为 600 已经覆盖,但实际生效时连接会被控制在 250 以内。所以建议把 QS_SrvMaxConn 设为 240,留一点余量。

5. 真实 IP 穿透

因为 Nginx 在后端,Apache 看到的所有连接都来自 127.0.0.1。必须启用 mod_remoteip

RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1

这样 Apache 日志里记录的 %h 才会是真实客户端 IP,mod_qos 的每 IP 限制也才能生效。否则所有流量都算到 Nginx 的头上,限流直接失效。

6. 内核模块配合

仅靠应用层还不够,还需要调整系统 TCP 参数,避免 TIME_WAIT 堆积:

cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_syn_backlog = 65535
net.core.somaxconn = 65535
EOF
sysctl -p

对于 keepalive 长连接,Nginx 到 Apache 的 keepalive 必须开启(上面 upstream 里已经加了 keepalive 32),否则每次请求都会新建 TCP,白白增加握手开销。在轻云互联的云服务器上,开启 keepalive 后 QPS 能有 20% 的提升,同时 CPU 占用率下降。

7. 压测与排错

abwrk 压测时,不要只看 QPS,要重点观察两个指标:

  • Apache 的 scoreboardW 状态数量——如果持续超过 100,说明有慢连接,检查 mod_qos 的 QS_ClientEventLimit 是否生效。
  • Nginx 的 error.log 中是否有 limit_req 丢弃记录。
# 查看 mod_qos 是否生效
apache2ctl -M | grep qos
# 查看实时连接数
watch -n1 'ss -s'
# 查看特定 IP 被拒绝的日志
grep -i "qos" /var/log/apache2/error.log | tail -20

如果发现 mod_qos 的日志没有输出,检查模块加载顺序:RemoteIPHeader 必须在 QS_ConnLimitPerIP 之前加载,因为 mod_qos 依赖请求的主机信息来判断 IP。

8. 总结

这套架构的关键不是堆配置,而是“连接级防护”和“真实 IP 还原”的配合。Nginx 管好入口频率,Apache 管好线程生命周期,再加上 mod_remoteip 的准确定位,云服务器才能在高并发下保持稳定。单个服务器如此,多台后面再接 LVS,同样适用。

记住:限流做不到“绝对禁止”,只能“把损失控制在你能承受的范围内”。上述配置已经在生产环境跑了一年,再也没有出现过“连接不够用”的告警。