Nginx Mirror 模块实现三套ES写入网关
一份写入要同时进三套 Elasticsearch 集群(比如新老集群并行验证期互相比对、或日志同时进热集群与归档集群),又不想在每个业务代码里改双写逻辑——最轻量的方案是把多写下沉到接入层,用 Nginx 的 mirror 模块做请求复制。
以下记录完整配置与关键注意事项。
版本说明
本文写于 2022-03。
Nginx mirror 模块的配置语法未变,经 2026-07 复核仍适用;若目标 ES 集群已升级到 8.x 并开启安全认证,转发请求需额外携带认证信息。
# 1. mirror 是怎么工作的
Nginx 在处理主请求时,会同步复制一份完整的请求(含 headers 和 body)发往 internal location:
- 主请求
proxy_pass到es_2,ES 返回的响应直接给客户端 - 同时发起 mirror 请求到
/mirror→es_1,不等待响应就被丢弃 - 同时发起 mirror 请求到
/mirror2→es_3,同样不等待
internal 指令保证这些 location 不能被外部直接访问,只能由内部 mirror 机制调用。
关键细节:
proxy_pass http://es_1$request_uri里的$request_uri必须带上,否则原始路径和查询参数会丢失proxy_buffering off是为了不让镜像请求的响应体在 Nginx 侧堆积——mirror 的响应本来就会被丢弃,所以及时清掉
# 2. 适用场景与限制
⚠️ 关键语义:mirror 是「异步镜像、不等待响应、失败无声」
- 主请求流向
proxy_pass的目标(es_2),mirror 请求流向/mirror和/mirror2 - mirror 请求不阻塞主请求返回,失败也不会影响主请求的 200 响应
- 这意味着:mirror 目标挂了,客户端无感知,数据已不一致
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 双写热备(可接受秒级不一致) | ✅ | mirror 异步,适合做主从延迟可接受的场景 |
| 双写强一致(必须两边都成功) | ❌ | mirror 不等待、失败无声,无法保证强一致 |
| 数据迁移验证(旁路核对) | ✅ | mirror 请求可作为数据核对旁路 |
| 日志审计(留存原始请求) | ✅ | 不关注 mirror 是否成功,仅留存 |
# 3. 配置详解
upstream es_1 {
server 192.0.2.11:9200;
keepalive 640; # 最大保持活动连接数
keepalive_requests 15000; # 每个连接允许处理的最大请求数
keepalive_timeout 3600s; # 长连接保持的最大时间
}
upstream es_2 {
server 192.0.2.12:9200;
keepalive 640;
keepalive_requests 15000;
keepalive_timeout 3600s;
}
upstream es_3 {
server 192.0.2.13:9200;
keepalive 640;
keepalive_requests 15000;
keepalive_timeout 3600s;
}
server {
listen 8000 reuseport;
location / {
mirror /mirror; # 异步镜像到 es_1(不等待响应)
mirror /mirror2; # 异步镜像到 es_3(不等待响应)
proxy_pass http://es_2; # 主请求(同步等待)
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
tcp_nopush on;
tcp_nodelay on;
# 连接超时
proxy_connect_timeout 50s;
proxy_send_timeout 100s;
proxy_read_timeout 100s;
proxy_buffer_size 1280k;
proxy_buffers 4 2560k;
proxy_busy_buffers_size 2560k;
proxy_temp_file_write_size 5120k;
}
location /mirror {
internal;
proxy_buffering off;
proxy_pass http://es_1$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 50s;
proxy_send_timeout 100s;
proxy_read_timeout 100s;
proxy_buffer_size 1280k;
proxy_buffers 4 2560k;
proxy_busy_buffers_size 2560k;
proxy_temp_file_write_size 5120k;
}
location /mirror2 {
internal;
proxy_buffering off;
proxy_pass http://es_3$request_uri;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 50s;
proxy_send_timeout 100s;
proxy_read_timeout 100s;
proxy_buffer_size 1280k;
proxy_buffers 4 2560k;
proxy_busy_buffers_size 2560k;
proxy_temp_file_write_size 5120k;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 4. 真实代价
请求体(bulk 请求的 JSON body)会被完整复制 N 份,出口带宽和内存是成倍增长的。
按文中 proxy_buffers 4 2560k 计算:
- 单连接缓冲区 = 4 × 2560k = 10MB
- keepalive 640 连接 = 640 连长连接
- 理论最大内存 =
10MB × 640 × 3 upstreams = 19.2 GB
这是一个极端情况下的上限估算。实际占用取决于请求大小和连接使用率,但确实要比单写场景多预留 2-3 倍内存。
# 5. mirror 的请求体限制
mirror 需要完整读取并缓冲请求体才能复制给镜像 location。这意味着 proxy_request_buffering off(流式转发)与 mirror 不能并存——想省内存关缓冲,mirror 就拿不到 body。对 ES 的 bulk 写入这点很关键:bulk 请求体动辄几 MB 到几十 MB,全部要在 Nginx 侧落到内存或临时文件。
# 5.1 三个关键配置项
| 配置项 | 作用 | 建议值 |
|---|---|---|
client_max_body_size | 超过就返回 413 | 至少 50m,ES bulk 大批量时默认 1m 肯定不够 |
client_body_buffer_size | 超过就写临时文件 | 根据请求大小调整,太小会触发磁盘 IO |
client_body_temp_path | 临时文件位置 | 高并发大 body 时单独给盘,别跟系统盘抢 IO |
配置示例:
server {
listen 8000 reuseport;
# 请求体限制
client_max_body_size 50m;
client_body_buffer_size 16k;
client_body_temp_path /var/cache/nginx/client_body_temp;
location / {
# ... mirror 配置
}
}
2
3
4
5
6
7
8
9
10
11
12
# 5.2 排查线索
如果表现是「小请求都正常、大 bulk 偶发失败」,排查顺序:
- 先查 Nginx 日志有没有 413(client_max_body_size 不够)
- 再查
client_body相关配置 - 最后查 ES 侧的
http.max_content_length
顺序别反——很多问题是 Nginx 侧先拒掉的,ES 日志里什么都没有。
# 5.3 上线节奏建议
mirror 是加在已有网关上的,建议:
- 先只加一个 mirror 目标观察一段时间
- 看第 6 章那份 mirror 专属 access_log 的非 2xx 比例
- 确认带宽和内存都扛得住,再加第二个 mirror
不要一次全开,出问题不好定位是哪个环节。
# 6. 怎么让失败不再无声
mirror「失败无声」是设计特性,但不是不可观测。给 /mirror 与 /mirror2 配独立的 access_log 格式:
log_format mirror_log '$remote_addr - $remote_user [$time_local] '
'"$request" $status $upstream_status $body_bytes_sent '
'"$http_referer" "ups=$upstream_addr" '
'"rt=$upstream_response_time" "$http_user_agent"';
location /mirror {
internal;
access_log /var/log/nginx/mirror_es1.log mirror_log;
# ... 其他配置
}
location /mirror2 {
internal;
access_log /var/log/nginx/mirror_es2.log mirror_log;
# ... 其他配置
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
基于这份日志出告警:
- 统计
$upstream_status非 2xx 的比例,持续 > 1% 时告警 - 统计
$upstream_response_time延迟,持续 > 5s 时告警
这样「异步」不代表「不可观测」,mirror 链路的健康状态可以独立监控。
兜底校验:定期跑三套集群的 _count 比对,把它变成定时任务而不是手工动作:
#!/bin/bash
C1=$(curl -s es_1:9200/myindex/_count | jq -r '.count')
C2=$(curl -s es_2:9200/myindex/_count | jq -r '.count')
C3=$(curl -s es_3:9200/myindex/_count | jq -r '.count')
if [ "$C1" -ne "$C2" ] || [ "$C2" -ne "$C3" ]; then
echo "ALERT: count mismatch es_1=$C1 es_2=$C2 es_3=$C3"
# 触发告警
fi
2
3
4
5
6
7
8
9
# 7. keepalive 为什么必须配
ES 的 bulk 写入是高频短请求,不复用连接的话每次都要 TCP 握手,CPU 和延迟都有明显损耗。
proxy_http_version 1.1 + proxy_set_header Connection "" 两行是 upstream keepalive 生效的前提:
- HTTP 1.0 默认不保持连接,必须显式声明 1.1
Connection: close会强制断开,所以用空字符串替代
这是最常见的配置遗漏——少了一行,keepalive 就不成立,连接仍走短连接模式。
# 8. reuseport 的作用
listen 8000 reuseport 让多个 worker 进程各自持有独立的监听套接字,由内核做连接分发,而非所有 worker 抢同一个锁。这能:
- 缓解高并发下的惊群问题
- 减少单个 worker 的负载不均
前提是 Linux 内核 3.9+ 且 Nginx 1.9.1+。
# 9. 资源评估
proxy_buffers 4 2560k = 10MB per connection,keepalive 640 = 640 connections
- 理论最大内存占用:
10MB × 640 × 3 (upstreams) = 19.2 GB - 实际占用取决于连接使用率,需按业务峰值评估
# 10. 验证与排查
# 1. 确认配置语法正确
nginx -t
# 2. 检查 mirror 是否生效(观察 access_log 中 mirror 标签)
tail -f /var/log/nginx/access.log | grep mirror
# 3. 模拟 mirror 目标故障(停掉 es_1),验证主请求仍返回 200
curl -XPOST http://localhost:8000/myindex/_doc -H 'Content-Type: application/json' -d '{"test":1}'
# 预期:HTTP 200,但 es_1 未收到数据
# 4. 对比各集群文档数校验一致性
GET es_1:9200/myindex/_count
GET es_2:9200/myindex/_count
GET es_3:9200/myindex/_count
2
3
4
5
6
7
8
9
10
11
12
13
14
# 11. 故障场景
| 症状 | 原因 | 排查 |
|---|---|---|
| 主请求返回 200 但数据丢失 | mirror 目标挂了 | 检查 es_1/es_3 是否可达 |
| Nginx 内存暴涨 | keepalive 连接数 × buffer 过大 | 调低 keepalive 或 buffer size |
| 主请求超时 | proxy_pass 目标 es_2 异常 | 检查 es_2 健康状态 |
# 12. 替代方案对比
| 方案 | 一致性 | 性能开销 | 适用场景 |
|---|---|---|---|
| mirror | 最终一致 | 低 | 日志双写、旁路核对 |
| 业务层双写 | 可配置 | 中 | 需确认两边成功才返回 |
| Kafka 双消费 | 最终一致 | 高 | 海量数据、需削峰填谷 |
| ES CCR | 近实时 | 高 | 跨机房热备(企业版功能) |
Agent 可直接解析的元数据块
{
"_meta": {
"version": "1.0",
"article_id": "nginx-mirror-es-multiwrite",
"scope": "Nginx 1.9+ / ES 6.x+",
"last_verified": "2026-07"
},
"quick_start": [
"配置 upstream 与 keepalive",
"location / 里加 mirror /mirror + proxy_pass 主目标",
"location /mirror { internal; proxy_pass http://cluster$request_uri; }",
"nginx -t 检查语法后 reload"
],
"safety_rules": [
"mirror 失败无声,必须配独立 access_log 监控",
"定期跑 _count 比对做兜底校验",
"不复用 connection 时检查 proxy_http_version 和 Connection header",
"评估内存:buffer × keepalive × upstream 数"
],
"verification": [
"curl 模拟写入后对比三集群 _count",
"tail -f mirror_es1.log 检查 upstream_status",
"停掉 mirror 目标验证主请求仍返回 200"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25