Docker 下搭建 Redis 集群,听起来就是拉镜像、起容器、敲一条 create 命令的事,但真正把三主三从跑起来,再经历过一次故障切换和扩容,才知道里面有不少细节是官方文档不会明确告诉你的。我会把一条完整的实操链路走完:网络怎么规划、配置文件怎么写、容器怎么起、集群怎么创建、槽位怎么迁移、节点挂了怎么处理,最后再整理一份我自己踩过的坑清单。如果你正准备用 Docker 搭一套 Redis Cluster,无论是学习、演示还是小规模生产,这篇应该能让你少走很多弯路。
1. 先把架构想清楚:原生集群模式还是哨兵模式
1.1 两种“集群”到底差在哪
先说结论:如果你的目标是数据能分片、写性能可以横向扩展、容量不够时能加节点,那应该选 Redis Cluster 原生集群模式;如果你的目标只是主库挂了不丢可用性,主从加哨兵就够了。很多人在部署时容易把哨兵也叫成“集群”,但哨兵模式下所有数据都是一份逻辑数据,主节点是唯一写入点,单机内存上限就是整个集群的上限。数据量一旦超过一台机器,你迟早还是要回头面对分片问题。
哨兵模式的运维逻辑更简单,它需要额外跑一组 Sentinel 进程来监控主从节点,主挂自动选新主,客户端感知也要做一层封装。但写性能无法水平扩展,所以它更适合数据量不大、对可用性有要求、但不需要横向扩容的场景。Cluster 模式把分片和故障转移都内置在 Redis 自身,节点之间通过 gossip 协议互相通信,没有独立的监控组件,不过它对网络质量更敏感,客户端驱动也必须支持 Cluster 协议。放到 Docker 场景下,Cluster 模式反而更容易部署,因为每个节点都是同一个镜像,差异只在配置和 IP。
1.2 三主三从的最小生产拓扑
Redis Cluster 有一个硬性约束:至少要有三个主节点。这里的原因是总共有 16384 个哈希槽,主节点负责把槽分配出去,少于三个主节点时要么部分槽无法覆盖,要么单个节点承担的压力过于集中,官方在创建集群时干脆直接拒绝少于三个主节点的方案。
生产环境一般会再加冗余,所以我这次直接按三主三从来搭。三个主节点负责槽的读写请求,三个从节点分别作为三个主的备份。任意一个主节点挂掉,对应从节点会被投票提升为主,集群继续对外服务。如果只是在一台机器上做演示,六个容器共享同一个 Docker 引擎,资源争抢不可避免,但这不影响验证集群逻辑。
内存规划别拍脑袋。每个节点除了数据本身,还要留出 Replication Backlog、AOF 缓冲区、操作系统页缓存和 RDB 子进程 fork 时的临时占用。我习惯给每个容器加 2GB 内存配额,并在配置里限制 maxmemory 为 1.5GB,让 Redis 在接近上限前触发淘汰策略,而不是被 Docker 直接 OOM 掉。如果你只是本地测试,降到 512MB 也能跑,但写入数据量别太大。
2. 网络与目录规划:这一步直接决定后面顺不顺
2.1 为什么不能直接 docker run 就完事
很多人习惯docker run -p 6379:6379 redis把端口映射到宿主机,这在单节点 Redis 下没问题,但 Cluster 模式下会埋坑。Redis Cluster 节点之间不仅用客户端访问端口通信,还会额外开一个集群总线端口,默认是数据端口加 10000,6379 对应 16379。如果你做了端口映射,宿主机的数据端口变成了 7001,那总线端口就应该是 17001 而非 16379;要是只映射了数据端口,节点间连不上总线端口,创建集群时会提示Failed to connect to the bus port。
更稳妥的做法是创建一个 Docker 自定义网络,给每个节点分配固定 IP,容器内部统一用 6379。自定义 bridge 网络的好处很多:容器间有独立 DNS,隔离性比默认网络更好,外部设备和容器不会直接在同一个二层网络里碰面。固定 IP 的最大意义是让节点身份稳定,容器重启后地址不变,集群节点的握手和 gossip 不会因为 IP 漂移而断裂。
2.2 创建网络和宿主机目录
创建一个网段,规划好网关和节点 IP,避免和宿主机现有网段冲突:
docker network create --driver bridge --subnet=172.21.0.0/16 --gateway=172.21.0.1 redis-cluster-net然后在宿主机准备六个节点的配置目录和数据目录:
mkdir -p /opt/redis-cluster/node-{1..6}/{conf,data}我的 IP 规划如下,固定下来后面所有命令都用这张表:
| 容器名 | 固定 IP | 数据端口 | 总线端口 | 数据目录 |
|---|---|---|---|---|
| redis-node-1 | 172.21.0.2 | 6379 | 16379 | /opt/redis-cluster/node-1/data |
| redis-node-2 | 172.21.0.3 | 6379 | 16379 | /opt/redis-cluster/node-2/data |
| redis-node-3 | 172.21.0.4 | 6379 | 16379 | /opt/redis-cluster/node-3/data |
| redis-node-4 | 172.21.0.5 | 6379 | 16379 | /opt/redis-cluster/node-4/data |
| redis-node-5 | 172.21.0.6 | 6379 | 16379 | /opt/redis-cluster/node-5/data |
| redis-node-6 | 172.21.0.7 | 6379 | 16379 | /opt/redis-cluster/node-6/data |
容器内部端口全部用 6379 和 16379,相关逻辑最简单。如果后续要允许宿主机外部程序访问,再单独映射端口并配合 announce 参数,这个在第 3 章会说明。
2.3 镜像选型:为什么我不用 alpine
Redis 官方镜像推荐redis:7.2-alpine,体积小,但里面缺少一些网络排查工具,比如 netstat、tcpdump,你想在容器里看监听端口和抓包都很麻烦。我平时用redis:7.2,也就是基于 Debian 的镜像,体积大一点,但可调试性和生产兼容性更好。Redis 6.2 和 7.2 在集群命令上差别不大,新项目直接上 7.2 就好。
3. 配置文件与容器启动
3.1 一份可复用的 redis.conf
六个节点的配置文件除了将来数据目录不同之外,内容可以完全一致。我直接通过脚本生成,方便批量修改:
for i in {1..6}; do cat > /opt/redis-cluster/node-$i/conf/redis.conf <<EOF port 6379 bind 0.0.0.0 protected-mode no cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes appendfilename appendonly.aof dir /data EOF done逐行解释一下关键参数。cluster-enabled yes是让 Redis 以集群模式启动,少了它后面创建集群直接报错。cluster-config-file nodes.conf是每个节点自行维护的集群状态文件,记录当前节点 ID、其他节点地址和槽位归属,这个文件一定要放在持久化目录里,容器重建后如果丢了,Redis 会把自己当成全新节点。cluster-node-timeout 5000是节点不可达多久判定为 fail 的超时时间,单位毫秒。太短容易误判,太长故障转移会慢,我一般用 5000 到 15000 之间。
appendonly yes开启 AOF 持久化,集群里主从切换后,新主节点需要从日志恢复数据,所以持久化不能省。dir /data指向数据目录,配合 Docker 挂载实现容器重建不丢数据。protected-mode no是因为容器内通过自定义网络访问,这个安全边界已经靠 Docker 隔离,但如果本机还映射了宿主机端口,并且没有设置密码,一定要小心被外部扫描到。
如果生产环境要加密码,每个节点加这两行:
requirepass yourpass masterauth yourpass只加 requirepass 不加 masterauth 是常见错误,主从复制和集群内部通信都会失败。
3.2 用 docker run 启动六个节点
注意一点:按照前面的规划,容器间通信走自定义网络,不需要映射宿主机端口。启动第一个节点:
docker run -d --name redis-node-1 \ --network redis-cluster-net \ --ip 172.21.0.2 \ --memory 2g \ --ulimit nofile=65535:65535 \ -v /opt/redis-cluster/node-1/conf/redis.conf:/etc/redis/redis.conf:ro \ -v /opt/redis-cluster/node-1/data:/data \ redis:7.2 \ redis-server /etc/redis/redis.conf后面五个节点照葫芦画瓢,改容器名、IP 和目录:
for i in 2 3 4 5 6; do docker run -d --name redis-node-$i \ --network redis-cluster-net \ --ip 172.21.0.$((i+1)) \ --memory 2g \ --ulimit nofile=65535:65535 \ -v /opt/redis-cluster/node-$i/conf/redis.conf:/etc/redis/redis.conf:ro \ -v /opt/redis-cluster/node-$i/data:/data \ redis:7.2 \ redis-server /etc/redis/redis.conf done这里的--ulimit nofile是很多人忽略的。Docker 默认文件描述符上限可能不足以支撑大量客户端连接,集群模式下节点间本身也有不少长连接,提前拉高限制更稳妥。
如果你更习惯 Docker Compose,也可以写成版本化的 compose 文件,本质参数一样,关键是 network 段要显式指定ipv4_address,并声明一个自定义子网。手动docker run的好处是节点名、IP 和目录之间的关系一眼就能看明白,所以我这篇主要用命令方式讲。
3.3 启动后自检,不要急着建集群
六个容器都起来后,先进第一个容器确认能连通:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 ping返回 PONG 就说明 Redis 进程活着。再看日志:
docker logs redis-node-1 --tail 20第一次启动时出现No cluster configuration found, using 'nodes.conf'是正常的,不是报错。但如果出现反复的Possible node failure或者Unable to connect to the bus port,先停在这里排查网络,不要继续创建集群。
4. 组建集群:核心命令和验证
4.1 一条命令创建六个节点
在任意一个节点容器里执行创建命令。这里我用 redis-node-1 发起:
docker exec -it redis-node-1 redis-cli --cluster create \ 172.21.0.2:6379 172.21.0.3:6379 172.21.0.4:6379 \ 172.21.0.5:6379 172.21.0.6:6379 172.21.0.7:6379 \ --cluster-replicas 1--cluster-replicas 1的意思是每个主节点配一个从节点。执行后 redis-cli 会把槽位分配方案和主从对应关系打印出来,问你确认。输入 yes 回车。它会自动完成节点握手、槽位分配和主从绑定。
如果提示Node 172.21.0.2:6379 is not empty,多半是 data 目录里有上一次运行留下的 nodes.conf 或 AOF 文件。清空该节点数据目录再重启容器:
docker rm -f redis-node-1 rm -rf /opt/redis-cluster/node-1/data # 然后重新执行 docker run如果多个节点同时报 not empty,可能是这些节点之前已经被初始化过,还在同一个集群配置里。用redis-cli --cluster check可以看当前状态,不要急着盲目清数据。
4.2 验证集群状态
集群创建成功后,执行:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 cluster info两个重点指标:cluster_state:ok和cluster_slots_assigned:16384。只要有一个槽没人负责,集群就会变成 fail 状态,拒绝部分读写请求。
再看节点角色:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 cluster nodes输出会有六个节点的 ID、IP、端口、角色和槽位区间。主节点形如master - 0 ... connected 0-5460,从节点会显示slave <主节点ID>。
4.3 客户端连接与 MOVED 重定向
集群不能用普通客户端一路 get/set,测试时我用 redis-cli 的-c参数:
docker exec -it redis-node-1 redis-cli -c -h 172.21.0.2 -p 6379然后写一个 key:
127.0.0.1:6379> set foo bar如果这个 key 的哈希槽不在 172.21.0.2 上,你会看到类似这样的输出:
-> Redirected to slot [12182] located at 172.21.0.4:6379 OK-c让 redis-cli 自动跟随 MOVED 重定向。不加-c,你会收到MOVED 12182 172.21.0.4:6379错误。这不是集群坏了,是 Redis 在告诉你这个槽归哪个节点管。生产环境的客户端驱动,比如 Java 的 Lettuce、Jedis,Python 的 redis-py,都要开启 Cluster 模式,让它在客户端内部处理路由和重定向。用普通客户端访问集群,会出现随机性的 MOVED 错误,这时候先怀疑客户端配置,别急着怀疑集群。
5. 扩容、缩容和故障转移实战
5.1 扩容:先加从节点再提升
模拟扩容时,我用一套尽量不影响线上流量的顺序。先增加一个节点,让它以从节点身份加入集群,再手动提升或重新分片。先准备节点 7,配置和前面一样,IP 使用 172.21.0.8:
docker run -d --name redis-node-7 \ --network redis-cluster-net \ --ip 172.21.0.8 \ --memory 2g \ --ulimit nofile=65535:65535 \ -v /opt/redis-cluster/node-7/conf/redis.conf:/etc/redis/redis.conf:ro \ -v /opt/redis-cluster/node-7/data:/data \ redis:7.2 \ redis-server /etc/redis/redis.conf加入集群:
docker exec -it redis-node-1 redis-cli --cluster add-node \ 172.21.0.8:6379 172.21.0.2:6379第一个参数是待加入节点,第二个参数是集群内任意一个现有节点。如果不指定角色,新节点会以主节点身份加入,但手上没有槽,暂时不承担读写。为了让数据层有冗余,我一般先把新节点作为某个主的从节点加入。先拿到目标主节点的 ID:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 cluster nodes | grep master然后执行:
docker exec -it redis-node-1 redis-cli --cluster add-node \ 172.21.0.8:6379 172.21.0.2:6379 \ --cluster-slave --cluster-master-id <master-id>这样新节点先同步主节点的全量数据,作为从节点运行。整个过程不触发槽位迁移,风险小很多。
5.2 重新分片:把槽迁到新主节点
如果你希望新节点独立承担一部分数据槽,需要执行 reshard。比如现在有三个主节点,各自约 5461 个槽,加入第四个主节点后,理想状态是每个节点约 4096 个槽。我的习惯是让三个旧主节点各让出 1024 个槽给新节点,迁移总量 3072:
docker exec -it redis-node-1 redis-cli --cluster reshard \ 172.21.0.2:6379 \ --cluster-from <old-master-id-1>,<old-master-id-2>,<old-master-id-3> \ --cluster-to <new-master-id> \ --cluster-slots 3072 --cluster-yes迁移过程里,slot 会分多个批次移动,每个批次迁移一部分 key,Redis 通过异步复制和阻塞迁移控制一致性。数据量大时可能持续很久,线上最好在低峰期操作。还有一个经验:槽迁移不是越多越好,一次性迁移过多会让网络和磁盘 IO 暴涨,建议分批执行,每批一两千个槽,观察集群负载正常后再继续。
5.3 缩容:安全移除节点
缩容的顺序一定要对:先把要移除的主节点上的所有槽迁出去,再把它从集群中摘除。直接删节点不管槽位,是最快的数据丢失方式。
先查目标节点的 ID 和槽位信息:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 cluster nodes | grep 172.21.0.4输出末尾会显示它负责的槽位区间。我要把它所有槽迁到另一个主节点上,比如 172.21.0.2 对应的主节点:
docker exec -it redis-node-1 redis-cli --cluster reshard \ 172.21.0.2:6379 \ --cluster-from <target-node-id> \ --cluster-to <dest-master-id> \ --cluster-slots <slot-count> --cluster-yes如果槽数量很多,redis-cli 会发起多轮迁移,等它全部结束。确认目标节点不再显示任何槽位后,再执行摘除:
docker exec -it redis-node-1 redis-cli --cluster del-node \ 172.21.0.2:6379 <target-node-id>最后停止并删除对应容器:
docker stop redis-node-4 docker rm redis-node-45.4 故障转移:模拟主节点宕机
故障转移是集群最核心的能力。我用主节点 172.21.0.2 做实验。注意,不要用docker stop来模拟宕机,因为 stop 会让 Redis 收到 SIGTERM 正常退出,集群可能在超时前感知到优雅下线,切换行为不够典型。更好的是用pause让容器网络和进程进入假死状态:
docker pause redis-node-2等待超过cluster-node-timeout后,在另一个节点查看集群状态:
docker exec -it redis-node-1 redis-cli -h 172.21.0.2 -p 6379 cluster nodes你会看到原本是 slave 状态的节点变成了 master,原主节点状态标记为fail。这个过程中,如果暂时只有一个主节点负责某段槽,集群状态可能短暂变为 fail,等从节点顶上后才恢复 ok。
恢复原节点时,直接docker unpause redis-node-2。它会重新加入集群,并自动成为新主节点的从节点,不需要手动配置。Redis Cluster 以节点 ID 作为身份,主从角色会跟随槽位和复制状态动态调整。
如果你想做计划内的主动切换,在从节点上执行:
docker exec -it redis-node-6 redis-cli -h 172.21.0.6 -p 6379 cluster failover它会触发一次平滑的主从切换,主节点变为从节点,业务侧的影响会比硬故障小很多。
6. 常见问题与排查技巧
6.1 问题速查表
我整理了这些年用 Docker 搭 Redis 集群踩过的坑,按“现象、原因、解决办法”列出来:
| 现象 | 原因 | 解决办法 |
|---|---|---|
创建集群提示Node xxx is not empty | data 目录里有旧的 nodes.conf 或 AOF 数据 | 清空该节点的 data 目录并重启容器 |
创建集群提示Failed to connect to the bus port | 总线端口被防火墙或端口映射规则挡住 | 自定义网络内开放 6379 和 16379,或映射时同时映射总线端口 |
| 客户端频繁收到 MOVED | 客户端没有开启 Cluster 模式 | 换成 Lettuce、Jedis、redis-py 的 cluster 客户端 |
| 容器重启后节点从集群消失 | data 目录未持久化,nodes.conf 丢失 | 确保 /data 挂载到宿主机持久化目录 |
| 加了 requirepass 后主从不同步 | 缺少 masterauth 配置 | 在所有节点配置 masterauth |
| 用 docker stop 模拟宕机,集群没反应 | 正常退出被节点感知,不算 fail | 用 pause 或 kill -9 模拟真实宕机 |
| 集群卡在 cluster_state:fail | 部分 slot 没有可用主节点 | 用cluster check检查槽位分配,修复未覆盖的槽 |
| cluster nodes 里都是 127.0.0.1 | 节点广播地址不对,通常是 bind 配置缺失 | 配置bind 0.0.0.0,必要时使用 cluster-announce-ip |
最典型的“所有节点都是 127.0.0.1”问题,我单独多说一句。这种情况通常是容器启动时没有正确 bind,或者网络模式没有使用自定义网桥。检查cluster nodes输出,发现一堆 127.0.0.1 时,基本可以断定是配置问题。解决办法是在 redis.conf 里写bind 0.0.0.0,如果有多主机或端口映射,还要通过 cluster-announce-ip 指定对外可达的 IP。
6.2 两个值得长期使用的调参经验
第一个是cluster-require-full-coverage。默认情况下,只要有一个 slot 没有任何可用主节点,整个集群就拒绝所有读写请求。这个设计保证了强一致性,但也让故障影响面扩大。如果业务能接受部分数据不可用,比如缓存场景,把这个参数设为 no,可以让集群在少数 slot 缺失时继续服务其他 slot。取舍由业务决定,如果做的是金融类强一致业务,建议保持默认。
第二个和容器内存相关。Docker 容器不限制内存时,Redis 可能吃满宿主机内存;限制太小又可能让 fork 子进程失败。我建议用--memory和maxmemory组合,并给系统预留至少 20% 余量。Redis 的写时复制机制在写入量大时,fork 出的子进程会额外消耗内存,这部分经常被忽略。
6.3 多机部署时的补充思路
如果六个节点不在同一台 Docker 引擎上,而是分布在多台物理机,前面说的固定 IP 方案就要调整。最朴素的方案是每个容器映射端口到宿主机的不同端口,然后用宿主机的 IP 加映射端口组建集群,同时在配置文件里写清楚 cluster-announce-ip、cluster-announce-port 和 cluster-announce-bus-port。这样节点对外通信都走宿主机网络,跨主机可访问,缺点是容器迁移后 announce 参数要跟着改。
我个人在实际操作中的体会是,Docker 化 Redis 集群最怕的不是 Redis 本身,而是容器网络和配置漂移。把网络和目录规划固定下来,把配置模板版本化,比每次手动敲命令可靠得多。你维护一段时间之后,会发现大多数问题都出在端口没放通、IP 对不上、数据目录没持久化这几件事上,这些恰恰是最值得在搭建初期就规划好的地方。