双机房双集群是什么
同城双机房(Active-Active)是国内中大型生产环境的标配——一个机房挂了,另一个机房能继续服务。K8s 时代实现双机房有几个关键挑战:
- 集群独立:两个 K8s 集群,apiserver / etcd 都独立
- 服务双活:nacos / mysql / mqtt / redis 在两边都部署
- 数据同步:MySQL binlog、MinIO rsync、Loki/Grafana 数据可选同步
- VIP + 防火墙:公网域名 + 内网 VIP 飘移
适用版本:K8s 1.28 / Redis 6.2 / EMQX 5.5.1 / 双机房(同城)
1. 整体架构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 公网用户
│
┌─────────▼─────────┐
│ DNS / 防火墙 │
│ (双 VIP 映射) │
└──┬──────────────┬──┘
│ │
┌────────▼───┐ ┌────▼───────┐
│ 机房 1 │ │ 机房 2 │
│ 10.8.33.x │ │ 10.8.35.x │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ K8s │ │ │ │ K8s │ │
│ │ cluster│ │ │ │ cluster│ │
│ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ nacos │◄┼────┼►│ nacos │ │
│ │ mysql │◄┼────┼►│ mysql │ │
│ │ redis │◄┼────┼►│ redis │ │
│ │ minio │◄┼────┼►│ minio │ │
│ └────────┘ │ │ └────────┘ │
└────────────┘ └────────────┘
▲ ▲
└────── 双向同步 ──────┘
|
2. Redis Cluster 6 节点跨机房
2.1 为什么是 6 节点
Redis Cluster 用 hash slot(16384 个槽位)分片。3 master + 3 slave 是最稳的:
- 3 master:分别负责 0-5460 / 5461-10922 / 10923-16383
- 3 slave:每个 master 1 个,热备
- 任意一个 master 挂了,对应 slave 自动升级
跨机房时——机房 1 3 节点(1 master + 1 master + 1 master),机房 2 3 节点(3 slave)。这样机房 1 整体挂了,机房 2 还在(但只有 slave,新写会失败)。
生产更稳的方案:机房 1 2 master + 1 slave,机房 2 1 master + 2 slave,跨机房分布副本——但 6 节点就够覆盖大部分场景。
2.2 节点规划
| 机房 | IP | 端口 | 角色 | 存储 |
|---|
| 机房 1 | 10.8.33.6 | 7360 | master 0 | /home/docker/redis-cluster |
| 机房 1 | 10.8.33.7 | 7370 | master 1 | /home/docker/redis-cluster |
| 机房 1 | 10.8.33.8 | 7380 | master 2 | /home/docker/redis-cluster |
| 机房 2 | 10.8.35.5 | 7550 | slave of 7380 | /home/docker/redis-cluster |
| 机房 2 | 10.8.35.6 | 7560 | slave of 7360 | /home/docker/redis-cluster |
| 机房 2 | 10.8.35.7 | 7570 | slave of 7370 | /home/docker/redis-cluster |
实际 IP 用占位符 10.8.33.x 10.8.35.x 替代。
2.3 镜像准备
1
2
3
| docker pull redis:6.2.17
docker tag redis:6.2.17 <harbor>:13001/base/redis:6.2.17
docker push <harbor>:13001/base/redis:6.2.17
|
2.4 6 个节点的 redis.conf
1
2
3
4
5
6
7
8
9
10
11
| cat > /home/docker/redis-cluster/redis.conf << "EOF"
port 7360 # 每节点不同
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly no
save ""
daemonize no
protected-mode no
pidfile /data/redis.pid
EOF
|
2.5 6 个节点的 docker run
1
2
3
4
5
6
7
8
| docker run -d \
--restart=always \
--privileged=true \
--name redis-7360 \
--net host \
-v /home/docker/redis-cluster:/data \
<harbor>:13001/base/redis:6.2.17 \
redis-server /data/redis.conf
|
6 节点命令相同,只改 --name 和 port。
2.6 创建集群
1
2
3
4
5
6
7
8
| mkdir -p /home/docker/redis-cluster
# 6 节点都要建这个目录
docker exec -it redis-7360 \
redis-cli -p 7360 --cluster create \
10.8.33.6:7360 10.8.33.7:7370 10.8.33.8:7380 \
10.8.35.5:7550 10.8.35.6:7560 10.8.35.7:7570 \
--cluster-replicas 1
|
输出(关键部分):
1
2
3
4
5
6
7
| >>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 10.8.35.6:7560 to 10.8.33.6:7360
Adding replica 10.8.35.7:7570 to 10.8.33.7:7370
Adding replica 10.8.35.5:7550 to 10.8.33.8:7380
|
确认 yes 后:
1
2
3
4
5
| >>> Performing Cluster Check (using node 10.8.33.6:7360)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
|
2.7 验证
1
2
3
4
5
6
7
8
9
10
11
12
| docker exec -it redis-7360 redis-cli -p 7360 -c
# 集群信息
127.0.0.1:7360> cluster info
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_known_nodes:6
# cluster_size:3
# 节点列表
127.0.0.1:7360> cluster nodes
# 02f0476e155902b1eda400ee84602ace90e821cd 10.8.35.5:7550@17550 slave 952938798efe17832a3c9492be9a45675c58fe69 ...
|
2.8 Spring Boot 客户端配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| spring:
redis:
timeout: 10s
cluster:
nodes:
- 10.8.33.6:7360
- 10.8.33.7:7370
- 10.8.33.8:7380
- 10.8.35.5:7550
- 10.8.35.6:7560
- 10.8.35.7:7570
max-redirects: 3
jedis:
pool:
max-active: 500
max-wait: -1ms
max-idle: 200
min-idle: 100
|
2.9 单 db0 的坑
Redis Cluster 只支持 db0——所有 key 都在 db0。如果你的应用假设"按 db 切分数据"(如 db:4),需要重构。
3. nacos 双活
nacos 双机房同时部署,应用启动时配两个 server:
1
| spring.cloud.nacos.discovery.server-addr=10.8.33.6:8848,10.8.35.5:8848
|
每个机房都跑 nacos,互相不依赖——一个挂掉另一个继续工作。
| 机房 | 对外地址 | 部署方式 | 账户 |
|---|
| 机房 1 | http://10.8.33.250:30680/nacos | k8s | nacos/nacos |
| 机房 2 | http://10.8.35.250:30680/nacos | k8s | nacos/nacos |
4. MySQL 双主架构
双机房 MySQL 同步有 2 种思路:
4.1 双主互相同步
1
2
3
| 机房1-Master1 ────→ 机房2-Master2
↑ │
└───────────────────┘
|
每个机房都是 Master,写自己 + 同步到对方。风险:双写冲突、循环同步。
解决:每张表用 server_id 区分,比如 ID 奇数写机房 1,偶数写机房 2。
4.2 主从单向同步
1
| 机房1-Master ────→ 机房2-Slave
|
机房 1 是主,机房 2 是从,只读不写。机房 1 挂掉时人工切主。
4.3 多租户 MySQL 部署
1
2
3
4
5
| volumes:
- name: mysql-data
nfs:
server: 10.8.33.111
path: /nfs/park-prod/mysql/data
|
1
2
3
4
5
| volumes:
- name: mysql-data
nfs:
server: 10.8.33.111
path: /nfs/safety-dev/mysql/data
|
/etc/mysql/conf.d 走 ConfigMap 注入。
| 机房 | 用途 | 部署方式 |
|---|
| 机房 1 10.8.33.8:3309 | 多租户库 + powerjob + nacos | 二进制 |
| 机房 2 10.8.35.6:3309 | 多租户库 | 二进制 |
| 机房 2 10.8.35.250:30606 | powerjob + nacos | k8s |
5. EMQX MQTT 集群
5.5.1 用 static 集群发现策略,所有节点都列在 seeds 里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| version: "3"
services:
emqx1:
image: emqx:5.5.1
container_name: emqx1
environment:
- EMQX_NODE_NAME=emqx@10.8.33.6
- EMQX_CLUSTER__DISCOVERY_STRATEGY=static
- EMQX_CLUSTER__STATIC__SEEDS=[emqx@10.8.33.6,emqx@10.8.33.7,emqx@10.8.33.8,emqx@10.8.35.5,emqx@10.8.35.6,emqx@10.8.35.7]
network_mode: "host"
healthcheck:
test: ["CMD", "/opt/emqx/bin/emqx_ctl", "status"]
interval: 5s
timeout: 25s
retries: 5
|
5.1 客户端配置
1
2
3
4
| 客户端账号: <your-org> / {{MQTT_PASSWORD}}
机房 1 节点: 10.8.33.6, 10.8.33.7, 10.8.33.8
机房 2 节点: 10.8.35.5, 10.8.35.6(不可用), 10.8.35.7
公网入口: tcp://mq.cloud.example.com:1885
|
防火墙映射:公网 1885 → 10.8.33.110:1883 → 10.8.33.6:1883
MQTT 密码用占位符 {{MQTT_PASSWORD}} 替代。
5.2 客户端连接示例
1
2
3
4
5
6
7
8
9
| # 加入集群
docker exec -it emqx1 emqx ctl cluster join emqx@10.8.35.5
# 看集群状态
docker exec -it emqx1 emqx ctl cluster status
# 重启
docker-compose -f /home/docker-compose-emqx.yml down
docker-compose -f /home/docker-compose-emqx.yml up -d
|
6. nginxwebui 负载均衡
| 服务 | 协议 | 策略 | 负载 | 备注 |
|---|
| mqtt-ws | http | ip | 6 节点 8083 | 前端订阅 |
| mqtt-tcp | tcp | - | 6 节点 1883 | 后端订阅 |
| gateway | http | ip | 双 VIP:30697 | 网关 |
| location-netty | tcp | - | 双 VIP:30687 | 定位基站 |
| minioapi | http | ip | 10.8.33.254:9091 | 文件服务 |
| safety-pc | http | ip | 双 VIP:30618 | PC 入口 |
| safety-apph5 | http | ip | 双 VIP:30628 | APP 入口 |
| webrtc-stun | tcp/udp | - | 10.8.33.127:3478 + 10.8.35.7:3478 | STUN |
| webrtc | http | - | 10.8.33.127:8000 + 10.8.35.7:8000 | 虚拟 ip:443 |
7. 防火墙映射
公网到机房 1:
1
| 公网 <your-public-ip>:1885 → 10.8.33.110:1883 → 10.8.33.6:1883
|
公网到机房 2:
1
| 公网 <your-public-ip>:xxxx → 10.8.35.2:xxxx → 机房 2 服务
|
实际 IP 用占位符替代。
机房 2 防火墙登录:https://10.8.35.2,admin / {{FIREWALL_PASSWORD}}
8. 同步策略
| 资源 | 同步方式 |
|---|
| nacos 配置 | nacos 自身多节点 + 应用端多 server-addr |
| MySQL 数据 | binlog 主从 / 双主 + server_id 区分 |
| Redis 数据 | Cluster 自动跨节点同步 |
| MinIO 文件 | rsync + lsyncd 双向(已有方案) |
| MQTT 消息 | EMQX 集群内自动同步 |
| 应用镜像 | 都从同一个 harbor 私有仓库拉 |
| Prometheus 数据 | 可选 federation / remote_write |
| Loki 日志 | 可选都指向同一个对象存储 |
9. VIP 与 keepalived 跨机房
unicast_src_ip + unicast_peer 模式(见 2015-02-15 文章)。
机房 1:
1
2
3
4
| unicast_src_ip 10.8.33.253
unicast_peer {
10.8.35.7
}
|
机房 2:
1
2
3
4
| unicast_src_ip 10.8.35.7
unicast_peer {
10.8.33.253
}
|
不同机房 VIP 飘移。
10. 排错
| 现象 | 原因 | 解决 |
|---|
Redis Cluster CLUSTERDOWN | 跨机房链路断 | 检查防火墙 / 路由 |
| MySQL 双主冲突 | 两个机房同时写 | 改 server_id 区分写 |
| nacos 注册的服务没同步 | 客户端没配双 server-addr | 应用配置加 , 分隔 |
| EMQX 节点反复脱离集群 | 网络抖动 | 调大 cluster.node_timeout |
| 公网 502 | keepalived 没漂 | 看 nginx / keepalived 日志 |
| 机房 1 全挂后机房 2 写不了 | MySQL 单向同步 | 改双向 / 切主 |
11. 小结
同城双机房 K8s 集群的核心是"两边都能独立工作":
- Redis Cluster 6 节点 跨机房分布是基础
- nacos / MySQL / EMQX 都做双活
- MinIO 用 rsync 双向同步
- VIP + 防火墙 让公网无感
- 故障演练——定期模拟机房 1 挂掉
这是 K8s 系列博客的最后一篇——到此生产 K8s 集群从 0 到异地多活的完整链路已经走通。