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
|
#!/usr/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
mkdir -p ./redis/data
cat > ./redis/redis.conf <<'EOF'
port 6379
bind 0.0.0.0
protected-mode no
requirepass redis
appendonly yes
appendfsync everysec
dir /data
logfile ""
EOF
echo "Redis 单机挂载目录与配置已生成:${SCRIPT_DIR}"
mkdir -p ./mysql/{conf,data}
cat > ./mysql/conf/my.cnf <<'EOF'
[mysqld]
bind-address=0.0.0.0
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default-time-zone=+08:00
[client]
default-character-set=utf8mb4
EOF
echo "MySQL 单机挂载目录与配置已生成:${SCRIPT_DIR}"
mkdir -p ./nacos/{data,logs}
|
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
|
networks:
middle-net:
driver: bridge
services:
redis:
image: redis:7.4.8
container_name: redis
restart: always
networks: [middle-net]
ports: ["6379:6379"]
volumes:
- ./redis/redis.conf:/etc/redis/redis.conf
- ./redis/data:/data
healthcheck:
interval: 10s
timeout: 5s
retries: 12
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 6379 ping | grep -qx PONG"]
command: ["redis-server", "/etc/redis/redis.conf"]
mysql:
image: mysql:8.4.10
restart: always
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: test
TZ: Asia/Shanghai
ports: ["3306:3306"]
volumes:
- ./mysql/conf:/etc/mysql/conf.d:ro
- ./mysql/data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=mysql mysql -h 127.0.0.1 -uroot -Nse 'SELECT 1' | grep -qx 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
networks: [middle-net]
nacos:
image: nacos/nacos-server:v3.2.4
restart: always
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: ""
NACOS_AUTH_ENABLE: "false"
NACOS_AUTH_ADMIN_ENABLE: "true"
NACOS_AUTH_CONSOLE_ENABLE: "true"
NACOS_AUTH_TOKEN: ZR8SunEu4VA8BwRx92SyRF1u5GM6lmQGByQCk+OPcvJfDBDnO4LZDOYwmilUFiqG
NACOS_AUTH_IDENTITY_KEY: nacos
NACOS_AUTH_IDENTITY_VALUE: nacos
JVM_XMS: 512m
JVM_XMX: 512m
JVM_XMN: 256m
TZ: Asia/Shanghai
ports:
- "8080:8080"
- "8848:8848"
- "9848:9848"
volumes:
- ./nacos/data:/home/nacos/data
- ./nacos/logs:/home/nacos/logs
networks: [middle-net]
|
Redis 在容器化部署里最大的坑是 集群模式(Cluster)和哨兵模式(Sentinel)会把地址"回传"给客户端:
- Cluster 模式下,客户端第一次连接任意节点后,节点会返回
MOVED/ASK 重定向指令,其中包含的 IP 是节点通过 cluster-announce-ip 宣告的地址;如果不配置,节点默认宣告的是容器内部的桥接网络 IP(如 172.18.0.x),宿主机外的客户端根本连不上,导致 -c 集群模式客户端连接后卡死或报错。
- Sentinel 模式下,Sentinel 通过
INFO replication 探测到的 master/replica 地址同样是容器内部 IP;客户端问 Sentinel “谁是 master” 时,Sentinel 会把这个内部 IP 返回给客户端,导致宿主机上的客户端拿到一个不可达的地址。
解决方式:使用 bridge 模式 + 显式端口映射(宿主机端口 = 容器端口,不同节点用不同宿主机端口区分),并在每个节点的配置里显式设置:
- Cluster:
cluster-announce-ip <宿主机IP>、cluster-announce-port <映射的宿主机端口>、cluster-announce-bus-port <映射的宿主机总线端口>。
- Sentinel:主从节点设置
replica-announce-ip <宿主机IP> + replica-announce-port <映射的宿主机端口>;Sentinel 自身监控 master 时也使用 <宿主机IP>:<宿主机映射端口>,而不是容器名/容器内部 IP。
统一密码:requirepass redis(主从复制场景同时设置 masterauth redis)。
standalone/deploy.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#!/usr/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
mkdir -p conf data
cat > conf/redis.conf <<'EOF'
port 6379
bind 0.0.0.0
protected-mode no
requirepass redis
appendonly yes
appendfsync everysec
dir /data
logfile ""
EOF
echo "Redis 单机挂载目录与配置已生成:${SCRIPT_DIR}"
|
standalone/docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
networks:
redis-net:
driver: bridge
services:
redis:
image: redis:7.4.8
container_name: redis
restart: always
networks: [redis-net]
ports: ["6379:6379"]
volumes:
- ./conf/redis.conf:/etc/redis/redis.conf
- ./data:/data
command: ["redis-server", "/etc/redis/redis.conf"]
|
1
2
3
4
|
cd standalone
chmod +x deploy.sh
bash deploy.sh
docker-compose up -d
|
1
2
3
4
|
redis-cli -h <宿主机IP> -p 6379 -a redis ping
# 期望输出:PONG
redis-cli -h <宿主机IP> -p 6379 -a redis set foo bar
redis-cli -h <宿主机IP> -p 6379 -a redis get foo
|
每个节点监听独立端口,并保持 宿主机端口 = 容器监听端口。业务端口为 7001–7006,总线端口为 17001–17006,所有节点通告 HOST_IP。
Cluster 客户端会根据 MOVED / ASK 返回的地址连接其他节点;若返回容器内部地址,宿主机外客户端可能无法访问。Sentinel 同样需要将可达的主从地址返回给客户端。
Redis 7.4.8 提供 cluster-announce-ip、cluster-announce-port、cluster-announce-bus-port 用于显式通告地址。本文按 bridge 要求采用固定通告地址方案,但它依赖容器可以经宿主机发布端口回流到自己及其他节点,不能视为任意 NAT 环境都适用。配置依据:Redis 7.4.8 配置文件。
客户端需要访问全部 6 个业务端口,节点间需要访问全部业务和总线端口。只开放种子节点端口会导致 MOVED 重定向后连接失败。总线端口仅向节点所需网络开放。
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
|
#!/usr/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
# 自动通过 `ip a` 获取本机物理网卡 IP(排除 lo/docker/br-/veth/virbr/tun/tap 等虚拟网卡);
# 如自动识别的不是期望的网卡,可显式传参覆盖:./init.sh <宿主机IP>
detect_host_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $2, $4}' \
| grep -Ev '^(lo|docker[0-9]*|br-[0-9a-f]+|veth.*|virbr[0-9]*|tun[0-9]*|tap[0-9]*)\s' \
| head -n1 \
| awk '{print $2}' \
| cut -d/ -f1 \
|| true
}
HOST_IP="${1:-$(detect_host_ip)}"
if [ -z "${HOST_IP}" ]; then
echo "错误:未能通过 ip a 自动识别宿主机物理网卡 IP,请手动指定:$0 <宿主机IP>" >&2
exit 1
fi
echo "使用宿主机IP:${HOST_IP}(如需手动指定,用法:$0 <宿主机IP>)"
for port in 7001 7002 7003 7004 7005 7006; do
BUS_PORT=$((port + 10000))
mkdir -p "node-${port}/conf" "node-${port}/data"
cat > "node-${port}/conf/redis.conf" <<EOF
bind 0.0.0.0
port ${port}
protected-mode yes
requirepass redis
masterauth redis
dir /data
appendonly yes
appendfsync everysec
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
cluster-announce-ip ${HOST_IP}
cluster-announce-port ${port}
cluster-announce-bus-port ${BUS_PORT}
replica-announce-ip ${HOST_IP}
replica-announce-port ${port}
daemonize no
logfile ""
EOF
echo "已生成 node-${port}/conf/redis.conf (announce ${HOST_IP}:${port})"
done
echo "全部 6 个节点挂载目录与配置已生成:${SCRIPT_DIR}"
|
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
|
networks:
redis-net:
driver: bridge
name: redis-cluster
x-redis: &redis-common
image: redis:7.4.8
restart: always
networks: [redis-net]
x-healthcheck: &redis-healthcheck
interval: 10s
timeout: 5s
retries: 12
services:
redis-7001:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7001:7001"
- "17001:17001"
volumes: ["./node-7001:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7001 ping | grep -qx PONG"]
redis-7002:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7002:7002"
- "17002:17002"
volumes: ["./node-7002:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7002 ping | grep -qx PONG"]
redis-7003:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7003:7003"
- "17003:17003"
volumes: ["./node-7003:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7003 ping | grep -qx PONG"]
redis-7004:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7004:7004"
- "17004:17004"
volumes: ["./node-7004:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7004 ping | grep -qx PONG"]
redis-7005:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7005:7005"
- "17005:17005"
volumes: ["./node-7005:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7005 ping | grep -qx PONG"]
redis-7006:
<<: *redis-common
command: ["redis-server", "/data/conf/redis.conf"]
ports:
- "7006:7006"
- "17006:17006"
volumes: ["./node-7006:/data"]
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 7006 ping | grep -qx PONG"]
|
nodes.conf 是 Redis 自动维护的拓扑文件,不是手写配置,必须持久化,每节点独立保存。
先验证 每个容器 → 每个宿主机业务/总线端口,全部通过才能创建集群: node-check.sh
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
|
#!/bin/bash
# node-check.sh
detect_host_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $2, $4}' \
| grep -Ev '^(lo|docker[0-9]*|br-[0-9a-f]+|veth.*|virbr[0-9]*|tun[0-9]*|tap[0-9]*)\s' \
| head -n1 \
| awk '{print $2}' \
| cut -d/ -f1 \
|| true
}
HOST_IP="${1:-$(detect_host_ip)}"
if [ -z "${HOST_IP}" ]; then
echo "错误:未能通过 ip a 自动识别宿主机物理网卡 IP,请手动指定:$0 <宿主机IP>" >&2
exit 1
fi
echo "使用宿主机IP:${HOST_IP}(如需手动指定,用法:$0 <宿主机IP>)"
for src in 7001 7002 7003 7004 7005 7006; do
for dst in 7001 7002 7003 7004 7005 7006; do
docker-compose exec -T -e REDISCLI_AUTH=redis "redis-${src}" \
redis-cli -h "$HOST_IP" -p "$dst" PING
docker-compose exec -T "redis-${src}" \
timeout 3 bash -c "exec 3<>/dev/tcp/${HOST_IP}/$((dst + 10000))" || break 2
done
done
|
业务端口应全部 PONG,总线 TCP 探测退出码应为 0(无需向总线发送 Redis 命令)。如有超时,先检查 Linux IP 转发、Docker NAT/防火墙、宿主机 IP 选择和端口绑定。仅容器内 localhost 能连通不算通过。
首次创建集群(只执行一次,要求节点无数据、未加入其他集群):create-cluster.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#!/bin/bash
detect_host_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $2, $4}' \
| grep -Ev '^(lo|docker[0-9]*|br-[0-9a-f]+|veth.*|virbr[0-9]*|tun[0-9]*|tap[0-9]*)\s' \
| head -n1 \
| awk '{print $2}' \
| cut -d/ -f1 \
|| true
}
HOST_IP="${1:-$(detect_host_ip)}"
if [ -z "${HOST_IP}" ]; then
echo "错误:未能通过 ip a 自动识别宿主机物理网卡 IP,请手动指定:$0 <宿主机IP>" >&2
exit 1
fi
echo "使用宿主机IP:${HOST_IP}(如需手动指定,用法:$0 <宿主机IP>)"
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 redis-cli \
--cluster create \
"$HOST_IP:7001" "$HOST_IP:7002" "$HOST_IP:7003" \
"$HOST_IP:7004" "$HOST_IP:7005" "$HOST_IP:7006" \
--cluster-replicas 1 --cluster-yes
|
--cluster-replicas 1 表示每个主节点配 1 个从节点,最终自动分配为 3 主 3 从
同宿主机节点没有主从物理隔离,创建时可能提示同 IP 的反亲和限制。预期仍应形成 3 主 3 从,每主对应一个副本;角色以实际拓扑为准。
查看集群状态命令
1
2
3
4
5
6
7
8
9
|
# 查看集群状态(state:ok 表示成功,cluster_known_nodes 应为 6,cluster_size 应为 3)
redis-cli -h <宿主机IP> -p 6371 -a redis cluster info
# 查看集群节点信息(角色 master/slave、槽位分配、connected 状态)
redis-cli -h <宿主机IP> -p 6371 -a redis cluster nodes
# 用集群客户端模式验证读写与自动重定向
redis-cli -h <宿主机IP> -p 6371 -a redis -c set foo bar
redis-cli -h <宿主机IP> -p 6372 -a redis -c get foo
|
cluster-check.sh
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
|
#!/bin/bash
detect_host_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $2, $4}' \
| grep -Ev '^(lo|docker[0-9]*|br-[0-9a-f]+|veth.*|virbr[0-9]*|tun[0-9]*|tap[0-9]*)\s' \
| head -n1 \
| awk '{print $2}' \
| cut -d/ -f1 \
|| true
}
HOST_IP="${1:-$(detect_host_ip)}"
if [ -z "${HOST_IP}" ]; then
echo "错误:未能通过 ip a 自动识别宿主机物理网卡 IP,请手动指定:$0 <宿主机IP>" >&2
exit 1
fi
echo "使用宿主机IP:${HOST_IP}(如需手动指定,用法:$0 <宿主机IP>)"
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli --cluster check "$HOST_IP:7001"
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli -h "$HOST_IP" -p 7001 CLUSTER INFO
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli -h "$HOST_IP" -p 7001 CLUSTER NODES
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli -h "$HOST_IP" -p 7001 CLUSTER SHARDS
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli -c -h "$HOST_IP" -p 7001 SET deployment:cluster ok
docker-compose exec -T -e REDISCLI_AUTH=redis redis-7001 \
redis-cli -c -h "$HOST_IP" -p 7002 GET deployment:cluster
|
成功标准:
cluster_state:ok、cluster_known_nodes:6、cluster_size:3。
cluster_slots_assigned:16384、cluster_slots_ok:16384,cluster_slots_fail 和 cluster_slots_pfail 为 0。
CLUSTER NODES 中 3 个 master、3 个 slave(协议仍用此词),全部 connected;没有 fail/fail? 标记。
- 地址显示
宿主机IP:700x@1700x,不能是 172.x 等容器地址;每个副本的主节点 ID 有效。
--cluster check 报告 16384 槽位覆盖完整;跨节点读写返回 OK、ok。
在客户端机器上使用 REDISCLI_AUTH=redis redis-cli -c -h <宿主机IP> -p 7001 GET deployment:cluster 再次验收。业务 SDK 必须启用 Cluster 模式,并配置多个种子节点。
故障演练:先用 CLUSTER NODES 找出一个主节点及其副本,执行 docker-compose stop redis-<主节点端口>。等待数个 cluster-node-timeout 后,从存活节点重复检查,确认副本晋升、槽位恢复且能读写。最后 docker-compose start redis-<原主节点端口>,确认节点重新加入。停止服务名应根据实际角色选择,不能默认 7001 永远是主节点。
初始主节点为 HOST_IP:16379,副本为 26379、36379;哨兵端口 46379、46479、46579。监控名称为 mymaster,quorum 为 2。
所有 Redis 节点设置 requirepass、masterauth、replica-announce-ip/port;所有哨兵设置 sentinel announce-ip/port,监控目标也使用宿主机 IP。这样哨兵返回给客户端的主节点地址、发现的副本及哨兵地址均可通过宿主机访问。参考:Sentinel 容器网络说明、副本地址通告。
本示例 Redis 数据节点密码和 Sentinel 自身连接密码均为 redis。客户端需分别配置 Redis 密码、Sentinel 密码和主名称 mymaster,只填写 Redis 密码不足以查询受保护的哨兵。
deploy.sh
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
|
#!/usr/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
# 自动通过 `ip a` 获取本机物理网卡 IP(排除 lo/docker/br-/veth/virbr/tun/tap 等虚拟网卡);
# 如自动识别的不是期望的网卡,可显式传参覆盖:./init.sh <宿主机IP>
detect_host_ip() {
ip -4 -o addr show scope global 2>/dev/null \
| awk '{print $2, $4}' \
| grep -Ev '^(lo|docker[0-9]*|br-[0-9a-f]+|veth.*|virbr[0-9]*|tun[0-9]*|tap[0-9]*)\s' \
| head -n1 \
| awk '{print $2}' \
| cut -d/ -f1 \
|| true
}
HOST_IP="${1:-$(detect_host_ip)}"
if [ -z "${HOST_IP}" ]; then
echo "错误:未能通过 ip a 自动识别宿主机物理网卡 IP,请手动指定:$0 <宿主机IP>" >&2
exit 1
fi
echo "使用宿主机IP:${HOST_IP}(如需手动指定,用法:$0 <宿主机IP>)"
mkdir -p master/conf master/data
cat > master/conf/redis.conf <<EOF
port 6379
bind 0.0.0.0
protected-mode no
requirepass redis
masterauth redis
appendonly yes
dir /data
replica-announce-ip ${HOST_IP}
replica-announce-port 16379
EOF
gen_replica() {
local name=$1 port=$2
mkdir -p ${name}/conf ${name}/data
cat > "${name}/conf/redis.conf" <<EOF
port 6379
bind 0.0.0.0
protected-mode no
requirepass redis
masterauth redis
appendonly yes
dir /data
replicaof ${HOST_IP} 16379
replica-announce-ip ${HOST_IP}
replica-announce-port ${port}
EOF
}
gen_replica slave1 26379
gen_replica slave2 36379
gen_sentinel() {
local name=$1 port=$2
mkdir -p ${name}/conf ${name}/data
cat > "${name}/conf/sentinel.conf" <<EOF
port 26379
bind 0.0.0.0
sentinel monitor mymaster ${HOST_IP} 16379 2
sentinel auth-pass mymaster redis
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel announce-ip ${HOST_IP}
sentinel announce-port ${port}
EOF
}
gen_sentinel sentinel1 46379
gen_sentinel sentinel2 46479
gen_sentinel sentinel3 46579
echo "哨兵模式挂载目录与配置已生成:${SCRIPT_DIR},宿主机IP=${HOST_IP}"
|
说明:sentinel monitor mymaster <宿主机IP> 6390 2 直接用宿主机 IP + 宿主机映射端口监控 master——因为 bridge 网络下容器访问宿主机映射端口通常是可达的(Linux 下宿主机 IP 本身就在同一网段/网桥可达;Docker Desktop for Windows/Mac 也可达宿主机的监听端口)。这样 Sentinel 后续 failover 后上报的新 master 地址也会是宿主机地址,客户端才能正常追随主从切换。
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
networks:
redis-net:
driver: bridge
x-redis-server-common: &redis-server-common
image: redis:7.4.8
restart: always
networks: [redis-net]
command: ["redis-server", "/etc/redis/redis.conf"]
x-redis-sentinel-common: &redis-sentinel-common
image: redis:7.4.8
restart: always
networks: [redis-net]
command: ["redis-sentinel", "/etc/redis/sentinel.conf"]
x-healthcheck: &redis-healthcheck
interval: 10s
timeout: 5s
retries: 12
services:
redis-master:
<<: *redis-server-common
container_name: redis-master
ports:
- "16379:6379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 6379 ping | grep -qx PONG"]
volumes:
- ./master/conf/redis.conf:/etc/redis/redis.conf
- ./master/data:/data
redis-slave1:
<<: *redis-server-common
container_name: redis-slave1
ports:
- "26379:6379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 6379 ping | grep -qx PONG"]
volumes:
- ./slave1/conf/redis.conf:/etc/redis/redis.conf
- ./slave1/data:/data
depends_on: [redis-master]
redis-slave2:
<<: *redis-server-common
container_name: redis-slave2
ports:
- "36379:6379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 6379 ping | grep -qx PONG"]
volumes:
- ./slave2/conf/redis.conf:/etc/redis/redis.conf
- ./slave2/data:/data
depends_on: [redis-master]
redis-sentinel1:
<<: *redis-sentinel-common
container_name: redis-sentinel1
ports:
- "46379:26379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 26379 ping | grep -qx PONG"]
volumes:
- ./sentinel1/conf/sentinel.conf:/etc/redis/sentinel.conf
depends_on: [redis-master, redis-slave1, redis-slave2]
redis-sentinel2:
<<: *redis-sentinel-common
container_name: redis-sentinel2
ports:
- "46479:26379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 26379 ping | grep -qx PONG"]
volumes:
- ./sentinel2/conf/sentinel.conf:/etc/redis/sentinel.conf
depends_on: [redis-master, redis-slave1, redis-slave2]
redis-sentinel3:
<<: *redis-sentinel-common
container_name: redis-sentinel3
ports:
- "46579:26379"
healthcheck:
<<: *redis-healthcheck
test: ["CMD-SHELL", "REDISCLI_AUTH=redis redis-cli -p 26379 ping | grep -qx PONG"]
volumes:
- ./sentinel3/conf/sentinel.conf:/etc/redis/sentinel.conf
depends_on: [redis-master, redis-slave1, redis-slave2]
|
这里将整个节点目录挂载到 /data,配置放在其 conf 子目录且可写。Sentinel 会重写自身配置,Redis 数据节点也需要持久化故障切换后的角色配置;不能使用只读配置、只挂载单个配置文件,或每次启动重生成初始 replicaof。官方 Redis 镜像入口在 /data 下准备属主;若权限报错,检查实际 UID/GID 和宿主机挂载权限。
1
2
3
4
|
cd redis-sentinel
chmod +x deploy.sh
bash deploy.sh # 默认通过 ip a 自动识别宿主机 IP;如识别有误,改用 ./init.sh <宿主机IP>
docker-compose up -d
|
1
2
3
4
5
6
7
8
9
10
11
|
# 查看主从复制状态
docker exec -it redis-master redis-cli -a redis info replication
# 查看哨兵视角
docker exec -it redis-sentinel1 redis-cli -p 26379 sentinel master mymaster
docker exec -it redis-sentinel1 redis-cli -p 26379 sentinel replicas mymaster
docker exec -it redis-sentinel1 redis-cli -p 26379 sentinel sentinels mymaster
# 模拟主库故障,观察自动切换
docker stop redis-master
docker exec -it redis-sentinel2 redis-cli -p 26379 sentinel master mymaster # 几秒后应显示新 master
|
宿主机验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 从slave1容器内部,测试能不能通过"宿主机IP"连回同一台机器的master端口
$ docker exec -it redis-slave1 redis-cli -h <宿主机IP> -p 6379 -a redis ping
docker exec -it redis-slave1 redis-cli -h 10.4.100.125 -p 6379 -a redis ping
# 应该返回 PONG
# 从sentinel1容器内部同样测试
$ docker exec -it sentinel1 redis-cli -h <宿主机IP> -p 6379 -a redis ping
docker exec -it sentinel1 redis-cli -h 10.4.100.125 -p 6379 -a redis ping
# 1. 主从复制是否正常
docker exec -it redis-master redis-cli -a redis info replication
# 应看到 connected_slaves:2,并且两个slave的ip字段显示的是宿主机IP,不是容器内部IP
# 2. sentinel看到的master地址
docker exec -it sentinel1 redis-cli -h 10.4.100.125 -p 46379 sentinel get-master-addr-by-name mymaster
# 应返回 <宿主机IP> 6379,而不是172.x.x.x这种内部地址
# 3. sentinel看到的从库地址
docker exec -it sentinel1 redis-cli -h 10.4.100.125 -p 46379 sentinel replicas mymaster
# 检查每个slave的ip/port字段,应该也是宿主机IP + 6380/6381
|
默认库 test,账号 root/mysql。挂载整个 /etc/mysql/conf.d 配置目录及 /var/lib/mysql 数据目录。初始化变量只对空数据目录有效;修改环境变量不会修改已有 root 密码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd "$SCRIPT_DIR"
mkdir -p ./{conf,data}
cat > conf/my.cnf <<'EOF'
[mysqld]
bind-address=0.0.0.0
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default-time-zone=+08:00
[client]
default-character-set=utf8mb4
EOF
chmod 644 conf/my.cnf
|
docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
name: mysql-standalone
services:
mysql:
image: mysql:8.4.10
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: test
TZ: Asia/Shanghai
ports: ["3306:3306"]
volumes:
- ./conf:/etc/mysql/conf.d:ro
- ./data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=mysql mysql -h 127.0.0.1 -uroot -Nse 'SELECT 1' | grep -qx 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
networks: [mysql-net]
networks:
mysql-net:
driver: bridge
|
主库对外 3306,从库对外 3307。容器间通过 mysql-primary:3306 通信;这里无需向客户端通告拓扑,不存在 Redis 式重定向地址映射问题。
以下流程仅适用于 两端均为全新空数据目录。已有主库需先做一致性备份、恢复及 GTID 衔接,不能将空从库直接当作已同步。主从复制本身不提供自动主库故障切换。
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
|
#!/bin/bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd $SCRIPT_DIR
mkdir -p ./{primary,replica}/{conf,data}
cat > primary/conf/my.cnf <<'EOF'
[mysqld]
bind-address=0.0.0.0
server-id=1
log-bin=mysql-bin
binlog-format=ROW
gtid-mode=ON
enforce-gtid-consistency=ON
log-replica-updates=ON
sync-binlog=1
innodb-flush-log-at-trx-commit=1
binlog-expire-logs-seconds=604800
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default-time-zone=+08:00
EOF
cat > replica/conf/my.cnf <<'EOF'
[mysqld]
bind-address=0.0.0.0
server-id=2
log-bin=mysql-bin
relay-log=relay-bin
relay-log-recovery=ON
binlog-format=ROW
gtid-mode=ON
enforce-gtid-consistency=ON
log-replica-updates=ON
read-only=ON
binlog-expire-logs-seconds=604800
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
default-time-zone=+08:00
EOF
chmod 644 primary/conf/my.cnf replica/conf/my.cnf
|
docker-compose.yaml
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
|
name: mysql-replication
x-mysql: &mysql-common
image: mysql:8.4.10
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: mysql
MYSQL_ROOT_HOST: "%"
MYSQL_DATABASE: test
TZ: Asia/Shanghai
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=mysql mysql -h 127.0.0.1 -uroot -Nse 'SELECT 1' | grep -qx 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
networks: [mysql-net]
services:
mysql-primary:
<<: *mysql-common
ports: ["3306:3306"]
volumes:
- ./primary/conf:/etc/mysql/conf.d:ro
- ./primary/data:/var/lib/mysql
mysql-replica:
<<: *mysql-common
ports: ["3307:3306"]
volumes:
- ./replica/conf:/etc/mysql/conf.d:ro
- ./replica/data:/var/lib/mysql
networks:
mysql-net:
driver: bridge
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d --wait --wait-timeout 360
# 在主库新建专用复制账号。此处复制账号为 repl / mysql-repl。
docker-compose exec -T -e MYSQL_PWD=mysql mysql-primary mysql -uroot <<'SQL'
CREATE USER 'repl'@'%' IDENTIFIED BY 'mysql-repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SELECT @@server_id, @@server_uuid, @@gtid_mode;
SQL
docker-compose exec -T -e MYSQL_PWD=mysql mysql-replica mysql -uroot <<'SQL'
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='mysql-primary',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='mysql-repl',
SOURCE_AUTO_POSITION=1,
GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
SET PERSIST read_only=ON;
SET PERSIST super_read_only=ON;
SHOW REPLICA STATUS\G
SQL
|
super_read_only 在镜像首次初始化完成后启用,并持久化到数据目录的 mysqld-auto.cnf,避免妨碍初始化创建用户及数据库。不能复制主库的整个运行中数据目录给从库,否则可能复用 server_uuid。
MySQL 8.4 使用默认 caching_sha2_password,无需启用旧 mysql_native_password。在此非 TLS 的内网示例中,GET_SOURCE_PUBLIC_KEY=1 支持复制账号认证;它不等于加密整个复制链路。SQL 使用 8.4 的 SOURCE/REPLICA 语法。参考:CHANGE REPLICATION SOURCE TO。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
docker-compose exec -T -e MYSQL_PWD=mysql mysql-primary mysql -uroot \
-e 'SHOW BINARY LOG STATUS; SELECT @@server_id, @@server_uuid, @@gtid_mode;'
docker-compose exec -T -e MYSQL_PWD=mysql mysql-replica mysql -uroot \
-e 'SHOW REPLICA STATUS\G'
docker-compose exec -T -e MYSQL_PWD=mysql mysql-replica mysql -uroot \
-e 'SELECT @@server_id, @@server_uuid, @@read_only, @@super_read_only;'
docker-compose exec -T -e MYSQL_PWD=mysql mysql-primary mysql -uroot test <<'SQL'
CREATE TABLE IF NOT EXISTS replication_check (id INT PRIMARY KEY, note VARCHAR(64));
INSERT INTO replication_check VALUES (1, 'primary-to-replica-ok')
ON DUPLICATE KEY UPDATE note='primary-to-replica-ok';
SQL
# 精确等待主库已提交的 GTID 集合,避免仅靠固定 sleep 判断复制完成。
GTID=$(docker-compose exec -T -e MYSQL_PWD=mysql mysql-primary \
mysql -uroot -Nse 'SELECT @@GLOBAL.gtid_executed' | tr -d '\r')
docker-compose exec -T -e MYSQL_PWD=mysql mysql-replica mysql -uroot \
-e "SELECT WAIT_FOR_EXECUTED_GTID_SET('${GTID}', 30) AS wait_result;"
docker-compose exec -T -e MYSQL_PWD=mysql mysql-replica mysql -uroot test \
-e 'SELECT * FROM replication_check;'
|
成功标准:
Replica_IO_Running: Yes 和 Replica_SQL_Running: Yes。
Source_Host: mysql-primary、Source_Port:3306、Auto_Position:1。
Last_IO_Error、Last_SQL_Error 为空;稳定后 Seconds_Behind_Source:0,不能只凭此一项判断成功。
- 主从
server_id 和 server_uuid 不同,从库 read_only=1、super_read_only=1。
wait_result=0(1 为超时),从库查到 1 / primary-to-replica-ok。
从外部客户端分别连接 宿主机IP:3306 和 宿主机IP:3307 查询 test.replication_check。若出现连接线程失败检查 DNS、账号和认证;SQL 线程失败检查冲突事务,不能跳过错误后直接认定成功。
挂载整个 /etc/nginx/conf.d 目录,主配置继续使用官方镜像提供的 /etc/nginx/nginx.conf,避免空目录覆盖掉主配置及 mime.types。参考:Nginx 官方镜像。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
mkdir -p "$DEPLOY_ROOT/nginx"/{conf.d,html}
cd "$DEPLOY_ROOT/nginx"
cat > conf.d/default.conf <<'EOF'
server {
listen 80;
server_name _;
charset utf-8;
location / {
root /usr/share/nginx/html;
index index.html;
}
location = /health {
default_type text/plain;
return 200 "nginx-ok\n";
}
}
EOF
printf '<!doctype html><html><body>Nginx deployment OK</body></html>\n' > html/index.html
|
创建 docker-compose.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
name: nginx
services:
nginx:
image: nginx:1.30.4
restart: unless-stopped
ports: ["80:80"]
volumes:
- ./conf.d:/etc/nginx/conf.d:ro
- ./html:/usr/share/nginx/html:ro
networks: [nginx-net]
networks:
nginx-net:
driver: bridge
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
docker-compose config --quiet
docker-compose pull
docker-compose run --rm --no-deps nginx nginx -t
docker-compose up -d
docker-compose exec -T nginx nginx -v
docker-compose exec -T nginx nginx -t
curl --fail-with-body "http://${HOST_IP}/health"
curl --fail-with-body "http://${HOST_IP}/"
docker-compose logs --tail=100 nginx
# 修改 conf.d 中配置后,检查成功才执行重载。
docker-compose exec -T nginx nginx -t && \
docker-compose exec -T nginx nginx -s reload
|
成功标准:版本 nginx/1.30.4,配置检测 syntax is ok、test is successful,/health 返回 HTTP 200 和 nginx-ok,首页显示 Nginx deployment OK。访问日志及错误日志通过 docker-compose logs 查看。
两个版本均提供单机及 3 节点仲裁队列部署。Compose 通过 .env 的 RABBITMQ_TAG 选择精确镜像,以下流程对两个版本分别执行即可。
| 版本 |
RABBITMQ_TAG |
单机目录 |
集群目录 |
| 3.13.6 |
3.13.6-management |
rabbitmq/3.13.6-management/standalone |
rabbitmq/3.13.6-management/quorum |
| 4.3.6 |
4.3.6-management |
rabbitmq/4.3.6-management/standalone |
rabbitmq/4.3.6-management/quorum |
选择一个版本:
1
2
3
|
export RABBITMQ_TAG=3.13.6-management
# 部署另一个版本时,改为下面这一行,并使用新的独立目录:
# export RABBITMQ_TAG=4.3.6-management
|
同一集群三个节点必须使用相同版本。此处是两套独立新部署方案,不是将 3.13 数据目录直接切换至 4.3 的升级方案。不要复用数据或混用镜像标签。
1
2
3
4
5
6
7
|
mkdir -p "$DEPLOY_ROOT/rabbitmq/$RABBITMQ_TAG/standalone"/{conf,data}
cd "$DEPLOY_ROOT/rabbitmq/$RABBITMQ_TAG/standalone"
printf 'RABBITMQ_TAG=%s\n' "$RABBITMQ_TAG" > .env
cat > conf/rabbitmq.conf <<'EOF'
listeners.tcp.default = 5672
management.tcp.port = 15672
EOF
|
创建 docker-compose.yaml:
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
|
name: rabbitmq-standalone
services:
rabbitmq:
image: rabbitmq:${RABBITMQ_TAG:?请设置精确版本}
hostname: rabbitmq
restart: unless-stopped
environment:
RABBITMQ_NODENAME: rabbit@rabbitmq
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- "5672:5672"
- "15672:15672"
volumes:
- ./conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./data:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"]
interval: 10s
timeout: 10s
retries: 30
start_period: 30s
networks: [rabbitmq-net]
networks:
rabbitmq-net:
driver: bridge
|
1
2
3
4
5
6
7
8
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d --wait --wait-timeout 360
docker-compose exec -T rabbitmq rabbitmq-diagnostics server_version
docker-compose exec -T rabbitmq rabbitmq-diagnostics check_port_connectivity
docker-compose exec -T rabbitmq rabbitmqctl list_users
curl --fail-with-body -u rabbitmq:rabbitmq \
"http://${HOST_IP}:15672/api/overview" | jq '{rabbitmq_version,cluster_name}'
|
管理控制台为 http://宿主机IP:15672,登录 rabbitmq/rabbitmq;AMQP 为 宿主机IP:5672,虚拟主机 /。默认用户环境变量仅首次空数据目录初始化生效。
RabbitMQ 集群负责节点协作,普通 classic 队列不会因建成集群自动拥有 3 副本。必须显式声明 x-queue-type=quorum,并验证成员数为 3。每条仲裁队列有自己的 leader 和 follower,整个 RabbitMQ 集群没有固定的全局主节点。3 副本多数派为 2,可容忍一个队列成员故障。参考:仲裁队列。
节点间通过 rabbitmq-net 的 DNS 名访问 4369(EPMD)、25672(Erlang 分布式通信),这两个端口在本单宿主机方案无需发布给外部客户端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
mkdir -p "$DEPLOY_ROOT/rabbitmq/$RABBITMQ_TAG/quorum"/conf
cd "$DEPLOY_ROOT/rabbitmq/$RABBITMQ_TAG/quorum"
mkdir -p data/rmq1 data/rmq2 data/rmq3
printf 'RABBITMQ_TAG=%s\n' "$RABBITMQ_TAG" > .env
cat >> .env <<'EOF'
RABBITMQ_ERLANG_COOKIE=middleware-rabbitmq-shared-cookie-2026
EOF
chmod 600 .env
cat > conf/rabbitmq.conf <<'EOF'
listeners.tcp.default = 5672
management.tcp.port = 15672
cluster_formation.peer_discovery_backend = classic_config
cluster_formation.classic_config.nodes.1 = rabbit@rmq1
cluster_formation.classic_config.nodes.2 = rabbit@rmq2
cluster_formation.classic_config.nodes.3 = rabbit@rmq3
EOF
|
固定 hostname、节点名和持久化目录,三个节点必须使用相同 Erlang cookie。此处采用内置静态节点发现,不依赖额外发现插件。参考:RabbitMQ 3.13 节点发现。
创建 docker-compose.yaml:
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
|
name: rabbitmq-quorum
x-rabbitmq: &rabbitmq-common
image: rabbitmq:${RABBITMQ_TAG:?请设置精确版本}
restart: unless-stopped
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "check_running"]
interval: 10s
timeout: 10s
retries: 30
start_period: 30s
networks: [rabbitmq-net]
x-environment: &rabbitmq-environment
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
RABBITMQ_ERLANG_COOKIE: ${RABBITMQ_ERLANG_COOKIE:?请设置共享 cookie}
services:
rmq1:
<<: *rabbitmq-common
hostname: rmq1
environment:
<<: *rabbitmq-environment
RABBITMQ_NODENAME: rabbit@rmq1
ports: ["5672:5672", "15672:15672"]
volumes:
- ./conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./data/rmq1:/var/lib/rabbitmq
rmq2:
<<: *rabbitmq-common
hostname: rmq2
environment:
<<: *rabbitmq-environment
RABBITMQ_NODENAME: rabbit@rmq2
ports: ["5673:5672", "15673:15672"]
volumes:
- ./conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./data/rmq2:/var/lib/rabbitmq
rmq3:
<<: *rabbitmq-common
hostname: rmq3
environment:
<<: *rabbitmq-environment
RABBITMQ_NODENAME: rabbit@rmq3
ports: ["5674:5672", "15674:15672"]
volumes:
- ./conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- ./data/rmq3:/var/lib/rabbitmq
networks:
rabbitmq-net:
driver: bridge
|
首次空目录启动按顺序进行,避免节点同时初始化为独立集群:
1
2
3
4
5
6
7
8
9
10
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d --wait --wait-timeout 360 rmq1
docker-compose up -d --wait --wait-timeout 360 rmq2
docker-compose up -d --wait --wait-timeout 360 rmq3
for node in rmq1 rmq2 rmq3; do
docker-compose exec -T "$node" rabbitmqctl cluster_status
done
curl --fail-with-body -u rabbitmq:rabbitmq \
"http://${HOST_IP}:15672/api/nodes" | jq '.[] | {name,running,partitions}'
|
必须在三个节点上看到同一个集群、3 个运行节点 rabbit@rmq1、rabbit@rmq2、rabbit@rmq3,无网络分区后,再声明队列。现有集群的重启使用 docker-compose up -d 同时恢复节点,不必强制沿用首次逐节点顺序;不应通过清空目录或 reset 来修复普通重启问题。
以下使用管理 HTTP API,%2F 为默认虚拟主机 / 的编码。队列名 deployment.quorum 应为新的测试队列;已有同名 classic 队列不能原地转换类型。API 用法参考:RabbitMQ HTTP API。
1
2
3
4
5
6
7
8
9
10
|
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X PUT \
"http://${HOST_IP}:15672/api/queues/%2F/deployment.quorum" \
-d '{"durable":true,"auto_delete":false,"arguments":{"x-queue-type":"quorum","x-quorum-initial-group-size":3}}'
# 查询 leader、成员及在线副本;以 quorum_status 的实时状态共同确认。
curl --fail-with-body -u rabbitmq:rabbitmq \
"http://${HOST_IP}:15672/api/queues/%2F/deployment.quorum" | \
jq '{name,type,durable,leader,node,members,online}'
docker-compose exec -T rmq1 rabbitmq-queues quorum_status --vhost / deployment.quorum
|
成功标准:队列类型 quorum、durable=true,3 个成员均在线;quorum_status 显示 1 个 leader 和 2 个 follower,稳定后日志提交/应用位置追上。显示 leader 的那一行节点名,就是这条队列当前的“主节点”,不能根据 rmq1 的启动顺序作判断。部分 HTTP 字段随版本展示不同,以 quorum_status 为准。
发布和消费一条持久消息:
1
2
3
4
5
6
7
8
9
|
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X POST \
"http://${HOST_IP}:15672/api/exchanges/%2F/amq.default/publish" \
-d '{"properties":{"delivery_mode":2},"routing_key":"deployment.quorum","payload":"quorum-ok","payload_encoding":"string"}'
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X POST \
"http://${HOST_IP}:15673/api/queues/%2F/deployment.quorum/get" \
-d '{"count":1,"ackmode":"ack_requeue_false","encoding":"auto","truncate":50000}'
|
预期发布返回 routed:true,从另一节点消费得到 payload: quorum-ok。该消费命令会确认并移除测试消息。生产客户端应启用发布确认、消费手动确认和断线重连,并配置多个 AMQP 地址;管理 API 验收不能代替应用客户端确认机制。
先发布一条尚未消费的测试消息,确保 routed:true:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X POST \
"http://${HOST_IP}:15672/api/exchanges/%2F/amq.default/publish" \
-d '{"properties":{"delivery_mode":2},"routing_key":"deployment.quorum","payload":"survive-leader-stop","payload_encoding":"string"}'
docker-compose exec -T rmq1 rabbitmq-queues quorum_status --vhost / deployment.quorum
# 提取实际 leader;如未获取到有效节点名,则停止自动演练并按上方 CLI 输出核对。
LEADER=$(curl --fail-with-body -sS -u rabbitmq:rabbitmq \
"http://${HOST_IP}:15672/api/queues/%2F/deployment.quorum" | jq -r '.leader // .node // empty')
case "$LEADER" in
rabbit@rmq1) TARGET=rmq1; SURVIVOR=rmq2; MGMT_PORT=15673 ;;
rabbit@rmq2) TARGET=rmq2; SURVIVOR=rmq1; MGMT_PORT=15672 ;;
rabbit@rmq3) TARGET=rmq3; SURVIVOR=rmq1; MGMT_PORT=15672 ;;
*) echo "未获取有效 leader,请人工核对 quorum_status"; TARGET= ;;
esac
if [ -n "$TARGET" ]; then
docker-compose stop "$TARGET"
docker-compose exec -T "$SURVIVOR" rabbitmq-queues quorum_status --vhost / deployment.quorum
fi
|
等待重新选举完成,再执行以下命令。若 TARGET 为空,不继续:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
: "${TARGET:?请先确认并停止实际 leader}"
curl --fail-with-body -u rabbitmq:rabbitmq \
"http://${HOST_IP}:${MGMT_PORT}/api/queues/%2F/deployment.quorum" | \
jq '{name,type,leader,node,members,online}'
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X POST \
"http://${HOST_IP}:${MGMT_PORT}/api/queues/%2F/deployment.quorum/get" \
-d '{"count":1,"ackmode":"ack_requeue_false","encoding":"auto","truncate":50000}'
# 原 leader 停止期间,继续验证可写。
curl --fail-with-body -u rabbitmq:rabbitmq \
-H 'content-type: application/json' -X POST \
"http://${HOST_IP}:${MGMT_PORT}/api/exchanges/%2F/amq.default/publish" \
-d '{"properties":{"delivery_mode":2},"routing_key":"deployment.quorum","payload":"after-failover","payload_encoding":"string"}'
docker-compose start "$TARGET"
docker-compose exec -T "$SURVIVOR" rabbitmq-queues quorum_status --vhost / deployment.quorum
|
验收:leader 转移到存活节点,两个成员在线时仍能消费 survive-leader-stop 并发布 after-failover(routed:true);恢复节点后重新有 3 个在线成员。不要同时停止两个节点,否则 3 副本队列失去多数派,停止读写是预期行为。单宿主机宕机仍会使三个节点同时不可用。
采用 MODE=standalone 的单节点内嵌 Derby 存储,不部署或依赖 MySQL。“不用数据库”在此指 不用外部数据库,Nacos 仍使用内嵌存储保存配置、用户等数据。挂载 /home/nacos/data 和 /home/nacos/logs,无需创建外部数据库表。
两个版本分别使用独立数据目录,不得混用 Derby 文件。镜像名与指定版本见 Nacos 官方下载页。
Nacos 2.4 起不再提供开箱即用的管理员默认密码。本文通过首次启动后的管理员初始化接口,将账号设置为 nacos/nacos,不是依赖不存在的默认密码环境变量。Token 密钥与登录密码是两回事;Token 密钥使用随机 48 字节的 Base64 编码。参考:Nacos 认证说明。
| 版本 |
控制台地址 |
SDK 服务地址 |
对客户端开放端口 |
| v2.5.4 |
http://宿主机IP:8848/nacos/ |
宿主机IP:8848 |
8848、9848 |
| v3.2.4 |
http://宿主机IP:8080/ |
宿主机IP:8848 |
8080、8848、9848 |
9848 是客户端 gRPC 端口;默认由 SDK 按服务端口 +1000 推导。本方案保持 8848/9848 原端口映射。3.x 的 8080 是控制台端口,不能作为 SDK 的 serverAddr。单节点不需要对外发布集群用 9849、7848。参考:Nacos Docker 快速开始。
1
2
3
4
5
6
|
mkdir -p "$DEPLOY_ROOT/nacos/v2.5.4"/{data,logs}
cd "$DEPLOY_ROOT/nacos/v2.5.4"
umask 077
printf 'NACOS_AUTH_TOKEN=%s\n' "$(openssl rand -base64 48 | tr -d '\n')" > .env
printf 'NACOS_AUTH_IDENTITY_VALUE=%s\n' "$(openssl rand -hex 24)" >> .env
umask 022
|
创建 docker-compose.yaml:
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
|
name: nacos-v2
services:
nacos:
image: nacos/nacos-server:v2.5.4
restart: unless-stopped
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: ""
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_TOKEN: ${NACOS_AUTH_TOKEN:?请先创建 .env}
NACOS_AUTH_IDENTITY_KEY: nacos-server-identity
NACOS_AUTH_IDENTITY_VALUE: ${NACOS_AUTH_IDENTITY_VALUE:?请先创建 .env}
JVM_XMS: 512m
JVM_XMX: 512m
JVM_XMN: 256m
TZ: Asia/Shanghai
ports:
- "8848:8848"
- "9848:9848"
volumes:
- ./data:/home/nacos/data
- ./logs:/home/nacos/logs
networks: [nacos-net]
networks:
nacos-net:
driver: bridge
|
启动及首次密码初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d
docker-compose logs --tail=200 nacos
curl --fail-with-body "http://${HOST_IP}:8848/nacos/v1/console/health/readiness"
# 等待服务启动成功后,仅对尚未创建管理员的全新实例执行一次。
curl --fail-with-body -X POST \
"http://${HOST_IP}:8848/nacos/v1/auth/users/admin" \
--data-urlencode 'password=nacos'
curl --fail-with-body -X POST \
"http://${HOST_IP}:8848/nacos/v1/auth/login" \
--data-urlencode 'username=nacos' --data-urlencode 'password=nacos'
|
初始化路由来自 Nacos 2.5.4 认证控制器。预期初始化成功、登录返回非空 accessToken,控制台可用 nacos/nacos 登录。已经初始化的实例会拒绝再次初始化,不应为重设密码删除数据。
功能验证:登录获取 Token,发布并读取配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
TOKEN=$(curl --fail-with-body -sS -X POST \
"http://${HOST_IP}:8848/nacos/v1/auth/login" \
--data-urlencode 'username=nacos' --data-urlencode 'password=nacos' | \
jq -er '.accessToken | select(length > 0)')
: "${TOKEN:?登录未获取 Token}"
curl --fail-with-body -X POST "http://${HOST_IP}:8848/nacos/v1/cs/configs" \
--data-urlencode "accessToken=${TOKEN}" \
--data-urlencode 'dataId=deployment-check' \
--data-urlencode 'group=DEFAULT_GROUP' \
--data-urlencode 'content=nacos-v2-ok' --data-urlencode 'type=text'
curl --fail-with-body -G "http://${HOST_IP}:8848/nacos/v1/cs/configs" \
--data-urlencode "accessToken=${TOKEN}" \
--data-urlencode 'dataId=deployment-check' --data-urlencode 'group=DEFAULT_GROUP'
|
预期发布 true,读取 nacos-v2-ok。重启后重新登录并读取同一配置,验证 data 挂载持久化。
先停掉占用 8848/9848 的 v2 实例,再部署 v3。以下目录和数据独立,不是版本升级。
1
2
3
4
5
6
|
mkdir -p "$DEPLOY_ROOT/nacos/v3.2.4"/{data,logs}
cd "$DEPLOY_ROOT/nacos/v3.2.4"
umask 077
printf 'NACOS_AUTH_TOKEN=%s\n' "$(openssl rand -base64 48 | tr -d '\n')" > .env
printf 'NACOS_AUTH_IDENTITY_VALUE=%s\n' "$(openssl rand -hex 24)" >> .env
umask 022
|
创建 docker-compose.yaml:
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
|
name: nacos-v3
services:
nacos:
image: nacos/nacos-server:v3.2.4
restart: unless-stopped
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: ""
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_ADMIN_ENABLE: "true"
NACOS_AUTH_CONSOLE_ENABLE: "true"
NACOS_AUTH_TOKEN: ${NACOS_AUTH_TOKEN:?请先创建 .env}
NACOS_AUTH_IDENTITY_KEY: nacos-server-identity
NACOS_AUTH_IDENTITY_VALUE: ${NACOS_AUTH_IDENTITY_VALUE:?请先创建 .env}
JVM_XMS: 512m
JVM_XMX: 512m
JVM_XMN: 256m
TZ: Asia/Shanghai
ports:
- "8080:8080"
- "8848:8848"
- "9848:9848"
volumes:
- ./data:/home/nacos/data
- ./logs:/home/nacos/logs
networks: [nacos-net]
networks:
nacos-net:
driver: bridge
|
启动及首次密码初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d
docker-compose logs --tail=200 nacos
curl --fail-with-body "http://${HOST_IP}:8080/"
# 等待启动完成后,仅对全新、未初始化管理员的实例执行。
curl --fail-with-body -X POST \
"http://${HOST_IP}:8848/nacos/v3/auth/user/admin" \
--data-urlencode 'password=nacos'
curl --fail-with-body -X POST \
"http://${HOST_IP}:8848/nacos/v3/auth/user/login" \
--data-urlencode 'username=nacos' --data-urlencode 'password=nacos'
|
预期日志显示启动成功并使用内嵌存储;初始化成功,登录返回 accessToken;控制台 http://宿主机IP:8080/ 可登录。控制台首页返回 200 只验证页面可访问,还需完成以下配置读写验证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
TOKEN=$(curl --fail-with-body -sS -X POST \
"http://${HOST_IP}:8848/nacos/v3/auth/user/login" \
--data-urlencode 'username=nacos' --data-urlencode 'password=nacos' | \
jq -er '.accessToken | select(length > 0)')
: "${TOKEN:?登录未获取 Token}"
curl --fail-with-body -X POST "http://${HOST_IP}:8848/nacos/v3/admin/cs/config" \
-H "accessToken: ${TOKEN}" \
--data-urlencode 'dataId=deployment-check' \
--data-urlencode 'groupName=DEFAULT_GROUP' \
--data-urlencode 'content=nacos-v3-ok' --data-urlencode 'type=text'
curl --fail-with-body -G "http://${HOST_IP}:8848/nacos/v3/client/cs/config" \
-H "accessToken: ${TOKEN}" \
--data-urlencode 'dataId=deployment-check' --data-urlencode 'groupName=DEFAULT_GROUP'
|
预期发布接口业务响应成功,读取响应包含 nacos-v3-ok,不能只判断 HTTP 200;有 code 字段时应为成功码 0。重启后重新登录再读取,确认配置仍存在。
1
2
3
4
5
|
docker-compose ps
docker-compose logs --tail=200 nacos
docker-compose exec -T nacos sh -c 'id; ls -ld /home/nacos/data /home/nacos/logs'
# 在客户端所在 Linux 机器执行,确认 SDK gRPC 端口可达:
timeout 3 bash -c "exec 3<>/dev/tcp/${HOST_IP}/9848"
|
若控制台可用但 SDK 失败,先检查 9848、防火墙、SDK 的 serverAddr 和用户名密码。若挂载目录不可写,通过指定镜像的 id 确认运行 UID/GID,再调整当前实例目录属主。若出现外部数据库连接错误,确认未设置 SPRING_DATASOURCE_PLATFORM=mysql 或遗留 MYSQL_* 参数。每次重启不得重新生成 .env 中的密钥。
账号 postgres,密码 postgresql,默认数据库 postgres。PostgreSQL 17 的数据目录挂载点为 /var/lib/postgresql/data;不要套用其他大版本不同的数据目录约定。参考:PostgreSQL 官方镜像、17.10 发布说明。
1
2
3
4
5
6
7
8
9
10
11
|
mkdir -p "$DEPLOY_ROOT/postgresql/17.10"/{conf,data}
cd "$DEPLOY_ROOT/postgresql/17.10"
cat > conf/postgresql.conf <<'EOF'
listen_addresses = '*'
port = 5432
max_connections = 100
shared_buffers = 128MB
password_encryption = 'scram-sha-256'
timezone = 'Asia/Shanghai'
log_timezone = 'Asia/Shanghai'
EOF
|
创建 docker-compose.yaml:
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
|
name: postgresql
services:
postgresql:
image: postgres:17.10
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgresql
POSTGRES_DB: postgres
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
TZ: Asia/Shanghai
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
ports: ["5432:5432"]
volumes:
- ./conf:/etc/postgresql:ro
- ./data:/var/lib/postgresql/data
shm_size: 256mb
healthcheck:
test: ["CMD-SHELL", "PGPASSWORD=postgresql psql -h 127.0.0.1 -U postgres -d postgres -tAc 'SELECT 1' | grep -qx 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 20s
networks: [postgresql-net]
networks:
postgresql-net:
driver: bridge
|
官方入口根据上述参数在空数据目录生成 pg_hba.conf,主机 TCP 连接使用 SCRAM 密码认证。环境变量只参与首次初始化;已有数据目录更改密码须通过 SQL,不能只改 POSTGRES_PASSWORD。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
docker-compose config --quiet
docker-compose pull
docker-compose up -d --wait --wait-timeout 360
docker-compose exec -T -e PGPASSWORD=postgresql postgresql \
psql -h "$HOST_IP" -p 5432 -U postgres -d postgres -v ON_ERROR_STOP=1 \
-c 'SELECT version(), current_database(), current_user;'
docker-compose exec -T -e PGPASSWORD=postgresql postgresql \
psql -h "$HOST_IP" -U postgres -d postgres -v ON_ERROR_STOP=1 <<'SQL'
CREATE TABLE IF NOT EXISTS deployment_check (id INTEGER PRIMARY KEY, note TEXT);
INSERT INTO deployment_check VALUES (1, 'postgresql-ok')
ON CONFLICT (id) DO UPDATE SET note=EXCLUDED.note;
SELECT * FROM deployment_check;
SHOW config_file;
SHOW data_directory;
SHOW hba_file;
SQL
|
成功标准:版本 PostgreSQL 17.10,数据库和用户均为 postgres,查询得到 1 / postgresql-ok;config_file 为 /etc/postgresql/postgresql.conf,数据和 HBA 文件位于持久化数据目录。
从外部客户端验证:
1
2
|
PGPASSWORD=postgresql psql -h "$HOST_IP" -p 5432 -U postgres -d postgres \
-v ON_ERROR_STOP=1 -c 'SELECT * FROM deployment_check;'
|
重启容器后重复查询确认数据仍在。所有章节的常规停机均在对应模式目录执行 docker-compose down;本文的 bind mount 数据不会因此删除,重新 up -d 应继续使用原数据及配置。清空数据目录属于重新初始化,不是普通重启步骤。