环境说明:MacBook(ARM架构)、本地Podman容器化部署

架构说明:结合生产环境最佳实践,本地部署将搭建3个Redis容器实例(一主二从)和3个Sentinel容器实例,用6个独立的容器实现Redis哨兵模式。

Podman容器部署细节:

  • 6个容器在同一个共享网络中(IP不同但能够相互访问)
  • 6个容器分别挂载到podman管理的持久化卷
  • Redis主节点分配固定的IP,从节点配置只读(读写分离)
  • 挂载Redis容器的配置文件到本地(只需要读,不涉及权限问题)
  • Sentinel容器基于Redis镜像,用dockerfile构建自己的镜像,把配置文件写入到镜像中,避免写权限问题
  • 在启动时指定资源大小,Redis容器 0.2C/100M,Sentinel 容器0.1C/20M(Mac本地环境,资源有限)
  • 日志文件可以通过podman logs查看,不需要挂载到本地

搭建官方推荐工具redisinsight容器访问Redis节点

使用密钥管理工具Vault(可选)

配置热重载(无需重启)(可选)

一、部署Redis(一主二从)

Redis主从模式,配从不配主。

(一)创建持久化卷和共享网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建持久化卷,把持久化卷交给podman去管理
for vol in redis_master_data redis_slave1_data redis_slave2_data sentinel1_data sentinel2_data sentinel3_data; do
podman volume create $vol
done

# 创建Redis哨兵模式共享网络
podman network create redis_sentinel_net

# (回退)删除持久化卷
for vol in redis_master_data redis_slave1_data redis_slave2_data sentinel1_data sentinel2_data sentinel3_data; do
podman volume rm $vol
done

# (回退)删除Redis哨兵模式共享网络
podman network rm redis_sentinel_net

(二)准备好配置文件

在本地目录创建配置文件,内容见附件(redis-master.conf、redis-slave.conf),标准配置文件(以 Redis6 为例)见附件(redis-default.conf)。

1
2
3
4
5
# 在本地目录创建配置文件
/Users/zoran/dev/podman/redis&sentinel
├── conf
│ ├── redis-master.conf
│ ├── redis-slave.conf

(三)部署

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
# 启动Redis主节点,分配固定IP 10.89.1.35
podman run -d \
--name redis-master \
-p 6379:6379 \
--ip 10.89.1.35 \
--restart always \
-v redis_master_data:/data \
-v ~/dev/podman/redis\&sentinel/conf/redis-master.conf:/etc/redis.conf:z \
--network redis_sentinel_net \
--cpus=0.2 \
--memory=100m \
-e TZ=Asia/Shanghai \
docker.io/arm64v8/redis:latest \
redis-server /etc/redis.conf

# 验证主节点IP
MASTER_IP=$(podman inspect -f '{{.NetworkSettings.Networks.redis_sentinel_net.IPAddress}}' redis-master)
echo "Master IP: $MASTER_IP"

# 测试主节点
podman exec -it redis-master /bin/bash
redis-cli

# 进入到redis命令行后需要鉴权
auth zoran.wang@redis!

# 查看身份
INFO replication

# 随便输入点命令玩玩
ping
set hello redis!
get hello

# 退出容器到宿主机
exit

# 启动Redis从节点,注意映射到宿主机的端口号有差异
podman run -d \
--name redis-slave1 \
-p 6389:6389 \
-v redis_slave1_data:/data \
-v ~/dev/podman/redis\&sentinel/conf/redis-slave.conf:/etc/redis.conf:z \
--network redis_sentinel_net \
--cpus=0.2 \
--memory=100m \
-e TZ=Asia/Shanghai \
docker.io/arm64v8/redis:latest \
redis-server /etc/redis.conf

podman run -d \
--name redis-slave2 \
-p 6399:6389 \
-v redis_slave2_data:/data \
-v ~/dev/podman/redis\&sentinel/conf/redis-slave.conf:/etc/redis.conf:z \
--network redis_sentinel_net \
--cpus=0.2 \
--memory=100m \
-e TZ=Asia/Shanghai \
docker.io/arm64v8/redis:latest \
redis-server /etc/redis.conf

(四)脚本(生产环境推荐)

以上的脚本可以用 DeepSeek 生成为 shell 脚本,脚本见附件(redis-master.sh、redis-slave.sh),可以实现生产环境的全自动部署。

(五)验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看容器状态是否为UP
podman ps -a

# 查看容器日志
podman logs -f redis-master
podman logs -f redis-slave1
podman logs -f redis-slave2

# 查看身份
podman exec redis-slave1 redis-cli -a 'zoran.wang@redis!' INFO replication

# 进入到从节点Redis客户端
podman exec -it redis-slave1 redis-cli -a 'zoran.wang@redis!'
# 查看刚才主节点存入的数据
keys *
get hello

# slave2同理

二、部署Sentinel哨兵(三个实例)

(一)准备好配置文件

新建配置文件(sentinel.conf),内容见附件(sentinel.conf)。

(二)哨兵的权限问题

  1. 文件权限问题:
    • 容器内的 Redis Sentinel 进程默认以 redis 用户(UID 999)运行。
    • 挂载的宿主机文件 ~/dev/podman/redis&sentinel/conf/sentinel.conf 未赋予 redis 用户足够的写权限。
      • 在MacOS系统中,podman本质上是个虚拟机,MacOS用户根本看不到虚拟机中的redis用户,属于不同层面,没办法赋予权限;
      • 而Linux系统不一样,可以理解成Linux系统本身就是一个容器,与podman其他容器是同级别的,所以权限正常就能写。
    • 当 Sentinel 尝试更新配置(如选举新主节点)时,会触发写入操作,导致权限错误。
  2. SELinux 限制(如果宿主机启用 SELinux):
    • 即使文件权限正确,SELinux 也可能阻止容器进程写入宿主机文件系统。

总结:需要理解容器的概念,容器中的进程对宿主机的文件很多情况只能在启动的时候读一次,不能进行写操作。

Linux解决方案

修改文件权限&调整安全策略即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 确保配置文件目录存在
mkdir -p ~/dev/podman/redis\&sentinel/conf/

# 修改文件所有者为容器内 redis 用户(UID 999)
sudo chown 999:999 ~/dev/podman/redis\&sentinel/conf/sentinel.conf

# 赋予读写权限
chmod 644 ~/dev/podman/redis\&sentinel/conf/sentinel.conf

# 调整SELinux标签
# 使用 :Z 重新标记文件
podman run ... \
-v ~/dev/podman/redis\&sentinel/conf/sentinel.conf:/etc/sentinel.conf:Z \
...

MacOS解决方案

MacBook上的podman实际上是运行在系统上的一个虚拟机,podman里面的容器无法对宿主机的文件进行写操作,只能在启动的时候读一次。

podman创建的数据卷是在虚拟机中的,我们可以把sentinel.conf挂载到数据卷sentinel_conf_data上,然后在创建容器时,用数据卷文件挂载取代本地文件挂载,sentinel进程(redis用户)就可以进行读写。

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
# ==========使用Podman数据卷解决权限问题,这里以一个节点为例==========

# 创建Sentinel配置文件专用数据卷
podman volume create sentinel_conf_data

# 拉取兼容ARM架构的镜像
podman pull docker.io/arm64v8/busybox:latest

# 复制配置文件到专用数据卷
podman run --rm\
-v ~/dev/podman/redis\&sentinel/conf:/source:ro \
-v sentinel_conf_data:/config \
docker.io/arm64v8/busybox:latest \
cp /source/sentinel.conf /config/sentinel.conf

# MacOS专属操作,可以用命令进入到podman虚拟机
podman machine ssh

# 在虚拟机内查看卷实际路径
sudo ls -l /var/lib/containers/storage/volumes/sentinel_conf_data

# 可以进入到该路径下进行复制、赋权等操作(也可以用临时容器赋权,但这种方式比较直接)
chmod -x sentinel_conf_data

podman run -d --name sentinel \
--network redis_sentinel_net \
-v sentinel_data:/data \
-v sentinel_conf_data:/etc/sentinel_conf_data:Z \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
docker.io/arm64v8/redis:latest \
redis-sentinel /etc/sentinel_conf_data/sentinel-node1.conf

如果需要部署三个 Sentinel 容器,需要三个独立的 sentinel.conf!!!Sentinel(Redis用户)会对自己的配置文件进行写操作,千万不能使用持久化卷中的同一份配置文件!

通用方案——Dockerfile

通过 Dockerfile 构建自定义 Sentinel 镜像,使用环境变量动态配置 Redis 主节点 IP、密码等可变参数,既能保证配置的版本控制,又能保持部署的灵活性。

由于篇幅较长,我们把这段单独移到第三节进行讲解。

三、云原生最佳实践——Dockerfile

(一)准备配置文件 sentinel.conf

创建配置文件模板,使用环境变量占位符:

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
# ==================== 基础配置 ====================
port ${SENTINEL_PORT}
# 容器中必须设为no
daemonize no
# 输出到标准输出,方便容器日志收集
logfile ""
# 持久化数据目录
dir "/data"

# ==================== 监控的主节点配置 ====================
# 格式:sentinel monitor <主节点名称> <IP> <端口> <仲裁数>
# sentinel monitor mymaster 10.89.1.35 6379 2
sentinel monitor ${MASTER_NAME} ${MYMASTER_IP} 6379 ${QUORUM}

# 主节点密码(必须与redis-master的requirepass一致)
# sentinel auth-pass mymaster zoran.wang@redis!
sentinel auth-pass ${MASTER_NAME} ${MYMASTER_PASSWORD}

# ==================== 故障判定参数 ====================
# 主节点无响应超过5000毫秒视为下线
sentinel down-after-milliseconds mymaster ${DOWN_AFTER_MS}

# 故障转移超时时间(毫秒)
sentinel failover-timeout ${MASTER_NAME} ${FAILOVER_TIMEOUT}

# 并行同步的新从节点数量
sentinel parallel-syncs ${MASTER_NAME} ${PARALLEL_SYNCS}

# ==================== 安全加固配置 ====================
# Sentinel自身密码(Redis 6.2+)
# 用户名
# sentinel sentinel-user default
sentinel sentinel-user ${SENTINEL_USERNAME}
# 密码
# sentinel sentinel-pass zoran.wang@sentinel!
sentinel sentinel-pass ${SENTINEL_PASSWORD}

# 禁止执行危险命令
# sentinel rename-command SHUTDOWN ""
# sentinel rename-command CONFIG "REDIS-CONFIG"

# 安全设置
sentinel deny-scripts-reconfig yes

# ==================== 高级调优参数 ====================
# 日志级别(notice适合生产环境)
loglevel notice
# 输出到标准输出,方便容器日志收集
logfile ""

# 通知脚本(可选)
# sentinel notification-script mymaster /scripts/notify.sh

# 客户端连接数限制
maxclients 10000

# 保护模式关闭(容器网络内部使用)
protected-mode no

(二)准备容器启动脚本entrypoint.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
#!/bin/bash

# 创建临时文件
temp_file=$(mktemp)

# 替换环境变量(不使用外部工具)
while IFS= read -r line
do
# 替换 ${VAR} 格式的环境变量
while [[ "$line" =~ (\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}) ]]; do
var_name="${BASH_REMATCH[2]}"
var_value="${!var_name}"
line="${line//${BASH_REMATCH[1]}/$var_value}"
done
echo "$line" >> "$temp_file"
done < /etc/redis/sentinel-default.conf

# 生成最终配置文件
mv "$temp_file" /etc/redis/sentinel.conf

# 确保配置文件可写
chown redis:redis /etc/redis/sentinel.conf
chmod u+w /etc/redis/sentinel.conf

# 启动 Sentinel
exec redis-sentinel /etc/redis/sentinel.conf

(三)创建 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Dockerfile
FROM docker.io/arm64v8/redis

# 设置环境变量默认值
ENV MYMASTER_IP=10.89.1.35
ENV MYMASTER_PASSWORD=zoran.wang@redis!
ENV SENTINEL_USERNAME=default
ENV SENTINEL_PASSWORD=zoran.wang@sentinel!
ENV SENTINEL_PORT=26379
ENV MASTER_NAME=mymaster
ENV QUORUM=2
ENV DOWN_AFTER_MS=5000
ENV FAILOVER_TIMEOUT=10000
ENV PARALLEL_SYNCS=1

# 复制配置文件和启动脚本
COPY conf/sentinel-default.conf /etc/redis/sentinel-default.conf
COPY entrypoint.sh /entrypoint.sh

# 设置权限
RUN chmod +x /entrypoint.sh

# 设置容器启动命令
ENTRYPOINT ["/entrypoint.sh"]

(四)构建镜像

1
2
3
4
podman build -t my-sentinel:1.0

# 构建成功之后查看镜像
podman images

(五)运行容器

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
# 映射端口号不同
podman run -d --name sentinel-node1 \
-p 26379:26379 \
-v sentinel1_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0

podman run -d --name sentinel-node2 \
-p 26389:26379 \
-v sentinel2_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0

podman run -d --name sentinel-node3 \
-p 26399:26379 \
-v sentinel3_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0

四、故障转移验证

(一)确认初始状态

1
2
3
4
5
6
7
8
9
# 查看当前主节点(通过任意 Sentinel)
podman exec sentinel-node1 redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

# 查看主节点信息
podman exec redis-master redis-cli -a 'zoran.wang@redis!' INFO replication

# 查看从节点状态(应显示连接到主节点)
podman exec redis-slave1 redis-cli -a 'zoran.wang@redis!' INFO replication | grep -E "role|master_host"
podman exec redis-slave2 redis-cli -a 'zoran.wang@redis!' INFO replication | grep -E "role|master_host"

(二)模拟主节点故障

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
# 查看 Sentinel 日志(观察故障检测过程)
podman logs -f sentinel-node1
podman logs -f sentinel-node2
podman logs -f sentinel-node3

# 查看从节点日志
podman logs -f redis-slave1

# 停止主节点容器
podman stop redis-master

# 过五秒(配置文件参数)查看从节点信息
podman exec redis-slave1 redis-cli -a 'zoran.wang@redis!' INFO replication
podman exec redis-slave2 redis-cli -a 'zoran.wang@redis!' INFO replication
# 发现redis-slave1的身份变成了master

# 查看当前主节点(通过任意 Sentinel)
podman exec sentinel-node1 redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 发现变成了redis-slave1的IP

# 启动(原)主节点
podman start redis-master
podman exec redis-master redis-cli -a 'zoran.wang@redis!' INFO replication
# 会发现,(原)主节点的身份成了slave

# 停止从节点,发现(原)主节点的身份恢复成了master
podman stop redis-slave1
podman start redis-slave1

(三)重新搭建哨兵模式

1
2
3
4
5
6
7
8
9
10
11
12
# 执行主节点和从节点部署脚本
sh redis-master.sh
sh redis-slave.sh

# 三台sentinel哨兵会自动选择Redis主节点,不需要重新启动容器
# (如果需要)删除三个Sentinel容器
for i in 1 2 3; do
podman stop sentinel-node${i}
done
for i in 1 2 3; do
podman rm sentinel-node${i}
done

五、官方可视化工具

1
2
3
4
5
6
7
8
9
# 安装网页工具redisinsight
podman volume create redisinsight_sentinel_data

podman run -d \
--name redisinsight-sentinel \
-p 5540:5540 \
-v redisinsight_sentinel_data:/db \
--network redis_sentinel_net \
docker.io/redislabs/redisinsight:latest

注意登录的时候,IP不是 localhost,而是下面的命令获取的IP(站在 redisinsight 的视角,与 redis-master 都属于同层容器,访问的是 podman 中 redis-master 容器的 IP)

1
2
# 获取主节点IP
podman exec sentinel-node1 redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

======附件======

文档树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
➜  redis&sentinel pwd
/Users/zoran/dev/podman/redis&sentinel
➜ redis&sentinel tree
.
├── conf
│   ├── redis-default.conf
│   ├── redis-master.conf
│   ├── redis-slave.conf
│   └── sentinel-default.conf
├── crt_redis.sh
├── crt_sentinel.sh
├── Dockerfile
├── entrypoint.sh
├── README.md
└── shell
├── redis-master.sh
└── redis-slave.sh

3 directories, 11 files

conf

redis-default.conf

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
################################## 基础配置 ###################################

# 绑定地址 (容器中建议注释掉或设为 0.0.0.0)
# bind 127.0.0.1 -::1
bind 0.0.0.0 # 容器内允许所有网络接口访问

# 监听端口 (默认6379)
port 6379

# 保护模式 (生产环境应禁用)
protected-mode no # 容器在内部网络运行时关闭

# 守护进程模式 (容器中必须设为 no)
daemonize no # 容器需要前台运行

# 进程PID文件 (容器中通常不需要)
# pidfile /var/run/redis.pid

# 客户端访问密码
requirepass "zoran.wang@redis!" # 主节点密码

# 主从复制密码
masterauth "zoran.wang@redis!" # 必须与requirepass一致

################################# 持久化配置 ###################################

# RDB快照配置 - 生产环境推荐
save 900 1 # 900秒(15分钟)内至少1个键变化则保存
save 300 10 # 300秒(5分钟)内至少10个键变化
save 60 10000 # 60秒内至少10000个键变化

# 后台保存出错时停止写入 (生产环境必须开启)
stop-writes-on-bgsave-error yes

# RDB文件压缩 (推荐开启)
rdbcompression yes

# RDB校验和 (推荐开启)
rdbchecksum yes

# RDB文件名
dbfilename dump.rdb

# 工作目录 (挂载卷位置)
dir /data # Podman卷挂载点

# AOF持久化配置 (推荐同时开启RDB+AOF)
appendonly yes # 开启AOF

# AOF文件名
appendfilename "appendonly.aof"

# AOF同步策略 (生产环境折衷方案)
appendfsync everysec # 每秒同步,兼顾性能与安全

# AOF重写时不同步 (提升性能)
no-appendfsync-on-rewrite no

# AOF自动重写阈值
auto-aof-rewrite-percentage 100 # 增长100%时重写
auto-aof-rewrite-min-size 64mb # 最小64MB触发重写

# AOF加载时错误处理 (生产环境必须)
aof-load-truncated yes

############################## 主从复制配置 ###################################

# 主节点密码 (容器间通信推荐设置)
# masterauth "your_strong_password"

# 从节点复制配置 (启动时动态设置)
# replicaof <masterip> <masterport>

# 从节点只读模式 (推荐开启)
replica-read-only yes

# 复制超时时间 (容器网络稳定可降低)
repl-timeout 60

# 无盘复制 (容器环境推荐关闭)
repl-diskless-sync no

# 后台传输延迟 (默认关闭)
repl-disable-tcp-nodelay no

# 复制积压缓冲区大小 (提升故障转移能力)
repl-backlog-size 128mb # 哨兵模式推荐值

# 积压缓冲区保留时间
repl-backlog-ttl 3600 # 1小时

############################### 安全配置 ######################################

# 访问密码 (生产环境必须设置)
requirepass "your_strong_password_here" # 替换为强密码

# 危险命令重命名 (生产环境必须)
rename-command FLUSHDB "" # 禁用FLUSHDB
rename-command FLUSHALL "" # 禁用FLUSHALL
rename-command CONFIG "REDIS-CONFIG" # 重命名CONFIG

# 最大客户端连接数
maxclients 10000 # 根据容器资源调整

############################### 资源限制 ######################################

# 最大内存限制 (容器环境必须设置)
maxmemory 2gb # 设置为容器内存限制的80-90%

# 内存淘汰策略 (推荐allkeys-lru)
maxmemory-policy allkeys-lru

# 关闭透明大页 (容器中通常不需要)
# disable-thp yes

############################### 日志配置 ######################################

# 日志级别 (生产环境推荐notice)
loglevel notice

# 日志文件 (容器中推荐输出到stdout)
# logfile "/var/log/redis/redis.log"
logfile "" # 输出到标准输出

# 数据库数量
databases 16

############################## 慢查询日志 #####################################

# 慢查询阈值 (单位微秒)
slowlog-log-slower-than 10000 # 10毫秒

# 慢查询记录长度
slowlog-max-len 128 # 保留128条记录

############################ 事件通知 ########################################

# 键空间通知 (按需开启)
# notify-keyspace-events ""

########################### 高级配置 #########################################

# 客户端超时 (容器网络稳定可降低)
timeout 30 # 30秒无活动断开连接

# TCP keepalive (推荐开启)
tcp-keepalive 300 # 5分钟

# 协议版本 (推荐新版)
protocol-version 3 # RESP3协议

# 内核OOM控制 (容器环境重要)
oom-score-adj no # 不调整OOM分数

# 动态HZ (推荐开启)
dynamic-hz yes

# 子进程调度 (Linux容器推荐)
linux-tcp-backlog 511 # 提高TCP队列
replica-ignore-maxmemory yes # 从节点忽略maxmemory

########################### 容器优化参数 ######################################

# 禁用THP (透明大页)
disable-thp yes # 避免内存分配延迟

# 后台进程优先级
bgrewriteaof-schedule-delay 100 # AOF重写延迟(ms)
bgsave-schedule-delay 100 # RDB保存延迟

# 容器内时钟源优化 (解决时间跳变问题)
slowclock-source tsc # 使用TSC时钟源


########################### 未使用但重要的参数 #################################

# 集群模式 (哨兵模式中不需要)
# cluster-enabled no

# TLS配置 (内网容器通信通常不需要)
# tls-port 6379
# tls-cert-file redis.crt
# tls-key-file redis.key

# 系统控制 (容器中由Podman管理)
# syslog-enabled no
# syslog-ident redis

# 多线程IO (Redis 6+特性,测试环境评估)
# io-threads 4
# io-threads-do-reads no

# 延迟监控 (调试使用)
# latency-monitor-threshold 0

# 子进程CPU亲和性 (物理机优化)
# set-cpu-affinity 0,2,4,6

# 内存碎片整理 (高负载环境评估)
# activedefrag yes
# active-defrag-ignore-bytes 100mb
# active-defrag-threshold-lower 10

redis-master.conf

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
# 容器内允许所有网络接口访问
bind 0.0.0.0

# 监听端口 (默认6379)
port 6379

# 客户端访问密码
requirepass "zoran.wang@redis!"

# 主从复制密码,必须与requirepass密码一致
masterauth "zoran.wang@redis!"

# 保护模式 (生产环境应禁用)
# 容器在内部网络运行时关闭
protected-mode no

# 工作目录 (挂载卷位置)
dir /data
# Podman卷挂载点

# 日志文件 (容器中推荐输出到stdout)
# 输出到标准输出
logfile ""

# 守护进程模式,容器是否前台运行 (容器中必须设为no)
daemonize no

# 进程PID文件 (容器中通常不需要)
# pidfile /var/run/redis.pid

# RDB快照配置 - 生产环境推荐
# 900秒(15分钟)内至少1个键变化则保存
save 900 1
# 300秒(5分钟)内至少10个键变化
save 300 10
# 60秒内至少10000个键变化
save 60 10000

# 后台保存出错时停止写入 (生产环境必须开启)
stop-writes-on-bgsave-error yes

# RDB文件压缩 (推荐开启)
rdbcompression yes

# RDB校验和 (推荐开启)
rdbchecksum yes

# RDB文件名
dbfilename dump.rdb

# AOF持久化配置 (推荐同时开启RDB+AOF)
appendonly yes

# AOF文件名
appendfilename "appendonly.aof"

# AOF同步策略 (生产环境折衷方案)
# 每秒同步,兼顾性能与安全
appendfsync everysec

# AOF重写时不同步 (提升性能)
no-appendfsync-on-rewrite no

# AOF自动重写阈值
# 增长100%时重写
auto-aof-rewrite-percentage 100

# 最小64MB触发重写
auto-aof-rewrite-min-size 64mb

# AOF加载时错误处理 (生产环境必须)
aof-load-truncated yes

redis-slave.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 前面的内容和主节点配置一样,复制过来!!!!!!!

# =======从节点新增配置========

# 从节点复制配置 (启动时动态设置)
replicaof redis-master 6379

# 从节点只读模式 (推荐开启)
replica-read-only yes

# 复制超时时间 (容器网络稳定可降低)
repl-timeout 60

# 无盘复制 (容器环境推荐关闭)
repl-diskless-sync no

sentinel-default.conf

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
# ==================== 基础配置 ====================
port ${SENTINEL_PORT}
# 容器中必须设为no
daemonize no
# 输出到标准输出,方便容器日志收集
logfile ""
# 持久化数据目录
dir "/data"

# ==================== 监控的主节点配置 ====================
# 格式:sentinel monitor <主节点名称> <IP> <端口> <仲裁数>
# sentinel monitor mymaster 10.89.1.35 6379 2
sentinel monitor ${MASTER_NAME} ${MYMASTER_IP} 6379 ${QUORUM}

# 主节点密码(必须与redis-master的requirepass一致)
# sentinel auth-pass mymaster zoran.wang@redis!
sentinel auth-pass ${MASTER_NAME} ${MYMASTER_PASSWORD}

# ==================== 故障判定参数 ====================
# 主节点无响应超过5000毫秒视为下线
sentinel down-after-milliseconds mymaster ${DOWN_AFTER_MS}

# 故障转移超时时间(毫秒)
sentinel failover-timeout ${MASTER_NAME} ${FAILOVER_TIMEOUT}

# 并行同步的新从节点数量
sentinel parallel-syncs ${MASTER_NAME} ${PARALLEL_SYNCS}

# ==================== 安全加固配置 ====================
# Sentinel自身密码(Redis 6.2+)
# 用户名
# sentinel sentinel-user default
sentinel sentinel-user ${SENTINEL_USERNAME}
# 密码
# sentinel sentinel-pass zoran.wang@sentinel!
sentinel sentinel-pass ${SENTINEL_PASSWORD}

# 禁止执行危险命令
# sentinel rename-command SHUTDOWN ""
# sentinel rename-command CONFIG "REDIS-CONFIG"

# 安全设置
sentinel deny-scripts-reconfig yes

# ==================== 高级调优参数 ====================
# 日志级别(notice适合生产环境)
loglevel notice
# 输出到标准输出,方便容器日志收集
logfile ""

# 通知脚本(可选)
# sentinel notification-script mymaster /scripts/notify.sh

# 客户端连接数限制
maxclients 10000

# 保护模式关闭(容器网络内部使用)
protected-mode no

crt_redis.sh

1
2
3
4
# bin/bash
cd shell
sh redis-master.sh
sh redis-slave.sh

crt_sentinel.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
# 映射端口号不同
podman run -d --name sentinel-node1 \
-p 26379:26379 \
-v sentinel1_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0;

podman run -d --name sentinel-node2 \
-p 26389:26379 \
-v sentinel2_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0;

podman run -d --name sentinel-node3 \
-p 26399:26379 \
-v sentinel3_data:/data \
--network redis_sentinel_net \
--cpus=0.1 \
--memory=20m \
-e TZ=Asia/Shanghai \
my-sentinel:1.0;

Dockerfile

注意首字母大写

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
# Dockerfile
FROM docker.io/arm64v8/redis

# 设置环境变量默认值
ENV MYMASTER_IP=10.89.1.35
ENV MYMASTER_PASSWORD=zoran.wang@redis!
ENV SENTINEL_USERNAME=default
ENV SENTINEL_PASSWORD=zoran.wang@sentinel!
ENV SENTINEL_PORT=26379
ENV MASTER_NAME=mymaster
ENV QUORUM=2
ENV DOWN_AFTER_MS=5000
ENV FAILOVER_TIMEOUT=10000
ENV PARALLEL_SYNCS=1

# 复制配置文件和启动脚本
COPY conf/sentinel-default.conf /etc/redis/sentinel-default.conf
COPY entrypoint.sh /entrypoint.sh

# 设置权限
RUN chmod +x /entrypoint.sh

# 设置容器启动命令
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.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
#!/bin/bash

# 创建临时文件
temp_file=$(mktemp)

# 替换环境变量(不使用外部工具)
while IFS= read -r line
do
# 替换 ${VAR} 格式的环境变量
while [[ "$line" =~ (\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}) ]]; do
var_name="${BASH_REMATCH[2]}"
var_value="${!var_name}"
line="${line//${BASH_REMATCH[1]}/$var_value}"
done
echo "$line" >> "$temp_file"
done < /etc/redis/sentinel-default.conf

# 生成最终配置文件
mv "$temp_file" /etc/redis/sentinel.conf

# 确保配置文件可写
chown redis:redis /etc/redis/sentinel.conf
chmod u+w /etc/redis/sentinel.conf

# 启动 Sentinel
exec redis-sentinel /etc/redis/sentinel.conf

README.md

1
2
3
4
5
6
7
8
# Redis哨兵模式部署大胜利!

部署步骤:

1. 检查并执行`crt_redis.sh`,会自动创建 Redis 一主二从容器;
2. 执行`podman images`查看镜像,确认存在`my-sentinel`镜像;
3. (如果没有)执行`podman build -t my-sentinel:1.0`构建镜像;
4. 检查并执行`crt_sentinel.sh`,会创建 Sentinel 三个哨兵容器。

shell

redis-master.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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/bin/bash

# Redis 主节点部署脚本
# 作者:王卓
# 日期:$(date +%Y-%m-%d)

set -euo pipefail # 启用严格错误检查

# 配置参数
CONTAINER_NAME="redis-master"
NETWORK_NAME="redis_sentinel_net"
VOLUME_NAME="redis_master_data"
CONFIG_PATH="$HOME/dev/podman/redis&sentinel/conf/redis-master.conf"
IMAGE="docker.io/arm64v8/redis:latest"
CPU="0.2"
MAX_SIZE="100M"
PORT=6379

# 检查必要文件是否存在
check_dependencies() {
# 检查配置文件是否存在
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "错误:Redis 配置文件不存在: $CONFIG_PATH"
exit 1
fi

# 检查镜像是否可用
if ! podman image exists "$IMAGE"; then
echo "正在拉取 Redis 镜像..."
podman pull "$IMAGE" || {
echo "错误:无法拉取 Redis 镜像"
exit 1
}
fi
}

# 创建网络(如果不存在)
create_network() {
if ! podman network exists "$NETWORK_NAME"; then
echo "创建网络 $NETWORK_NAME..."
podman network create "$NETWORK_NAME"
fi
}

# 主部署函数
deploy_redis_master() {
echo "正在部署 Redis 主节点..."

podman run -d \
--name "$CONTAINER_NAME" \
--ip 10.89.1.35 \
-p ${PORT}:6379 \
--restart always \
-v "${VOLUME_NAME}:/data" \
-v "${CONFIG_PATH}:/etc/redis.conf:z" \
--network "$NETWORK_NAME" \
--cpus=${CPU} \
--memory=${MAX_SIZE} \
-e TZ=Asia/Shanghai \
"$IMAGE" \
redis-server /etc/redis.conf

echo "✅ Redis 主节点已成功部署"
echo "容器名称: $CONTAINER_NAME"
echo "访问端口: $PORT"
}

# 执行部署
main() {
check_dependencies
create_network

# 检查容器是否已存在
if podman container exists "$CONTAINER_NAME"; then
read -rp "容器 $CONTAINER_NAME 已存在,是否重新创建?[y/N] " response
if [[ "$response" =~ ^[Yy]$ ]]; then
echo "删除现有容器..."
podman rm -f "$CONTAINER_NAME"
else
echo "操作已取消"
exit 0
fi
fi

deploy_redis_master
}

# 执行主函数
main

# 获取主节点IP
IP=$(podman inspect -f '{{.NetworkSettings.Networks.redis_sentinel_net.IPAddress}}' redis-master)
echo "容器内IP: $IP"

redis-slave.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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/bin/bash

# Redis 从节点部署脚本
# 支持部署多个从节点实例
# 修复了 POSIX shell 兼容性问题

set -euo pipefail # 启用严格错误检查

# 全局配置
NETWORK_NAME="redis_sentinel_net"
IMAGE="docker.io/arm64v8/redis:latest"
CONFIG_BASE_DIR="$HOME/dev/podman/redis&sentinel/conf"

# 从节点配置 - 使用索引数组代替关联数组
SLAVE_NAMES=("redis-slave1" "redis-slave2")
SLAVE_VOLUMES=("redis_slave1_data" "redis_slave2_data")
SLAVE_CONFIGS=("redis-slave.conf" "redis-slave.conf")
SLAVE_PORTS=("6389" "6399")

# 检查依赖项
check_dependencies() {
# 检查镜像是否可用
if ! podman image exists "$IMAGE"; then
echo "正在拉取 Redis 镜像..."
podman pull "$IMAGE" || {
echo "错误:无法拉取 Redis 镜像"
exit 1
}
fi

# 检查配置目录
if [[ ! -d "$CONFIG_BASE_DIR" ]]; then
echo "错误:配置目录不存在 $CONFIG_BASE_DIR"
exit 1
fi
}

# 创建网络(如果不存在)
create_network() {
if ! podman network exists "$NETWORK_NAME"; then
echo "创建网络 $NETWORK_NAME..."
podman network create "$NETWORK_NAME"
fi
}

# 检查容器是否存在并处理
handle_existing_container() {
local container_name=$1

if podman container exists "$container_name"; then
read -rp "容器 $container_name 已存在,是否重新创建?[y/N] " response
if [[ "$response" =~ ^[Yy]$ ]]; then
echo "删除现有容器 $container_name..."
podman rm -f "$container_name"
return 0
else
echo "跳过 $container_name 部署"
return 1
fi
fi
return 0
}

# 部署单个从节点
deploy_redis_slave() {
local index=$1
local name=${SLAVE_NAMES[$index]}
local volume=${SLAVE_VOLUMES[$index]}
local config=${SLAVE_CONFIGS[$index]}
local port=${SLAVE_PORTS[$index]}

local config_path="$CONFIG_BASE_DIR/$config"

# 检查配置文件
if [[ ! -f "$config_path" ]]; then
echo "警告:配置文件不存在 $config_path,使用默认配置"
config_path="/dev/null" # 使用镜像内默认配置
fi

echo "正在部署从节点 $name..."

podman run -d \
--name "$name" \
-p "${port}:6379" \
-v "${volume}:/data" \
-v "${config_path}:/etc/redis.conf:z" \
--network "$NETWORK_NAME" \
--cpus=0.2 \
--memory=100m \
-e TZ=Asia/Shanghai \
"$IMAGE" \
redis-server /etc/redis.conf

echo "✅ Redis 从节点 $name 已部署"
echo " - 数据卷: $volume"
echo " - 外部端口: $port"
echo " - 配置文件: $config_path"
}

# 主部署函数
deploy_all_slaves() {
for index in "${!SLAVE_NAMES[@]}"; do
echo ""
echo "===== 处理从节点 ${SLAVE_NAMES[$index]} ====="

if handle_existing_container "${SLAVE_NAMES[$index]}"; then
deploy_redis_slave "$index"
fi
done
}

# 主执行流程
main() {
echo "====== 开始部署 Redis 从节点 ======"
check_dependencies
create_network
deploy_all_slaves
echo ""
echo "====== 部署完成 ======"
podman ps --filter "name=redis-slave*" --format "table {{.Names}}\t{{.Ports}}\t{{.Status}}"
}

# 执行主函数
main