1. 项目概述
最近一直在琢磨一件事儿:手里的服务器越来越多,单机版Redis撑业务越来越吃力,读多写少的场景勉强能扛,但一到写多或者突发流量,单节点那点内存和CPU实在顶不住。琢磨了几天,决定直接在生产环境用Docker搭一套真正的Redis集群,注意,是Redis Cluster集群,不是哨兵模式。标题里特别强调了“非哨兵模式”,这个方向很多刚入门的朋友容易搞混,我先花点篇幅把两者掰扯清楚。
哨兵模式解决的是高可用问题,主节点挂了自动把从节点提拔成主节点,但对客户端来说永远只有一个写入口,数据量大了之后单机内存就是天花板,你加再多从节点也没用,从节点只是读取副本,写压力全压在主节点上。而Redis Cluster是真正的分布式方案,采用无中心架构,数据按照哈希槽(hash slot)自动分片到不同节点上,每个节点只存一部分数据,写压力被摊薄,扩容缩容都能在线完成,更适合数据量大、写入频繁的业务场景。
这篇博客就是基于我在某模拟项目X上的完整实操过程,从环境准备、网络规划、配置文件编写、容器启动、集群初始化到踩坑记录,一步不落记录下来。适合已经对Redis基本概念有了解、想在生产环境跑一套集群、或者准备面试时被问到“Redis集群怎么搭建”的朋友参考。我会尽量把每个操作背后的原理也讲透,不光是让你“照着敲能跑”,还要让你知道每一步到底在干什么、为什么这么做。
2. 方案选型与架构设计
2.1 为什么选Docker而不是二进制部署
有些朋友可能会问,既然要搭集群,直接下载Redis压缩包、解压、编译、配好配置文件,一台台启动不就行了?为什么要额外引入Docker?我个人的经验是:二进制部署最大的问题在于环境一致性和自动初始化。生产环境往往不是一两台机器,而是几台、十几台,每台机器的基础系统、依赖库、目录结构都有差异,你很难保证每台机器上Redis运行环境完全一致。而且Redis Cluster初始化需要执行一堆CLI命令,手动一台台操作非常容易漏步骤。
Docker带来的两个核心优势是:
一是镜像即环境。官方镜像封装好了Redis运行需要的所有依赖,拉到服务器上一定能跑,不存在“装了半天缺个库”的问题。
二是容器即单元。每个Redis节点就是一个容器,网络、存储、资源限制都能独立配置,后续扩容缩容只需要启停容器,不需要折腾宿主机。
虽然多了一层容器网络的开销,但Redis实例本身对网络延迟敏感度相对可控,实测在Docker的NAT网络下,单次请求延迟比裸机部署高出大概0.1-0.3毫秒,这点开销对大多数业务完全可接受。
2.2 Redis Cluster集群架构解析
Redis Cluster集群的核心理念是数据分片。整个集群的键空间被分成16384个哈希槽,每个节点负责一部分槽。当一个key进来的时候,客户端通过CRC16算法算出哈希值,再对16384取模,得到该key所属的槽编号,然后集群把请求路由到负责该槽的节点上。这个机制保证了key和节点之间的映射关系是确定的、可计算的,而不是随机分配。
一个完整的Redis Cluster集群通常包含至少6个节点,其中3个主节点各带1个从节点。从节点是主节点的数据副本,当主节点故障时,从节点可以自动提升为新的主节点。这跟哨兵模式的区别在于,哨兵是选举出来的独立监控守护进程,而Cluster模式把故障转移逻辑内置在了每个节点里,通过Gossip协议互相通信,不需要单独的哨兵进程。
我这里采用的节点规划是6节点环形小集群设计方案:
| 容器 | 宿主机端口 | 角色 | 数据目录 |
|---|---|---|---|
| redis-a1 | 7001 | 主节点 | /data/redis-a1 |
| redis-a2 | 7002 | 从节点(主a1) | /data/redis-a2 |
| redis-b1 | 7003 | 主节点 | /data/redis-b1 |
| redis-b2 | 7004 | 从节点(主b1) | /data/redis-b2 |
| redis-c1 | 7005 | 主节点 | /data/redis-c1 |
| redis-c2 | 7006 | 从节点(主c1) | /data/redis-c2 |
每个节点映射一个独立的宿主机端口,容器内部统一使用6379端口。数据目录挂在宿主机上,这样容器挂了数据还在,重新起容器立刻接上。
2.3 网络规划:为什么要用独立网桥
这里有一个新手特别容易踩的坑:直接使用Docker默认的bridge网络搭建集群。Docker默认bridge网络的特点是基于NAT的,宿主机和容器之间、容器和容器之间通信,IP地址是Docker分配的网段(通常是172.17.0.0/16),Redis Cluster节点之间需要通过IP互相通信,但这个网段是动态的,每次起容器分配的IP都可能变。
更麻烦的是,Docker的bridge网络对容器之间的通信虽然开放,但Redis Cluster启动时会在节点配置中写入自己的IP地址,如果这个IP是默认bridge网络分配的临时IP,重启容器IP一变,集群其他节点就找不到它了,整个集群直接瘫痪。
解决方案有两种:一种是使用--link静态绑定,但链路一多根本维护不过来;另一种更推荐,创建自定义网桥。Docker自定义网桥的好处是容器之间可以通过名称互相访问,而且DNS解析自动注入,省去手动管理IP的麻烦。我最终选择创建一个独立的网桥redis-cluster-net,子网固定为172.31.0.0/16,这样所有Redis容器都在一个隔离网络里,既不会跟外部网络冲突,也方便节点间稳定通信。
3. 核心实现细节:从环境准备到容器集群启动
3.1 基础环境准备与镜像选型
开始之前,先确认宿主机上已经安装好了Docker。版本方面,我使用的是20.10以上的版本,Docker Compose如果要用也可以装上,但这里我直接采用docker run命令来一个个起容器,六条命令照样管理得过来,反而更直观。
Redis镜像版本选型上需要单独说两句。有人习惯直接用最新版redis:latest,但在生产环境,latest标签意味着不可控的版本漂移——你今天部署是7.x,明天拉镜像可能变成8.x,集群节点版本不一致很容易出问题。我建议退一步,锁定一个稳定版本,比如redis:7.0.12,具体小版本按官方镜像仓库的稳定版选就行。7.x相比6.x在集群模式下有一些性能和稳定性提升,而且AES等新命令的支持更好。
# 拉取镜像 docker pull redis:7.0.12 # 创建独立网桥 docker network create --subnet=172.31.0.0/16 redis-cluster-net3.2 配置文件编写与参数解析
Redis Cluster的容器化部署和单机部署最大区别就在于配置文件,有几个参数是必须得配的,缺一个集群都初始化不了。先看我最终使用的配置文件模板:
# /data/redis-common.conf port 6379 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes appendfilename "appendonly.aof" protected-mode no daemonize no bind 0.0.0.0逐个参数我说一下为什么这样设置:
port 6379:容器内部固定使用6379,宿主机端口通过-p参数映射成7001-7006,这种内外端口分离的方式便于在宿主机上快速辨认是哪个节点。cluster-enabled yes:这是Redis Cluster模式的总开关,只有打开这个,Redis启动后才会进入Cluster模式,侦听集群总线端口(6379+10000,即16379)。cluster-config-file nodes.conf:每个节点保存集群元数据的文件,包括节点ID、槽位分配信息、与其他节点的连接关系。Docker重启容器时,只要数据目录里这个文件还在,节点ID就不会变,整个集群的状态就能无缝恢复。cluster-node-timeout 5000:节点间通信超时时间设为5秒。这个值不能太小,网络稍微抖动一下节点就被判定为故障,会导致频繁的主从切换;但也不能太大,否则主节点真挂了,从节点要等很久才开始选举。5秒在大多数局域网内是合理的。protected-mode no:Docker容器环境下,设置这个是因为容器网络内没有NAT转换,如果保持protect mode默认开启,集群节点之间通过IP直连时,只有配置了密码或者绑定了特定IP才会放行。我这里的网络是隔离的独立网桥,没有暴露到公网的风险,所以关掉保护模式。如果你的环境有公网访问风险,一定不要关,而是要配置requirepass和masterauth密码认证。
3.3 启动6个容器节点
搞定了配置和网络,接下来就是启动6个容器节点了。这里我把宿主机上的配置目录和数据目录都预先建好,统一规划在/data/redis-*下,方便备份和维护。
for port in 7001 7002 7003 7004 7005 7006; do echo "[+] 启动redis-${port} ..." docker run -d \ --name redis-${port} \ --network redis-cluster-net \ --ip 172.31.0.1${port: -1:1} \ -p ${port}:6379 \ -v /data/redis-${port}/data:/data \ -v /data/redis-common.conf:/etc/redis/redis.conf \ redis:7.0.12 \ redis-server /etc/redis/redis.conf done等等,上面的循环写法有问题,是我后来调试时用的一种动态生成IP的小技巧,实际跑的时候发现了几个问题。先直接用最直观的单条命令起,避免给自己挖坑:
# 节点1:a1,主节点 docker run -d \ --name redis-7001 \ --network redis-cluster-net \ --ip 172.31.0.10 \ -p 7001:6379 \ -v /data/redis-7001/data:/data \ -v /data/redis-common.conf:/etc/redis/redis.conf \ redis:7.0.12 \ redis-server /etc/redis/redis.conf后面5个命令除了容器名、映射端口、IP地址、数据目录不同,其余完全一样,这里就不一一贴出来了。关键注意点是IP:redis-7001对应172.31.0.10,redis-7002对应172.31.0.20,依次类推,这样看到IP就能认出来是哪个端口,也好记。虽然Docker自定义网络支持自动分配IP,但我手动指定IP是为了后续命令输入时少犯低级错误。
启动完成后,先检查容器状态:
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.IPAddress}}"正常的话6个容器都处于Up状态。紧接着把端口连通性测一下:
# 从任意容器里尝试连接另一个节点的Redis端口 docker exec -it redis-7001 redis-cli -h 172.31.0.20 -p 6379 ping3.4 Cluster集群初始化:meet操作与插槽分配
容器全部启动之后,容器内Redis默认都还是独立节点,没有建立任何集群关系,就像6个人各自住在一个房间里,还没有互相“认识”。而集群初始化的本质就是让每个节点“互相认识”然后开始“分地盘”。
Redis Cluster提供了两种初始化方式:一种是低版本的redis-trib.rb脚本,这个脚本在5.x之后就被弃用了,官方推荐直接使用redis-cli命令完成所有集群操作。这里我使用的是第二种,也是目前最推荐的做法。
第一步,把6个节点互相meet起来。所谓meet,就是让一个节点主动去联系另一个节点,建立gossip通信。只要有一个节点认识第二个节点,第二个节点又认识第三个节点,整个集群的节点关系网就会逐渐传遍。实际操作时,只需要把a1节点分别meet其他5个节点就行:
# 进入任意一个容器(这里用7001节点) docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster meet 172.31.0.20 6379 docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster meet 172.31.0.30 6379 docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster meet 172.31.0.40 6379 docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster meet 172.31.0.50 6379 docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster meet 172.31.0.60 6379先从任何一台(比如7001节点)主动联系其他所有节点,当时我心里想的是:反正meet操作是有向的,只要7001节点认识其他5个节点,通过gossip协议,其他节点之间也会逐渐互相认识,这是集群协议设计好的行为。执行完meet后,随便进一个节点查看:
docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster nodes如果输出里能看到6条节点记录(包含各自的ID和IP地址),说明节点间gossip通信正常。无论从哪个节点执行cluster nodes,看到的都应该是一致的信息,这也是集群一致性的一个体现。如果只出现部分节点,等几秒再查一下,gossip协议是周期性传播的,这个群里最新的节点信息会逐渐同步过去,如果一直不出现,就要排查网络隔离和端口开放了。
第二步,分配主从关系。目前6个节点都是主节点(M),需要额外指定3个从节点。采用cluster replicate命令,逐个指定:
# 先把7002、7004、7006对应的节点ID查出来 docker exec -it redis-7002 redis-cli -h 172.31.0.20 -p 6379 cluster myid docker exec -it redis-7004 redis-cli -h 172.31.0.40 -p 6379 cluster myid docker exec -it redis-7006 redis-cli -h 172.31.0.60 -p 6379 cluster myid实际跑的时候三个ID分别是(示意,实际是40位Hex字符串):826de...a03、bfee2...c11、8d1da...2ff。然后分别在对应的节点上执行cluster replicate <主节点ID>指令。
假设7001的节点ID是MASTER_A_ID,在7002容器中执行:
docker exec -it redis-7002 redis-cli -h 172.31.0.20 -p 6379 cluster replicate MASTER_A_ID同理,7004复制7003,7006复制7005。执行完之后再次查看cluster nodes,那些节点标识里的M和S关系就明确出来了。
第三步,分配哈希槽(hash slot)。这是集群初始化的重头戏,前提是所有主节点必须将16384个槽位一一分完,集群才能进入ok的在线状态。根据三个主节点7001、7004、7005(注意我的规划里主节点是7001、7004、7005,从节点是7002、7006、7003,如果你按之前表格配置,稍有不同,以实际为准),可以均分:
- 7001节点负责
0 - 5460 - 7004节点负责
5461 - 10922 - 7005节点负责
10923 - 16383
分配方式是逐个把槽位添加给对应节点。这里有个小技巧,不用一条条敲16384条命令,直接把连续的槽位segments批量塞进去:
# 7001节点分配0~5460槽 docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster addslots {0..5460} docker exec -it redis-7004 redis-cli -h 172.31.0.40 -p 6379 cluster addslots {5461..10922} docker exec -it redis-7005 redis-cli -h 172.31.0.50 -p 6379 cluster addslots {10923..16383}上面代码里的{0..5460}语法在bash中会展开成0到5460个数字,执行效率很高。如果是zsh也支持类似的展开。执行完之后再检查集群状态:
docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster info重点看这两行:
cluster_state:ok cluster_slots_assigned:16384cluster_state:ok表示集群可用,cluster_slots_assigned:16384说明全部槽位分配完毕。
如果有兴趣,也可以看一下每个节点确认自己的槽位区间:
docker exec -it redis-7001 redis-cli -h 172.31.0.10 -p 6379 cluster slots这个命令输出的是一张槽位分配表,长这样(示意):
0) 1) (integer) 0 2) (integer) 5460 3) 1) "172.31.0.10" 2) (integer) 6379 3) "..."到这里,一个基本的Redis Cluster集群已经建立起来了。不过还没完,客户端要访问集群,有几个坑必须提前测一下。
4. 验证与故障演练:亲测集群可用性
4.1 写入数据验证自动分片
集群搭好只是第一步,真正需要验证的是数据能不能按照哈希槽正确分流。正常Redis单机客户端直接用redis-cli set key value就行,但Cluster模式下如果直接连到某台节点上写入,会发现一个奇怪的现象:有些key能写进去,有些key直接报MOVED错误。
这其实是Redis Cluster的正确行为。当你写入一个key时,客户端会计算它的哈希槽,如果计算出来的槽位正好落在当前连接的节点上,直接写入;如果不在,当前节点会返回一个MOVED错误,告诉你这个key应该在哪个IP端口上处理。普通模式的redis-cli不会自动跟随跳转,所以我们需要用-c参数启用集群模式:
# -c 表示cluster模式,客户端会自动处理MOVED跳转 docker exec -it redis-7001 redis-cli -c -h 172.31.0.10 -p 6379进去后写几个测试key:
127.0.0.1:6379> set name zhangsan -> Redirected to slot [5799] located at 172.31.0.40:6379 OK 127.0.0.1:6379> get name -> Redirected to slot [5799] located at 172.31.0.40:6379 "zhangsan" 127.0.0.1:6379> set age 18 -> Redirected to slot [741] located at 172.31.0.10:6379 OK看到Redirected to提示,就说明数据确实不是都存在一个节点上,而是按槽位合理分散了。我连续写入几十个key观察分布,三个主节点上的key数量基本是均匀的,这正是哈希槽机制的功劳。
4.2 主从数据同步验证
数据写入主节点后,从节点理论上会通过异步复制同步数据。检查方法是从从节点读取刚才写入的key:
docker exec -it redis-7002 redis-cli -h 172.31.0.20 -p 6379 # 从节点读取数据 127.0.0.1:6379> get name "zhangsan"这里有个细节:从节点默认是只读的,配置文件中也没有设置slave-read-only no,所以从节点能读不能写,这是默认的好行为。如果从节点读不到数据,排查顺序是:
- 先看主从复制是否正常连上:
redis-cli -h 从节点IP -p 6379 info replication,重点看master_link_status:up。 - 如果显示
down,查看从节点日志或主节点的repl_backlog状态。
4.3 节点宕机与故障转移实测
集群的故障转移能力是核心卖点,光搭不测等于白搭。这里我直接kill掉一个主节点,比如7005节点,然后观察整个集群的反应:
# 停止7005主节点容器 docker stop redis-7005停掉之后等一会儿,大约cluster-node-timeout5000毫秒到期后,集群开始选举。从节点7003会自动成为候选者,发起投票,超过半数主节点通过后,它被提升为新的主节点,同时接管7005上的所有槽位。
查看集群状态:
docker exec -it redis-7001 redis-cli -c -h 172.31.0.10 -p 6379 cluster nodes这时应该能看到7003节点变成了M(主节点),而原来的7005节点显示fail状态。更重要的是重新执行cluster info:
cluster_state:ok cluster_slots_assigned:16384集群仍然可用,读写不受影响。我特地用外部客户端连续写入数据,验证槽位接管是否正常。这里一个小小提示:如果你用的是旧版客户端,它可能不会自动刷新节点映射表,出现过期的MOVED跳转信息,需要在客户端侧开启自动更新集群拓扑的功能。
再把7005容器启回来:
docker start redis-70057005重新上线后,它会以一个新节点身份加入集群,此时cluster nodes会看到它变成主节点但没有槽位。可以手动添加它作为某个新主节点,或者做rebalance,如果只是恢复原状测试,直接把它作为某个从节点replicate过去就行。我这里复测一下,让它重新成为某主节点的从节点。这一步说明,Redis Cluster的优雅设计让节点可以随时增删,不会造成集群不可用的动荡。
5. 常见问题排查与避坑经验
5.1 集群总线端口冲突
这是Docker部署Redis Cluster踩坑率最高的问题,没有之一。Redis Cluster有一个隐藏要求:每个节点除了监听6379端口,还需要监听6379+10000=16379端口,这个端口是集群总线端口,用于gossip通信、故障检测、主从选举等。
问题来了,Docker启动容器时如果只映射了-p 7001:6379,容器内的16379端口确实也开了,但数据包通过宿主机NAT转发时,Redis节点拿到的对端地址是宿主机映射后的地址和端口。如果宿主机没把16379端口映射出来,容器之间的跨宿主机通信就会失败。
我的六个节点全在同一个宿主机上,用的是自定义网桥,容器之间直接走虚拟网络,不需要经过宿主机NAT,所以16379端口的问题没有暴露。但如果你要做跨宿主机集群,每个节点除了映射6379,也要映射对应的集群总线端口,比如:
-p 7001:6379 -p 17001:16379同时那意味着--ip模式在跨宿主机场景就行不通了,需要换用不同宿主机上的不同网段IP,然后cluster meet时要写宿主机的真实通信IP,这个模式比较复杂,一般建议在Kubernetes环境下用StatefulSet + Headless Service解决,而非手动管理容器网络。
5.2 数据目录持久化与节点ID漂移
Docker容器的重启机制很简单:容器停了再启动,配置和数据目录如果没有挂载持久化卷,容器内的数据直接归零,节点ID也会重新生成,这在集群场景里是致命的——其他节点存着的旧node ID会全部失效,集群直接崩掉。
所以我强烈建议,容器的/data目录必须挂在宿主机上。这样即使容器删了重建,只要nodes.conf文件还在,节点ID不变,节点能无缝回归集群。
具体操作就是将-v /data/redis-7001/data:/data挂载上,别偷懒用无持久化的匿名卷。
5.3 集群初始化后状态一直fail
遇到过集群初始化后cluster_state:fail的情况,排查了一圈发现是槽位没分配完整。好几个人第一次搭的时候都会漏掉其中一个主节点的槽位分配,或者把槽位区间写错了导致重叠。推荐初始化完成后一定执行一次:
redis-cli -h 任意节点IP -p 6379 cluster info | grep -E "cluster_state|cluster_slots_assigned"如果cluster_slots_assigned小于16384,说明还有槽位没分配出去。可以用cluster addslots补上,也可以用官方推荐的redis-cli --cluster reshard工具来动态迁移槽位——后者适合向已有集群添加新主节点时使用,初次初始化没必要那么麻烦。
5.4 MOVED vs ASK重定向
客户端接入集群时,经常能看到两种重定向错误:MOVED和ASK。两者含义不同,排障方向也不一样:
| 异常类型 | 含义 | 处理方案 |
|---|---|---|
| MOVED | 槽位映射已永久变化,请求必须去新节点 | 客户端更新本地槽位缓存,后续直接发往新节点 |
| ASK | 槽位正在迁移过程中,数据部分还在旧节点 | 先向新节点发ASKING命令,再执行实际操作 |
遇到MOVED不用慌,改客户端就行;遇到ASK说明集群正在reshard或迁移,等迁移完成后再查。我这里搭的集群没有触发自动迁移,因此业务侧体验就是无感。
5.5 Docker restart vs stop/start对集群状态的影响
一个小细节:对容器执行docker restart和docker stop && docker start,在Redis Cluster中的表现是有区别的。docker restart发送的是SIGTERM信号,Redis会优雅关闭,把集群总线连接正常断开,其他节点能及时感知到这个节点下线;而docker kill直接发SIGKILL,节点来不及通知其他节点就没了,集群要等cluster-node-timeout到期才能判定故障。实际演练故障转移时,建议用docker stop模拟优雅下线,用docker kill模拟突发宕机,两种场景下的故障切换时间会有差异。
5.6 千万别把密码配置忘了
因为我用的是独立网桥隔离环境,不需要设密码。但如果你把端口映射到了宿主机公网IP上,务必在配置中加入:
requirepass 强密码 masterauth 强密码注意两个都要配。requirepass是客户端访问认证,masterauth是节点间在主从复制和故障转移时的认证信息。只配了requirepass而忽略masterauth,主从复制会一直处于down状态,数据同步完全失败。这是一个非常容易踩的坑,很多刚上手的朋友把密码配到一半,结果集群看起来正常,但从节点数据永远不同步。
同样道理,protected-mode在公网环境下就别关,保持默认开启反而更安全。
6. 细节优化与生产环境建议
6.1 内存与持久化策略选择
Redis的持久化有两种方式:RDB快照和AOF日志。在Cluster场景下,我建议至少开启AOF,因为集群本身已经做了数据分片,单节点数据量相对可控,AOF文件规模能接受,换来的是最多丢失1秒数据的保障。配置中我用了appendonly yes,默认的AOF刷盘策略是everysec,这个策略在性能和数据安全之间取了一个相对平衡的点。
如果追求更高可靠性,可以改为always,每条写命令都刷盘,但性能会明显下降;如果追求极致性能,可以关掉持久化,纯靠主从复制保底,但主节点硬盘损坏时从节点也会缺失一段数据。我的建议是everysec,这也是绝大多数生产环境的默认配置。
内存方面特别提醒:如果宿主机内存有限,比如只有8G,6个Redis节点各占1.5G内存,光Redis就占了9G,这会导致容器被宿主机OOM Killer杀掉。配置maxmemory和maxmemory-policy很有必要。一般建议每个Redis容器设置--memory=2g,并在配置中设置maxmemory 1.5gb,超过内存不再接收写入,避免把宿主机拖垮。
6.2 自动故障恢复与重启策略
我在生产环境部署容器时,通常会给容器加上--restart=always策略,但这在Redis Cluster场景下反而有个小坑:如果宿主机重启,所有容器同时启动,Redis集群节点会同时尝试连接对方,虽然最终会恢复一致,但启动初期可能出现瞬时的CLUSTERDOWN错误。更好的做法是让容器按顺序启动,或者至少给一个启动延迟。在Docker Compose里可以通过depends_on和healthcheck配合控制启动顺序,但追求极致稳定的话,用Kubernetes的StatefulSet可以通过podManagementPolicy: OrderedReady实现有序启动。
为了榨出更高的集群利用率,几个节点也可以设置为replica-announce-ip和replica-announce-port参数,让节点在广播自己地址时使用宿主机映射后的地址和端口,这样即使客户端从外部网络接入,集群节点返回的重定向地址也是可路由的。
6.3 容器日志与监控
Grep集群日志是排障的第一手段,但在容器环境下,docker logs默认输出的容器stdout就是Redis日志,方便归集:
Docker部署Redis集群在这个迷你项目里跑通之后,我尝试用Prometheus + redis_exporter来监控集群状态。这种方式确实方便扩展,redis_exporter容器可以通过环境变量REDIS_ADDR指定多个Redis地址,从而识别出集群节点状态。
如果暂时不想引入Prometheus体系,至少要保证以下指标可观测:每个节点的cluster_state、connected_slaves、master_link_down_since_seconds。我自己常用的一条快速巡检命令是:
for p in 7001 7002 7003 7004 7005 7006; do echo "=== redis-${p} ===" docker exec redis-${p} redis-cli -p 6379 cluster info | grep -E "cluster_state|cluster_known_nodes" done一条命令把6个节点的集群状态全拉出来,省得一个个容器进去查看。
7. 写在最后:踩过坑之后的几点体会
这套集群从零到跑通,整个过程花了一个多小时,真正的命令操作不到20分钟,剩下的时间全在排查各种问题。我个人总结下来,Docker部署Redis Cluster这件事,最容易出问题的不是Redis本身,而是容器网络层面那些看不见的细节。
真要给建议的话,第一点是容器网络一定要统一规划,不要图省事直接用默认bridge,宁可多花两分钟创建独立网桥并固定IP,后续带来的稳定性能省下成倍排查时间。第二点是每个节点的数据目录一定要挂载到宿主机,这不仅是数据安全层面的问题,更是节点ID能不能保持稳定的前提。第三点是无论用redis-cli还是客户端接入集群,一定要用-c参数,否则遇到MOVED重定向一头雾水,以为是集群出了问题,实际只是客户端模式不对。
当初试完这套集群后,我又顺手试了在集群模式下执行一些批量操作,发现Lua脚本和MULTI事务在多key场景下是受限的,因为不同key可能落在不同节点上,跨节点事务是不支持的。如果有这类业务需求,就得靠客户端hash tag机制把需要原子操作的key强制放到同一个槽位。这方面内容也比较有意思,后面有空可以再单独写一篇讲讲。
用Docker搭Redis集群这件事,本质上就是把Redis Cluster的架构原理在容器化环境中复现了一遍。说实话,当你用一条cluster info看到cluster_state:ok的时候,那种踏实感,跟写完了单元测试全绿、编译器零告警的感觉差不多。