灯下哥谭 灯下哥谭
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

灯下哥谭

灯还亮着
首页
关于
  • Hermes Agent 平台
  • Claude Code
  • OpenClaw
  • GPU 推理节点运维
  • DeepSeek Harness
  • MySQL 运维知识地图
  • Elasticsearch 运维知识地图
  • Redis 运维知识地图
  • TiDB 体系
  • DBA 常用 SQL 与命令
  • Nginx 运维知识地图
  • Prometheus 监控
  • Docker
  • Systemd
  • Iptables
  • Firewalld
  • Sshd
  • MySQL8 运维 SOP 手册
  • MySQL 实战 45 讲(读书笔记)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • MySQL

  • Redis

  • 高性能KV

  • TiDB

  • Elasticsearch

    • Elasticsearch 运维知识地图:从安装配置到排障加速恢复
    • Elasticsearch 集群安装:RPM 裸机生产部署 vs docker-compose 快速起集群
    • 给Elasticsearch集群添加用户密码
    • Elasticsearch 分片和副本:容量怎么规划,改分片数为什么这么麻烦
    • Elasticsearch集群节点磁盘使用分配不均解决办法
    • Elasticsearch 索引模板与映射:Composable 模板、mapping 与动态模板
    • Elasticsearch 分页查询三种方案:from/size、search_after、scroll 怎么选
    • Elasticsearch字符串搜索方式
    • Elasticsearch使用wildcard字段模糊匹配
    • Elasticsearch 数据迁移方案对比:esm vs Logstash
    • Nginx Mirror 模块实现三套ES写入网关
      • 1. mirror 是怎么工作的
      • 2. 适用场景与限制
      • 3. 配置详解
      • 4. 真实代价
      • 5. mirror 的请求体限制
        • 5.1 三个关键配置项
        • 5.2 排查线索
        • 5.3 上线节奏建议
      • 6. 怎么让失败不再无声
      • 7. keepalive 为什么必须配
      • 8. reuseport 的作用
      • 9. 资源评估
      • 10. 验证与排查
      • 11. 故障场景
      • 12. 替代方案对比
    • ES排障两件套:慢查询日志阈值配置 + tcpdump 抓包看真实请求
    • ES 集群恢复太慢?三个参数加速节点/分片恢复
    • Elasticsearch 常用 DSL 语句(速查表)
    • ES 集群 Yellow 复盘:1023 个副本永远分配不出去,问题不在磁盘
  • 数据管道

  • 其他数据库

  • 数据库
  • Elasticsearch
灯下哥谭
2022-03-17
目录

Nginx Mirror 模块实现三套ES写入网关

一份写入要同时进三套 Elasticsearch 集群(比如新老集群并行验证期互相比对、或日志同时进热集群与归档集群),又不想在每个业务代码里改双写逻辑——最轻量的方案是把多写下沉到接入层,用 Nginx 的 mirror 模块做请求复制。

以下记录完整配置与关键注意事项。

版本说明

本文写于 2022-03。
Nginx mirror 模块的配置语法未变,经 2026-07 复核仍适用;若目标 ES 集群已升级到 8.x 并开启安全认证,转发请求需额外携带认证信息。

# 1. mirror 是怎么工作的

Nginx 在处理主请求时,会同步复制一份完整的请求(含 headers 和 body)发往 internal location:

  1. 主请求 proxy_pass 到 es_2,ES 返回的响应直接给客户端
  2. 同时发起 mirror 请求到 /mirror → es_1,不等待响应就被丢弃
  3. 同时发起 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;
    }
}
1
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 配置
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 5.2 排查线索

如果表现是「小请求都正常、大 bulk 偶发失败」,排查顺序:

  1. 先查 Nginx 日志有没有 413(client_max_body_size 不够)
  2. 再查 client_body 相关配置
  3. 最后查 ES 侧的 http.max_content_length

顺序别反——很多问题是 Nginx 侧先拒掉的,ES 日志里什么都没有。

# 5.3 上线节奏建议

mirror 是加在已有网关上的,建议:

  1. 先只加一个 mirror 目标观察一段时间
  2. 看第 6 章那份 mirror 专属 access_log 的非 2xx 比例
  3. 确认带宽和内存都扛得住,再加第二个 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;
    # ... 其他配置
}
1
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
1
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
1
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"
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#架构设计#Elasticsearch#Nginx
上次更新: 9/8/2026

← Elasticsearch 数据迁移方案对比:esm vs Logstash ES排障两件套:慢查询日志阈值配置 + tcpdump 抓包看真实请求→

最近更新
01
DeepSeek Harness 实战 05|让两个编码 Agent 共用一份长期记忆 原创
09-09
02
DeepSeek Harness 实战 04|学习笔记:从「已知限制」里读出三处设计张力 原创
09-08
03
DeepSeek Harness 实战 03|学习笔记:读 12 篇 README 拿下 dsh 的五个核心心智模型 原创
09-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式