nginx

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > nginx > Nginx 接口复制

Nginx中mirror模块接口复制实战

作者:极客无极

本文深入解析Nginx内置mirror模块的接口复制能力,详细讲解nginx mirror模块的配置方法、工作原理和避坑指南,感兴趣的可以了解一下

有一次线上系统要验证一个新服务的逻辑是否和现有服务完全一致,可又不能把真实用户的请求直接切过去。按传统做法,得在业务代码里埋点、写回放脚本、造测试数据,一套流程折腾下来,上线窗口早过了。后来我改用 Nginx 的接口复制能力,只改了几行配置,就把线上真实请求复制了一份到新服务,客户端完全无感。所谓接口复制,简单说就是 Nginx 在把请求转发给原始后端的同时,把同一份请求原样复制给另一个后端,两个后端都会被调用,但客户端拿到的永远是原后端的响应。这听起来像是“反向代理 + 负载均衡”的花活,其实是 Nginx 一个低调但非常实用的内置模块:mirror。

这篇文章适合谁?如果你正在做新老系统逻辑对比、影子流量压测、第三方联调数据采集,或者单纯想把线上接口请求“抄送”一份给另一个服务,同时又不想动业务代码,那 Nginx 的接口复制方案值得完整看一遍。我会从需求场景、工作原理、可落地配置、上线前要避开的坑、进阶玩法五个部分展开,全程按实际项目里的操作思路来讲。

1. 接口复制到底复制的是什么:先理清需求场景再说配置

1.1 复制的是请求,不是响应

接口复制最核心的语义是“一份请求进来,多条链路同时收到”。客户端发送请求到 Nginx,Nginx 照常将请求转发给主后端,同时复制一份几乎完全相同的请求给复制目标后端。复制目标返回的响应,Nginx 直接丢弃;主后端返回的响应,则正常回到客户端。

这个过程可以类比开会:主会议室里的讨论正常进行,另一个会议室通过直播同步收看,但直播间的反馈不会影响主会议的结论。Nginx 在这里扮演的就是那个“直播分发器”。它复制的是“请求”这份内容,不是处理结果。

很多人第一次听这个概念时会问:那这不就是反向代理吗?还真不是。反向代理是把请求转给一台后端,每份请求只被处理一次;接口复制是同一份请求被处理两次或多次,只是客户端只知道第一次处理的结果。搞清楚这个区别,后面的配置逻辑就顺了。

1.2 哪些场景真正需要接口复制

这些场景有一个共同前提:主链路必须绝对稳定,复制过程不能影响线上业务。Nginx 的 mirror 模块在设计上就奔着这个目标去,所以天然适合。

1.3 不是只有 Nginx 能做到:方案对比

接口复制的实现路径不止一条。按我的经验,常见的方案有四种,各有各的适用边界。

方案改造成本侵入性适用层级典型问题
Nginx mirror 模块低,只改配置无侵入,不动业务代码接入层无法动态控制复制比例,需要搭配其他手段
应用内拦截器/过滤器高,每个服务都要改侵入性强应用层代码耦合,多语言项目维护成本高
API 网关插件(如 APISIX、Kong)中,需要在网关上写插件依赖网关能力接入层如果没用网关,等于为复制功能引入一套系统
交换机端口镜像 / 抓包回放低,不碰业务无侵入网络层只能拿到流量,无法精确对应到某个接口语义

我当时选 Nginx mirror,核心原因只有两个:一是线上本来就有 Nginx 接入层,不需要新增任何组件;二是配置都是声明式的,出问题可以秒回滚。你如果是运维或者全栈工程师,应该能理解“能靠配置解决就别写代码”的价值。

2. mirror 模块的工作原理:理解子请求机制才算真正会用

2.1 版本和模块检查

在动手写配置前,先确认你的 Nginx 支持 mirror 模块。这个模块是开源版 Nginx 1.13.4 开始内置的,官方编译参数叫 --with-http_mirror_module 。很多发行版默认已经带上,但有些精简编译的版本未必。

检查命令很简单:

nginx -V 2>&1 | grep http_mirror_module

如果输出里有 --with-http_mirror_module ,说明模块可用。如果什么也没输出,就需要重新编译 Nginx,或者换用带这个模块的发行版安装包。千万不要跳过这一步,我曾经在客户机器上配好 mirror 后执行 nginx -t 直接报错,模块压根没编译进去。

2.2 两条核心指令:mirror 和 mirror_request_body

mirror 模块的指令非常少,生产环境主要用两个:

这里有个容易误解的地方: mirror /mirror; 里的 /mirror 不是复制目标的 upstream 地址,而是一个内部 location。Nginx 会以子请求的方式去访问这个 location,具体的转发逻辑(比如 proxy_pass 到哪台机器)由这个 location 自己决定。

2.3 子请求到底怎么产生和结束

搞懂子请求机制,才算真正会用 mirror。整个流程是这样的:

  1. 客户端请求进入 location /api/ ,Nginx 开始正常的反向代理流程。
  2. 在转发主请求的同时,Nginx 为每一条 mirror 指令创建一个子请求(subrequest)。
  3. 子请求访问 mirror 指定的内部 URI,进入对应的内部 location,执行该 location 下的 proxy_pass 、 proxy_set_header 等指令。
  4. 子请求的响应不会返回给客户端,直接丢弃。

关键点在于第 4 步。Nginx 不需要等子请求处理完再回复客户端,所以从客户端视角看,复制目标是快是慢、是成功是失败,都不影响主请求的响应内容。

但这里我必须提醒一个容易踩的误区: mirror 不等于异步消息队列 。子请求依然运行在 Nginx worker 进程里,会占用当前 worker 的连接、内存和 CPU。如果复制目标响应非常慢,虽然客户端看不到影响,但 Nginx 的 worker 并发能力会被大量占住,最终可能拖累整个 Nginx 的性能。这个坑我会在第 4 章细讲。

3. 从零写一份可落地的接口复制配置

3.1 最小可用配置:upstream + internal 镜像 location

先看一份最简配置,理解了它,再往复杂方向加东西。

upstream api_origin {
    server 10.0.0.10:8080;
}
upstream api_copy {
    server 10.0.0.20:8080;
}
server {
    listen 80;
    server_name example.com;
    location /api/ {
        # 关键:声明镜像请求要访问的内部 URI
        mirror /mirror;
        proxy_pass http://api_origin;
        proxy_set_header Host $host;
    }
    location = /mirror {
        # 必须 internal,外部请求不能直接访问
        internal;
        proxy_pass http://api_copy;
        proxy_set_header Host $host;
    }
}

这份配置要做的事很简单:所有 /api/ 下的请求,主转发到 api_origin ,同时复制一份到 api_copy 。

三个细节必须记住:

3.2 默认就会复制 Body,但大 Body 要小心

mirror_request_body 默认是 on,也就是说 POST、PUT 这类带请求体的接口,默认就会把 body 完整复制到镜像目标,不需要你额外写指令。你只需要在业务上确认“复制目标真的需要 body”,如果不需要,把它关掉可以省不少资源:

location /api/ {
    mirror /mirror;
    mirror_request_body off;
    proxy_pass http://api_origin;
}

为什么要单独提这点?因为我见过一个事故:某团队复制一个文件上传接口,默认 on 的情况下,Nginx 需要把较大的 body 先读入内存,超出 client_body_buffer_size 后落到临时文件,本来主接口转发不受影响,但大量上传请求触发了临时目录的磁盘 IO,导致整体响应变慢。复制这种大请求体之前,先想想镜像目标到底需不需要完整 body。

如果确实需要复制 body,建议根据请求体大小评估一下 client_body_buffer_size 和临时目录的磁盘水位,避免无谓的磁盘压力。

3.3 只复制指定接口:靠 location 拆解,而不是 if

实际项目中,通常不会把整个服务所有接口全部复制,往往只需要复制某几个核心接口。

mirror 指令是静态配置,不能放在 if 块里做动态开关。下面这种写法是错的, mirror 指令在 Nginx 配置中不允许出现在 if 语句里:

# 错误示范,仅为说明,不要这样写
location /api/ {
    if ($request_uri ~ ^/api/order/) {
        mirror /mirror;    # 不会生效,甚至可能报错
    }
    proxy_pass http://api_origin;
}

正确的做法是拆 location。把需要复制的接口单独用 location 匹配,然后在那个 location 里声明 mirror:

# 需要复制的订单接口
location /api/order/ {
    mirror /mirror;
    proxy_pass http://api_origin;
}

# 不需要复制的用户接口
location /api/user/ {
    proxy_pass http://api_origin;
}

如果接口数量很多,可以优先用前缀匹配把接口分组,而不是一条条精确匹配,配置会干净很多。

3.4 想按比例复制怎么办:不要硬造,交给下游或 OpenResty

mirror 指令本身不支持“复制 10% 的流量”这种比例控制。纯 Nginx 配置层面,最干净的做法是:把所有流量都镜像到一个内部采集服务,由采集服务按比例抽样或者过滤,决定哪些真正转发到处理目标。Nginx 只负责“全量复制”,下游负责“按需取用”,职责清晰,也不会把配置搞得难以维护。

如果你确实希望流量入口直接做比例控制,那就不能用标准 Nginx 了,考虑 OpenResty。在 mirror location 里用 Lua 在 access 阶段随机放行,大约只有 1% 的子请求会继续执行 proxy_pass :

location = /mirror {
    internal;
    access_by_lua_block {
        -- 随机放行 1% 的镜像子请求
        if math.random() > 0.01 then
            return ngx.exit(200)
        end
    }
    proxy_pass http://api_copy;
}

这个方案的前提是团队里有 OpenResty 运维能力。如果只是为比例复制引入 Lua 依赖,我通常不建议,除非流量入口本来就跑在 OpenResty 上。

3.5 一份相对完整的配置示例

把上面的要点合到一起,我给出一份更接近生产环境的配置:

upstream api_origin {
    server 10.0.0.10:8080 max_fails=3 fail_timeout=10s;
}

upstream api_copy {
    server 10.0.0.20:8080 max_fails=0;
}

server {
    listen 80;
    server_name example.com;

    location /api/order/ {
        mirror /mirror;
        mirror_request_body on;
        proxy_pass http://api_origin;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /api/user/ {
        proxy_pass http://api_origin;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location = /mirror {
        internal;
        proxy_connect_timeout 1s;
        proxy_read_timeout 2s;
        proxy_send_timeout 2s;
        proxy_pass http://api_copy;
        proxy_set_header Host $host;
        proxy_set_header X-Mirror-Request "true";
        proxy_set_header X-Original-Request-Id $http_x_request_id;
    }
}

注意几点:镜像 location 的 proxy_connect_timeout 和 proxy_read_timeout 我刻意设得很短,避免镜像目标拖住 Nginx 资源; X-Mirror-Request: true 是给镜像目标识别流量用的,第 4 章会细说。

配置写完后,务必先执行 nginx -t 检查,再 nginx -s reload 平滑加载。

4. 上线前必须检查的坑:Header、超时、递归、监控误报

4.1 递归镜像:internal 的底线不能丢

前面提过, internal; 是防止递归镜像的关键。这类问题在第一次配置 mirror 时特别容易犯,因为默认模板里很容易漏写这一行。

一旦 mirror 的 location 没有 internal,外部用户可以直接访问 /mirror 。这个请求本身也是一个普通请求,会再次触发 mirror 逻辑,于是它又创建一个子请求访问 /mirror ,再触发……请求数量和嵌套层数会指数级增长,Nginx 很快就会被拖死。这不是理论推演,是线上真实发生过的故障。

所以我在每次配置 mirror 时都会强制要求三条原则:Location 必须 internal; ;必须是精确匹配 = ;Nginx reload 前必须 nginx -t 验证。

4.2 复制目标慢:不会拖垮响应,但会拖垮 worker

mirror 子请求的响应内容虽然被丢弃,但子请求从建立连接到发送请求、等待响应、关闭连接的整个过程,都发生在 Nginx worker 进程内部。如果复制目标处理很慢,子请求会占住这个 worker 的连接,高并发场景下,大量 worker 都被慢请求占住,整体服务能力就下来了。

我遇到过一次很典型的故障:镜像目标是一台配置较低的测试服务器,接口本身就要 3 秒多才能返回,导致 Nginx 的 worker 大量堆积。线上用户响应固然没有变慢,但 Nginx 的并发连接数快速上涨,最终影响了主链路。

对策就是给镜像 location 设置短超时。我通常用:

location = /mirror {
    internal;
    proxy_connect_timeout 1s;
    proxy_read_timeout 2s;
    proxy_send_timeout 2s;
    proxy_pass http://api_copy;
}

镜像目标本身要有降级预案,哪怕它完全挂了,主接口也不能受影响。超时时间宁可设短一点,也不要让它拖住线上。

4.3 Header 与链路追踪:镜像流量要能认出来

镜像请求默认会携带原始请求的大部分 header,但这里有两个问题需要处理。

第一,镜像目标需要能区分“真实用户流量”和“镜像流量”。否则新服务的监控、告警、日志会混在一起,你根本分不清某个错误是真实用户触发的,还是复制流量造成的。通用的做法是在 mirror location 里通过 proxy_set_header 加一个自定义头:

proxy_set_header X-Mirror-Request "true";

镜像目标的 WAF、监控系统、日志采集,都可以根据这个 header 把流量单独归类。

第二,链路追踪的 request id 问题。如果客户端和主链路已经通过 X-Request-Id 或 APM 的 trace header 做全链路追踪,镜像请求会携带同一个请求标识进入复制目标,这其实是有用的,可以让你把主链路和镜像链路对应起来。但也有副作用:如果镜像目标也接入 APM,它会认为每个请求都出现了两条链路,trace 数据量翻倍,还会出现大量的“同 ID 不同耗时”的困惑数据。

我常用的折中方案是:在镜像 location 里把原始 X-Request-Id 存成另一个 header,再重新生成一个带 -mirror 后缀的标识给镜像目标,这样两边都能独立追踪,又能通过 X-Original-Request-Id 关联回主请求:

location = /mirror {
    internal;
    proxy_set_header X-Mirror-Request "true";
    proxy_set_header X-Request-Id "${http_x_request_id}-mirror";
    proxy_set_header X-Original-Request-Id $http_x_request_id;
    proxy_pass http://api_copy;
}

$http_x_request_id 这个变量在客户端没有传对应 header 时是空字符串,所以如果你们已经固定了下游的请求 ID 生成规范,这里要按实际链路里的 header 名来做适配。

4.4 日志与监控:别让复制流量污染线上指标

默认情况下,Nginx 的 access_log 是不会记录子请求的,这个由 log_subrequest 指令控制,默认 off。换句话说,你在 Nginx 本机日志里看不到镜像请求的痕迹,这不是 mirror 没生效,而是它压根没往主日志里写。

排查镜像流量时,不要只盯着 Nginx 日志看,重点应该去镜像目标服务器的 access_log 里确认有没有收到请求。有些团队为了审计方便会打开 log_subrequest on ,这样原始请求和镜像请求会同时出现在 Nginx access_log 里。开启后日志量会翻倍,建议只在排查期间临时开,排查完就关掉。

复制目标的新服务上线后,它的监控指标默认是把所有请求都算成真实流量的。如果复制流量占了很大比例,这个服务的 QPS、错误率、耗时都会被镜像流量影响,很容易把告警打爆。做监控大盘时一定要按 X-Mirror-Request header 做一层过滤,把真实流量和镜像流量分开看,至少也要给它们打上不同标签。

5. 接口复制的进阶玩法与边界

5.1 复制到多个目标:一条 mirror 不够就多条

mirror 指令可以在同一个 location 里写多条,Nginx 会为每条配置分别创建子请求。等于一份流量进来,可以同时复制到两个甚至更多目标。

location /api/order/ {
    mirror /mirror-to-test;
    mirror /mirror-to-analysis;
    proxy_pass http://api_origin;
}

我实际项目里用这个能力做过“双复制”:一份流量打到压测集群做性能验证,另一份打到数据采集服务做日志分析,两个目标互不干扰。每条 mirror 都有自己的内部 location,可以独立设置超时、header、连接池。

5.2 mirror 不适用的场景:别拿着锤子看什么都是钉子

mirror 模块本质上是 HTTP 语义的子请求复制,下面这些场景它帮不上忙:

另外要明确一点:mirror 只负责“把请求复制到别的服务”,不改写任何内容。如果你想对响应内容做替换或注入,那是 sub_filter 和 Lua 模块的事,跟 mirror 是两个方向。

5.3 接口复制与这些“形似”的配置怎么区分

很多人在群里问“接口复制、反向代理、负载均衡、灰度发布有什么区别”,我列个表直接说清楚:

配置类型请求被处理几次客户端感知典型用途
反向代理1 次只和一个后端交互隐藏后端,统一入口
负载均衡1 次,分发到不同后端只和一个后端交互分摊压力,横向扩容
灰度发布1 次,按权重选择版本只会命中新版本或旧版本新版本小流量验证
接口复制多次,同一请求复制到多后端只感知主后端真实流量对比、影子压测

“灰度发布”和“接口复制”经常被放一起讨论,但语义完全不同:灰度是让一部分用户走新版本,另一部分走老版本,每个用户只被处理一次;接口复制是同一个用户的同一个请求,被主后端处理的同时,又被复制目标处理一次。前者是分流,后者是放大。

5.4 我使用接口复制的一些经验

最后分享几条实战经验,都是踩过坑换回来的。

第一次配置 mirror 时,建议先在测试环境用 curl 做验证,不要直接上线上接口。验证方式很简单:主后端和复制目标各看 access_log,分别确认同一个请求是否都记录了;再检查一次 Header 里的 X-Mirror-Request 是否带上;最后看客户端返回的是不是只有主后端的响应。这几个点确认完,基本就稳了。

线上逐步放开的时候,先从只读接口开始,比如查询类的 GET 接口。因为复制目标通常会执行同样的逻辑,如果目标是写库的服务,GET 也可能触发写入或缓存更新。跑几天确认对线上没有影响后,再考虑复制 POST/PUT 这类写接口。写接口复制出去,复制目标会真实写库,如果复制目标没有独立的隔离环境,一不小心就会污染数据。

排查镜像请求有没有到达复制目标,最快的办法是在复制目标服务器上抓包:

tcpdump -i eth0 -A -s 0 'tcp port 8080' | grep --line-buffered 'X-Mirror-Request'

能看到镜像请求头,说明 Nginx 到复制目标的链路是通的;看不到,问题大概率出在 Nginx 配置层面。

就我自己的体会,Nginx 接口复制最值钱的地方,不是省掉了写代码的时间,而是它让“验证”这件原本只能靠猜的事情变成了可对照、可重放、可审计的真实流量实验。如果你也有一个不敢轻易切流量的新系统,建议先从复制一个只读接口开始,跑上一周对比数据,你会发现很多测试环境永远暴露不出来的问题。

到此这篇关于Nginx中mirror模块接口复制实战的文章就介绍到这了,更多相关Nginx 接口复制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文