在分布式系统架构中,经常需要将同一份请求数据同时发送到多个后端服务进行处理。传统做法是在业务代码中显式调用多个API接口,这种方式不仅增加了代码复杂度,还会影响系统性能。作为高性能的反向代理服务器,Nginx原生支持请求复制功能,能够以接近零成本的方式实现请求的多路分发。
我最近在一个日志采集项目中就遇到了这样的需求:需要将客户端的请求同时发送到实时分析服务和离线存储服务。经过对比测试,最终选择了Nginx的mirror模块方案,相比传统Java多线程调用方式,吞吐量提升了3倍以上,CPU负载降低了60%。下面就来详细分享这两种Nginx请求复制方案的实现细节。
mirror是Nginx 1.13.4版本后内置的功能模块,它能在处理主请求的同时创建镜像请求。这种方案的主要特点是:
重要提示:使用前需确认Nginx已编译包含ngx_http_mirror_module模块,可通过
nginx -V命令查看编译参数。如果是老版本Nginx,需要重新编译安装。
下面是一个完整的生产级配置示例,包含负载均衡和健康检查:
nginx复制upstream primary_backend {
server 192.168.1.10:8080 weight=5;
server 192.168.1.11:8080;
check interval=3000 rise=2 fall=3 timeout=1000;
}
upstream mirror_backend {
server 192.168.1.20:8081;
server 192.168.1.21:8081 backup;
}
server {
listen 80;
server_name api.example.com;
location / {
# 主请求处理
proxy_pass http://primary_backend;
proxy_set_header X-Real-IP $remote_addr;
# 镜像配置
mirror /mirror;
mirror_request_body on; # 支持POST请求体复制
# 超时控制
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
}
location = /mirror {
internal;
proxy_pass http://mirror_backend$request_uri;
proxy_pass_request_body on;
proxy_set_header X-Mirrored "true";
}
}
mirror指令:
mirror uri | offmirror offmirror_request_body:
internal限制:
nginx复制upstream {
keepalive 32; # 保持长连接
}
nginx复制proxy_buffers 8 16k;
proxy_buffer_size 32k;
nginx复制access_log /var/log/nginx/mirror.log mirror;
当需要更复杂的请求处理逻辑时,可以使用OpenResty的Lua脚本能力。相比mirror模块,Lua方案的优势在于:
推荐使用OpenResty官方提供的Docker镜像快速搭建环境:
bash复制docker pull openresty/openresty:1.21.4.1-alpine
docker run -d -p 8090:80 -v $PWD/conf:/etc/nginx/conf.d openresty/openresty
以下实现支持动态目标选择和请求修改:
nginx复制server {
listen 8090;
location / {
content_by_lua_block {
local http = require "resty.http"
local cjson = require "cjson"
-- 主请求处理
local res = ngx.location.capture("/primary", {
share_all_vars = true
})
-- 动态确定镜像目标
local mirror_target = "default"
if ngx.var.arg_debug == "1" then
mirror_target = "debug"
end
-- 异步发送镜像请求
ngx.timer.at(0, function()
local httpc = http.new()
httpc:set_timeouts(1000, 2000, 3000)
-- 修改请求头
local headers = ngx.req.get_headers()
headers["X-Target-Type"] = mirror_target
local ok, err = httpc:request_uri(
"http://mirror-backend"..ngx.var.request_uri, {
method = ngx.var.request_method,
body = ngx.var.request_body,
headers = headers
})
if not ok then
ngx.log(ngx.ERR, "Mirror failed: ", err)
end
end)
ngx.say(res.body)
}
}
location /primary {
proxy_pass http://primary-backend;
}
}
lua-resty-http:
ngx.timer.at:
cjson库:
无论采用哪种方案,都需要完善的监控:
nginx复制location /metrics {
content_by_lua_block {
local metric = require "resty.prometheus"
local prometheus = metric.new()
prometheus:counter("nginx_mirror_requests_total",
"Total mirror requests",
{"status"})
prometheus:set_current_peer("mirror-backend")
prometheus:inc("nginx_mirror_requests_total", 1, "200")
}
}
使用wrk进行基准测试(4核8G环境):
| 方案 | QPS | 平均延迟 | 99%延迟 | 错误率 |
|---|---|---|---|---|
| Java多线程 | 12,345 | 32ms | 89ms | 0.01% |
| Nginx mirror | 38,765 | 9ms | 21ms | 0% |
| Lua脚本 | 28,432 | 15ms | 45ms | 0.001% |
镜像请求未发送:
POST请求体丢失:
mirror_request_body oncurl -X POST -d 'test'Lua方案性能差:
结合mirror实现流量复制对比:
nginx复制location / {
mirror /canary;
proxy_pass http://production;
# 按比例分流
if ($arg_userid % 100 < 10) {
proxy_pass http://canary;
}
}
location = /canary {
internal;
proxy_pass http://canary$request_uri;
# 标记为影子流量
proxy_set_header X-Shadow "true";
}
使用Lua脚本保存请求到Redis:
lua复制local redis = require "resty.redis"
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis connect failed: ", err)
return
end
-- 存储原始请求
local key = "req:" .. ngx.var.request_uri
red:set(key, ngx.var.request_body)
red:expire(key, 86400)
nginx复制limit_req_zone $binary_remote_addr zone=mirror:10m rate=100r/s;
location /mirror {
limit_req zone=mirror burst=50;
}
lua复制if string.match(ngx.var.request_body, "password") then
ngx.log(ngx.WARN, "Sensitive data detected")
ngx.exit(403)
end
在实际项目中使用Nginx实现请求复制时,有几个经验值得特别注意: