Nginx mirror 模块流量镜像实战:用真实线上流量做版本预演与压测取样,用户零感知

线上流量,能不能复制一份去别的机器上跑

做运维的站长经常遇到一个尴尬问题:你改了一版 Nginx 配置、换了一套 PHP 版本、或者上线了一个新的缓存策略,但不敢直接上生产,因为真实流量千奇百怪,测试环境根本模拟不出来。你想要的是:把线上的真实请求,原样复制一份到一台「影子服务器」上跑,看它在真实流量下会不会崩、日志有没有异常,而线上用户完全无感知。

这件事 Nginx 内置的 ngx_http_mirror_module(镜像模块)就能做到,而且配置极简。这篇文章讲清楚它怎么用、能做什么(灰度验证、压测取样、新版本预演)、以及它几个容易误用的坑——尤其是「镜像请求会真的打到后端、会写库」这件事。

mirror 模块的原理:一进两出,主请求不等回包

先建立正确的心智模型:Nginx 收到一个请求后,正常处理并返回给客户端(这是主请求),同时异步把这份请求复制一份,转发到你指定的上游(这是镜像请求)。关键特性有三条:

  • 主请求不等待镜像的响应:镜像的返回被直接丢弃,不会影响客户端看到的响应体或状态码。这就是它「无感」的原因。
  • 镜像请求是独立的一次子请求:它走完整的 upstream 处理流程,会真的连到目标后端。
  • 只镜像「请求」:可以把请求体(POST body)也镜像过去,但响应不回到客户端。
第一坑先讲在最前面:镜像是真实的第二次请求,会真的执行。如果你镜像的是「下单」「发帖」「支付回调」这类会写数据库、发消息、扣库存的接口,影子服务器会真的执行一遍。镜像模块只适合 GET 类幂等请求,或者要镜像的写请求必须打到一台「专门为镜像准备的、数据是脏的也无所谓」的影子环境上。这一条搞错会直接造成重复下单、重复发货。本文后面会展开。

最小可用配置:把所有请求复制到影子机

假设线上是 www.example.com,你有一台影子机 shadow.internal:8080,它的代码版本是最新的、数据库是脱敏副本。配置如下:

server {
    listen 443 ssl;
    server_name www.example.com;

    location / {
        # 关键:把请求镜像到影子上游,镜像 URI 前缀用 /mirror
        mirror /mirror;
        mirror_request_body on;   # POST 请求体也镜像过去

        proxy_pass http://backend_production;
    }

    # 内部 location,只处理镜像子请求,不对外暴露
    location = /mirror {
        internal;
        proxy_pass http://shadow_backend/mirror$request_uri;
        proxy_set_header X-Original-URI $request_uri;
    }
}

upstream backend_production {
    server 10.0.0.10:8080;
}

upstream shadow_backend {
    server 10.0.2.20:8080;   # 影子服务器
}

几个配置点解释一下:

  • mirror /mirror; 写在你要镜像的 location 里,参数是内部 location 的路径。
  • location = /mirror { internal; } 的 internal 很重要:它保证这个 location 只能被 Nginx 内部子请求命中,外部访问 /mirror 会 404。
  • mirror_request_body on; 默认就是 on,但显式写出来更清楚。要关掉用 off。
  • 影子机上用 /mirror$request_uri 拼出原始路径,方便在影子侧按前缀区分「这是镜像流量」。

重载配置后,用一次请求验证:

curl -s -o /dev/null -w "%{http_code}\n" https://www.example.com/ -H "Host: www.example.com"
# 期望 200(主请求正常)
# 同时影子机的 access.log 里应该出现一条 /mirror/ 记录

影子机上确认:

tail -f /var/log/nginx/access.log | grep mirror

手法一:新版本预演——配置/代码改了先让影子跑真实流量

这是 mirror 模块最实用的场景。你打算把 PHP 从 8.1 升到 8.3、或者换掉某个扩展,直接上生产心里没底。做法:

  1. 影子机上部署新版本,数据库连接指向脱敏副本(千万别连生产库)。
  2. 生产 Nginx 加 mirror,把一部分或全部请求镜像过去。
  3. 观察影子机的错误日志、响应状态、PHP-FPM 日志、响应时间。
  4. 如果影子机在真实流量下稳定跑几小时无异常,再考虑正式上线。

注意:镜像请求默认不影响主请求,但会占用额外带宽和后端连接。如果你每天 PV 很大,全量镜像会把影子机的带宽和连接数打满,所以更推荐下面这种「按比例抽样」。

手法二:按比例抽样镜像,别把影子机打爆

mirror 模块本身不提供采样比例——它要么全镜像,要么不镜像。所以抽样要靠「用变量 + 条件代理」来实现。最简单的方法是用 Nginx 的 split_clients(做负载均衡分流用的),或者按 URI/参数条件镜像:

# 用 map 决定是否镜像:URL 里带 ?shadow=1 才镜像
map $arg_shadow $should_mirror {
    default  "";
    "1"      "/mirror";
}

server {
    location / {
        mirror $should_mirror;        # 无参数时值为空,等于不镜像
        mirror_request_body on;
        proxy_pass http://backend_production;
    }
    location = /mirror { internal; proxy_pass http://shadow_backend/mirror$request_uri; }
}

也可以用 split_clients 按客户端 IP 的哈希做 10% 抽样:

split_clients "${remote_addr}AAA" $mirror_sample {
    10%     "/mirror";
    *       "";
}
实战提醒:mirror 指令的值是「内部 location 路径」,如果变量解析为空字符串,则视为不镜像。用 map/split_clients 生成「要么是 /mirror,要么是空」是最干净的采样写法,比在 if 里做判断更安全(Nginx 的 if 是出了名的坑)。

手法三:给影子流量打标记,便于日志分离

影子机上要能把镜像流量和正常流量分开看,否则日志混在一起没法分析。两种做法:

做法 A:沿 URI 前缀区分。上面我们拼了 /mirror$request_uri,影子机日志里天然带 /mirror 前缀,grep mirror 即可分离。

做法 B:加自定义请求头区分。在生产侧给镜像请求打标记:

location = /mirror {
    internal;
    proxy_pass http://shadow_backend$request_uri;
    proxy_set_header X-Shadow-Request "true";
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Original-Host $host;
}

然后影子机的 log_format 里把 $http_x_shadow_request 记下来,或者在后端应用里据此走「只读模式」。加 X-Real-IP 很重要——否则影子后端看到的全是生产 Nginx 的内网 IP,日志分析和限流都会失真。

必须知道的坑(都是真金白银的教训)

  1. 镜像会真的写数据。再强调一次:镜像请求打到影子后端后是一次完整执行。如果影子后端连的是生产库,等于每个请求被处理了两遍——重复下单、重复发帖、重复发信。所以影子后端必须连独立数据库,或者应用层根据 X-Shadow-Request 头强制只读。
  2. 镜像不重试、失败静默。镜像请求失败(影子机挂了、超时)不会报错给客户端,也不会重试,你只能从生产 Nginx 的 error.log 里看到。所以要监控 upstream shadow_backend 的健康度。
  3. 会放大后端压力。全量镜像 = 后端请求量翻倍(虽然分在两个环境)。低配影子机会被打爆,务必用采样。
  4. 镜像请求体的内存开销。mirror_request_body on 时,Nginx 要缓冲请求体来发两次,大文件上传场景会吃内存。对上传接口建议 mirror_request_body off 或不镜像。
  5. 镜像的响应被丢弃,但连接要正常关闭。如果影子上游响应特别慢,会占用生产 Nginx 的 worker 连接资源,直到超时。给镜像的 upstream 配上较短的超时:proxy_read_timeout 5s;。
  6. 不能镜像 WebSocket、长连接。mirror 面向的是普通 HTTP 请求/响应,WebSocket、gRPC 流式这类不合适。

进阶:用镜像做「压测取样」

压测工具(ab、wrk)造出来的流量往往不真实——没有登录态、路径分布不对、参数单一。一个更真实的压测方案是:把生产流量镜像到影子机,在影子机上配一层 Nginx 把镜像请求「放大 N 倍」再打到目标。做法是在影子机的镜像 location 里用 mirror 多写几次(Nginx 从 1.13.4 起支持多次 mirror 指令)或者用 Lua 复制请求。这样你压测用的就是 100% 真实的请求序列、真实的 Cookie 和参数分布,比任何手工构造的脚本都准。

location = /mirror {
    internal;
    # 镜像请求本身再复制 3 份,模拟 3 倍流量
    mirror /mirror2;
    mirror /mirror2;
    proxy_pass http://target_for_stress$request_uri;
}

和「灰度发布」的区别,别混了

很多人把流量镜像和灰度发布搞混。区别很关键:

  • 灰度发布(canary):把一部分真实用户的请求导到新版本,用户能感知到(他们访问的就是新版本),目的是验证新版本对小部分真实用户是否 OK。
  • 流量镜像(mirror):把请求复制一份到影子环境,用户仍访问旧版本,完全无感,目的是观察新版本在真实流量下的表现,不影响任何人。

所以 mirror 适合「上线前的最后一道验证」,灰度适合「上线后的逐步放量」。两者可以配合:先用 mirror 验证新版不崩,再用 canary 逐步放量。

小结

ngx_http_mirror_module 是 Nginx 里被严重低估的一个模块:配置只有几行,却能让你用真实线上流量去验证新版本、新配置、新缓存策略,而且对用户零影响。用好它的关键就三点——用 map/split_clients 做采样避免打爆影子机、影子后端必须连独立数据库并靠 X-Shadow-Request 头走只读、以及给镜像 upstream 配短超时防止拖垮生产 worker。避开「镜像会真写库」这个最大陷阱之后,它就是一套零成本的线上预演与真实压测取样方案。

Last modification:October 4th, 2026 at 01:24 pm

Leave a Comment