在日常运维中,遇到这样一个“诡异”现象:

主库的 information_schema.processlist 中显示 Binlog Dump 连接来自 10.1.0.1,但从库配置的 Master_Host 却是 192.168.100.66

一、问题现象复现

场景 A:同主机容器化主从

  • 从库容器 IP10.1.0.83
  • 从库配置
    MASTER_HOST = '192.168.100.66', MASTER_PORT = 33306
    
  • 主库观察结果
    SELECT * FROM information_schema.processlist 
    WHERE state LIKE '%binlog%'\G
    -- HOST: 10.1.0.1:44797
    

场景 B:跨主机物理复制

  • 从库所在主机 IP192.168.100.22
  • 从库配置
    MASTER_HOST = '192.168.100.21', MASTER_PORT = 13307
    
  • 主库观察结果
    -- HOST: 192.168.100.201:59763 (真实出口 IP)
    

为何同一套 MySQL 复制逻辑,在不同部署模式下,主库看到的 Slave IP 完全不同?

二、根本原因:Linux 网络命名空间与 Docker MASQUERADE

2.1 Docker 自定义桥接网络的工作机制

当使用 docker network create 创建自定义 bridge 网络(如 local_network_dev)时:

  • Docker 会在宿主机创建一个虚拟网桥(如 br-982b127ffbf6
  • 分配子网(如 10.1.0.0/24),网关为 10.1.0.1
  • 容器通过 veth pair 接入该网桥,获得 10.1.0.x IP
# 查看接口
ip -br a
# br-982b127ffbf6  UP   10.1.0.1/24

2.2 出站流量的 SNAT(源地址转换)

关键点在于:当容器访问非 Docker 网络内的地址(如宿主机的 192.168.100.66)时,Docker 会自动添加 iptables MASQUERADE 规则

iptables -t nat -L POSTROUTING
# 输出包含:
# MASQUERADE  all  --  10.1.0.0/24    anywhere

该规则的作用是:

将所有源自 10.1.0.0/24 且目标不在该网段的流量,源 IP 替换为网桥接口 IP(即 10.1.0.1

因此,主库收到的 TCP 连接自然显示为 10.1.0.1:port

2.3 SNAT(源地址转换)详解

SNAT(Source Network Address Translation,源网络地址转换)是 NAT(Network Address Translation,网络地址转换) 的一种形式,用于在数据包离开网络时修改其源 IP 地址

1. SNAT 的核心作用

让内网主机能够访问外网,同时对外“隐藏”真实 IP。

典型场景:

  • 家庭路由器:多台手机/电脑(内网 IP 如 192.168.1.x)共享一个公网 IP 上网。
  • Docker 容器:容器使用私有 IP(如 10.1.0.83),但能访问宿主机或互联网。
  • 企业 NAT 网关:内部服务器通过统一出口 IP 访问外部服务。

2. SNAT 工作原理(以 Linux + iptables 为例)

当一个数据包从内网发出时,Linux 内核在 POSTROUTING 链(路由之后、发送之前)执行 SNAT:

原始包:
  源 IP: 10.1.0.83     ← 容器 IP
  目标 IP: 192.168.100.66

经过 SNAT 后:
  源 IP: 10.1.0.1      ← 改为网桥 IP(或公网出口 IP)
  目标 IP: 192.168.100.66

返回包时,内核会自动将目标 IP 反向转换回 10.1.0.83(基于连接跟踪表 conntrack)。

🔧 在 Linux 中,常用命令:

# 查看 SNAT 规则
iptables -t nat -L POSTROUTING

# 典型规则
MASQUERADE all -- 10.1.0.0/24 anywhere

MASQUERADE 是 SNAT 的一种动态形式(自动使用出口网卡 IP)。

3. SNAT vs DNAT 对比

类型全称修改字段典型用途
SNATSource NAT源 IP内网 → 外网(如容器访问互联网)
DNATDestination NAT目标 IP外网 → 内网(如访问 宿主机:8080 转发到容器:80)

💡 Docker 同时使用两者:

  • -p 8080:80 → 创建 DNAT(外部访问宿主机 8080 → 容器 80)
  • 容器访问外网 → 触发 SNAT/MASQUERADE

4. 当前 MySQL 场景

在当前环境中:

  • 从库容器 IP10.1.0.83
  • 它连接192.168.100.66:33306(宿主机上的主库)
  • 因为目标 IP 不属于 10.1.0.0/24 网段,触发 SNAT
  • 源 IP 被改为 Docker 网桥 IP 10.1.0.1
  • 所以主库看到的连接来自 10.1.0.1

这就是为什么 processlist.HOST = '10.1.0.1:xxxx'

5. SNAT 的优缺点

优点

  • 节省公网 IP(多个内网设备共享一个出口 IP)
  • 隐藏内部网络结构,提升安全性
  • 无需修改应用代码即可实现网络互通

缺点

  • 主机无法直接看到真实客户端 IP(需额外日志或代理传递)
  • 增加内核处理开销(需维护连接跟踪表)
  • 调试网络问题时更复杂(IP 被“伪装”)

总结

SNAT 就是“出门换马甲”——内网设备用一个公共 IP 对外通信,回来时系统再把“马甲”换回真身。

三、数据流对比:同主机 vs 跨主机

3.1 同主机容器 → 宿主机服务(触发 SNAT)

在这里插入图片描述

3.2 跨主机直接连接(无 SNAT)

在这里插入图片描述

四、关键验证命令汇总

目标命令说明
查看容器 IPdocker inspect <name> | grep IPAddress确认容器在 10.1.0.0/24
查看端口映射ss -tulnp | grep <port>确认主库通过 docker-proxy 暴露
查看 NAT 规则iptables -t nat -L POSTROUTING确认存在 MASQUERADE 10.1.0.0/24
查看复制状态SHOW SLAVE STATUS\G确认 IO/SQL Running: Yes
查看主库连接SELECT * FROM processlist WHERE ...观察 HOST 字段

五、影响与最佳实践

5.1 是否影响复制功能?

。只要:

  • 复制账号授权正确
  • 网络连通
  • binlog 格式兼容

复制即可正常工作。

5.2 复制账号授权建议

部署模式推荐授权方式
同主机容器GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.1.0.1';
跨主机GRANT REPLICATION SLAVE ON *.* TO 'repl'@'192.168.100.%';
混合环境同时授权多个 host,或使用 'repl'@'%'(需评估安全风险)

若未授权 10.1.0.1,将出现 Access denied for user 'repl'@'10.1.0.1' 错误。

5.3 架构优化建议(可选)

若希望主库看到真实容器 IP,可采用:

  • 方案 1:主从均加入同一 Docker 网络,从库直连主库容器 IP(如 10.1.0.86
  • 方案 2:使用 host 网络模式运行从库容器(--network host
  • 不推荐:修改 iptables 禁用 MASQUERADE(破坏 Docker 网络模型)

六、面试题

Q1:为什么 MySQL 主库看到的 Slave IP 是 10.1.0.1,而不是容器的真实 IP?

:因为从库容器通过宿主机 IP(如 192.168.100.66)访问主库,触发 Docker 的 MASQUERADE 规则,将源 IP 伪装为 Docker 网桥 IP(10.1.0.1)。这是 Linux 内核对跨网络命名空间流量的标准处理方式。

Q2:如何让主库看到真实的容器 IP?

:让主从容器处于同一 Docker 网络,并配置从库直连主库的容器 IP(如 10.1.0.86),避免经过宿主机端口映射和 SNAT。

Q3:10.1.0.1 是什么?它能被外部访问吗?

10.1.0.1 是 Docker 自定义 bridge 网络的网关 IP,对应宿主机上的虚拟网桥接口(如 br-982b...)。它仅用于容器出站流量的 SNAT 源地址,通常不能被外部主动连接

Q4:跨主机复制为何能显示真实 IP?

:因为流量直接通过物理网络传输,未经过 Docker 的 MASQUERADE 规则,源 IP 保持为从库所在主机的真实 IP(或企业 NAT 后的公网 IP)。

更多推荐