ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker下搭建Redis集群:从哈希槽原理到故障转移实战

Docker下搭建Redis集群:从哈希槽原理到故障转移实战 说实话一看到“docker下搭建redis集群”这个标题我就知道这兄弟多半是被线上Redis单机版的“内存告警”“延迟飙升”“主从不顶用”折腾到失眠了。我当年也是这么过来的那时候打开搜索引擎满屏的教程不是照着敲半天敲不出来就是只丢几行命令让你背至于为什么这么配、踩坑了怎么排查半个字都没有。今天我就把这套东西掰开揉碎了讲一遍从原理到实操再到故障排查全程用真实的容器命令演示照着一步步来最后你手里会有一套真正能扛数据分片和故障转移的Redis集群。这套方案能解决什么问题说白了就三个数据不再只堆在一台机器上、读写压力能分摊到多节点、个别节点挂了集群能自动顶上。适合什么样的读者业务开发、后端工程师、运维或者正在准备Redis相关面试的人。如果你是那种“Redis只用来做缓存数据丢一点都不怕”的情况单机加主从就够用了别没事找事上集群。但如果你动了“把Redis当可靠存储用”的念头或者数据量和QPS已经明确让单机喘不过气那这篇文章就是为你写的。1. 搭建前先把这块“骨头”啃明白1.1 Redis集群到底解决了什么问题很多人对Redis集群的理解就一句话“多搞几台机器把数据拆开放。”这个理解方向上没问题但细节上差了十万八千里。Redis Cluster采用的是无中心化架构节点之间通过Gossip协议互相通信掌握彼此的存活状态、主从关系、槽位分配情况。集群里没有“老大”任何一个节点收到客户端的请求都能根据键计算出该数据属于哪个槽位、由哪个节点负责。重点是那个“槽位”的概念。整个集群固定划分了16384个哈希槽你存一个keyRedis会对这个key做CRC16运算然后对16384取模得到一个0到16383之间的数字这就是它归属的槽位。集群在初始化的时候会把这16384个槽位分配给各个主节点。所以当你往节点A写入一个key算出来的槽位并不在A的管辖范围内时它会直接返回一个MOVED错误告诉你正确访问地址在哪。这就是为什么搭建完集群以后客户端必须得支持Cluster协议——像JedisCluster、Lettuce这类客户端会自动跟随节点返回的重定向去访问正确的节点普通连接池没有这个能力。那主从复制和故障转移是怎么回事呢每个主节点可以挂一个或多个从节点。主节点负责处理它管辖槽位的读写请求从节点实时同步主节点的数据。一旦主节点挂了集群里的其他节点会在从节点中投票选出一个把它提升为新的主节点然后接管对应的槽位。这整个过程不需要哨兵参与跟主从加哨兵那套是两种路线集群自己就能完成。我个人的理解是哨兵模式更像是给单机或主从架构配了个“监工”而集群模式本身就是一个能自我管理的分布式系统。1.2 为什么偏偏选Docker来做这件事如果你在裸机上直接搭这套集群先不说你要准备六台机器三主三从光是装环境、下载Redis、协调端口就够你喝一壶的。Docker的价值在于把环境差异这个最大的不确定性直接干掉了。镜像里打包好了操作系统、Redis二进制、运行环境不管你的宿主是CentOS还是Ubuntu拉下来就能跑跑起来行为一致。更重要的一点是Docker容器的“销毁和重建”成本几乎为零。集群搭建过程中难免配错参数、启动失败大不了删掉容器、改配置、重新起一个耗时不过几秒钟。这在裸机上不可想象——你装了半天Redis配置文件搞乱了要么硬着头皮改要么卸载重装。我搭这套集群的时候至少删了三十次容器每次删完重来都很轻松这就是容器化实操的底气。但有一点我必须泼冷水容器天然有资源隔离的开销网络性能也不如宿主机直接跑Redis。在生产环境如果追求极致性能很多团队还是选择物理机或虚机直接部署。Docker这套方案的定位是“低成本的实验和测试环境”或者中等规模、对性能富余度要求不高的业务场景。不要被网上的所谓“生产最佳实践”忽悠一定要结合自己团队的基础设施再下结论。1.3 网络模式怎么选host与bridge的真实权衡这是搭建之前最需要想明白的一个决策点也是最容易踩坑的地方。Docker默认的bridge网络模式容器有自己的独立IP宿主机通过端口映射把流量转发进去。看起来没什么问题但Redis集群有个特殊机制——节点之间除了普通的客户端通信端口还额外占用一个端口专门用于集群内部通信端口号一般是客户端端口加10000比如6379对应16379。这个内部通信端口走的是长连接和Gossip协议在bridge网络下容器里的Redis会把自身的IP地址和外联信息告诉集群里的其他节点这里极容易产生“容器IP与宿主机IP”错位的问题。所以我的强烈建议是在Linux宿主机上直接用host网络模式搭建Redis集群。host模式下容器和宿主机共享网络栈没有NAT、没有端口映射Redis看到的IP就是宿主机的真实IP节点之间通信顺畅无阻性能也比bridge高。但这里有个现实问题——如果你用的是macOS上的Docker Desktop或者Windows的Docker Desktophost网络模式的支持很有限甚至启动直接报错。这种情况我见过太多了网上搜“docker desktop failed to start”搜到崩溃的都有。所以下面实操部分我会以Linux环境为主演示host模式也会在第五小节里给出Docker Desktop环境下的替代方案。2. 环境准备与配置细节2.1 基础环境与镜像选择这次实操我用的是一台4核8G的Linux服务器Docker版本是24.xRedis镜像选的是官方redis:7.0。为什么选7.0不用更新的7.2、8.0特性更多但对于搭建集群这个目标来说7.0稳定程度高、资料多、网上踩坑案例也最丰富。不建议用太老版本Redis 3.2以下的集群机制和现在的行为差别比较大网上资料容易混淆。先确认环境没问题docker --version docker pull redis:7.0镜像拉下来以后别忘了验证一下docker run --rm redis:7.0 redis-server --version能打印版本号就说明镜像可用。这一步看起来多余但我曾遇到过镜像下载不完整导致容器启动秒退的情况后来发现是本地残留了损坏的镜像层。建议定时清理一下本地镜像缓存遇到启动异常就先删镜像重新拉别在这上面浪费时间。2.2 redis.conf配置文件逐项说明集群需要六份配置文件对应六个节点。但它们的核心内容大同小异无非是端口号不一样。我先贴一份典型的配置然后逐项解释为什么要这么写这个比命令本身重要的多。port 6379 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 15000 appendonly yes appendfilename appendonly.aof appendfsync everysec protected-mode no daemonize noport不用多说每个节点的门牌号。cluster-enabled yes是开关不打开这个你连cluster指令都识别不了。cluster-config-file是集群元数据文件节点会把集群状态、节点ID、主从关系、槽位分配信息都写进这个文件这个文件绝对不要手动修改也不要放在容易被清掉的临时路径里。cluster-node-timeout设置的是节点超时时间单位是毫秒。如果某个节点在这个时间内没有响应心跳其他节点就会认为它挂了触发故障转移。这里设置15000毫秒也就是15秒既不会因为网络瞬时抖动就误判宕机也不会让故障转移等得太久。如果你把业务场景的网络质量不好可以适度加大这个值。appendonly开启AOF持久化appendfsync everysec保证每秒刷盘一次兼顾可靠性和性能。注意集群模式下不是说开启了持久化就可以高枕无忧了只是说节点重启后数据不会丢得干干净净。protected-mode一定要设成no不然集群节点跨IP通信的时候容易被拒绝连接。daemonize no是因为我们用Docker的PID 1去跑Redis进程不应该让进程后台化否则容器会变成Exit状态。还有一点容易被忽略——每份配置文件里最好加上bind 0.0.0.0。默认情况下Redis只监听本机回环地址如果你用了自定义网络或者客户端要在其他机器访问不绑定全地址就只听127.0.0.1直接导致连接不上。生产环境谨慎用0.0.0.0但在测试和隔离网络环境下这是最省事的做法。2.3 目录规划与容器启动六个节点的数据目录我建议这样规划清晰且好维护/usr/local/redis-cluster/ ├── node-1/conf/redis.conf ├── node-1/data/ ├── node-2/conf/redis.conf ├── node-2/data/ ├── node-3/conf/redis.conf ├── node-3/data/ ├── node-4/conf/redis.conf ├── node-4/data/ ├── node-5/conf/redis.conf ├── node-5/data/ ├── node-6/conf/redis.conf └── node-6/data/先从node-1复制出六份配置然后把每份的port改成对应的6379到6384。六份配置的端口规划如下节点客户端端口集群通信端口node-1637916379node-2638016380node-3638116381node-4638216382node-5638316383node-6638416384这里再啰嗦一句Redis集群的通信端口是在原端口上自动加10000不需要你在配置文件里额外指定但防火墙和安全组必须把这个端口也放行。很多新手只开了6379结果集群一直卡在握手阶段怎么查都查不出毛病最后发现是16379被墙了。3. 六节点集群搭建实操3.1 逐个启动节点容器确认好目录和配置以后开始启动容器。以node-1为例命令长这样docker run -d \ --name redis-node-1 \ --net host \ --restart unless-stopped \ -v /usr/local/redis-cluster/node-1/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-1/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf参数说明一下--net host让容器共享宿主机网络栈这是Linux下跑集群的关键--restart unless-stopped保证宿主机重启后容器自动拉起不至于手动一个个点挂载的配置文件和数据目录对应好时区文件挂载进去是为了让日志时间跟宿主一致排查问题时少一层干扰。后面五个容器一个接一个启动把名字、端口、挂载路径跟着变就行# node-2 端口6380 docker run -d --name redis-node-2 --net host \ -v /usr/local/redis-cluster/node-2/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-2/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf # node-3 端口6381 docker run -d --name redis-node-3 --net host \ -v /usr/local/redis-cluster/node-3/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-3/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf # node-4 端口6382 docker run -d --name redis-node-4 --net host \ -v /usr/local/redis-cluster/node-4/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-4/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf # node-5 端口6383 docker run -d --name redis-node-5 --net host \ -v /usr/local/redis-cluster/node-5/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-5/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf # node-6 端口6384 docker run -d --name redis-node-6 --net host \ -v /usr/local/redis-cluster/node-6/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-6/data:/data \ -v /etc/localtime:/etc/localtime:ro \ redis:7.0 \ redis-server /etc/redis/redis.conf全部启动完别急着初始化集群先看一眼容器状态docker ps | grep redis-node正常的话六行都是Up状态。万一有容器没起来用docker logs查看具体报错最常见的是配置里指定了容器内不存在的路径或者配置文件权限导致Redis拒绝了启动。把这个排查习惯养成后面能省很多时间。3.2 用redis-cli完成集群初始化Redis 5.0之后官方把集群初始化工具直接集成进了redis-cli里不再需要单独的redis-trib.rb。这一步极其关键命令如下redis-cli --cluster create \ 192.168.1.10:6379 \ 192.168.1.10:6380 \ 192.168.1.10:6381 \ 192.168.1.10:6382 \ 192.168.1.10:6383 \ 192.168.1.10:6384 \ --cluster-replicas 1你要把自己的宿主机IP替换成192.168.1.10的位置。那个--cluster-replicas 1意思是每个主节点配一个从节点。这条命令执行后redis-cli会自动把前三个节点规划为主节点后三个对应设为它们的从节点。它还会根据你的配置回显一个槽位分配计划大概长这样 Performing hash slots allocation on 6 nodes... Master[0] - Slots 0 - 5460 Master[1] - Slots 5461 - 10922 Master[2] - Slots 10923 - 16383 Adding replica 192.168.1.10:6383 to 192.168.1.10:6379 ... Can I set the above configuration? (type yes to accept):这里必须手动输入yes回车。我见过有人写脚本把这里漏掉了导致集群初始化卡在半路。输入yes以后redis-cli会依次把槽位分配给各个主节点再让从节点去同步主节点的数据。整个过程很快但输出里如果出现了[ERR] Node ... is not empty或者[ERR] Not all 16384 slots are covered by any nodes多半是数据目录里有旧数据或者之前的集群信息没清干净干脆把data目录里的AOF文件和nodes.conf全删掉重新来。执行完以后你会看到一行确认信息说明集群已经成型。到这一步简单搭建其实已经完成了。但如果用的不是host模式而是bridge网络做端口映射这里的IP就得写容器IP同时还要在配置文件里指定cluster-announce-ip和cluster-announce-port向集群通告外部可访问的地址。这块坑非常深所以我才反复强调Linux上直接host模式别自找麻烦。3.3 验证集群状态与槽位分布集群建好了不做验证就等于是盲人骑瞎马。先用cluster info看一下整体状态redis-cli -p 6379 cluster info重点关注cluster_state:ok和cluster_slots_assigned:16384。如果cluster_state显示fail说明有槽位没有被主节点覆盖节点挂掉或者构建的时候出了岔子。然后再看节点详情redis-cli -p 6379 cluster nodes输出里的每一行代表一个节点包括节点ID、IP:端口、角色标志、槽位范围。主节点标记是master从节点标记是slave。每行的末尾还会带一个busport比如637916379这时候你就可以确认集群通信端口已正常开放。这还不够我还建议你用cluster slots命令查一下槽位映射的具体归属redis-cli -p 6379 cluster slots输出的数组里每一组列出了槽位范围、对应主节点的地址和ID、从节点地址。这组数据既是判断集群健康度的依据也是后续做扩容和缩容时的原始参考值得多看两眼。4. 集群验真故障转移与日常维护4.1 写数据验证与槽位重定向集群搭建完第一件事当然是往里写数据。你可以用redis-cli -c参数进入集群模式客户端这个模式跟普通客户端最大的区别是它能够自动处理MOVED重定向。redis-cli -c -p 6379 127.0.0.1:6379 set user:1001 zhangsan - Redirected to slot [5474] located at 192.168.1.10:6380 OK看到那行Redirected了吗这说明key经过CRC16计算分配的槽位在6380这个节点上客户端自动帮你跳过去了。如果你不用-c参数用普通模式连接6379去写这个key结果会直接报错(error) MOVED 5474 192.168.1.10:6380这个报错不是故障是集群的正常行为。它相当于告诉你“这个key归6380管你去那边操作。”如果你在测试的时候遇到MOVED别慌这是它工作正常的证据。客户端层面像Spring Boot项目里用Lettuce客户端配置了Redis集群模式这些重定向是透明的代码里什么都不用管。4.2 主从切换实测验证集群能不能真的扛事最硬核的做法就是把一个主节点干掉看集群是否自动响应。先找到三个主节点里最不顺眼的那个我这把集群里node-1是6379直接把它停掉docker stop redis-node-1停掉以后别急着查先等等。因为集群要等cluster-node-timeout到了也就是15秒才会认定节点失联并开始故障转移流程。等个二十秒左右再查redis-cli -p 6380 cluster nodes这时你会发现原本是6383从节点的那个实例角色变成了master槽位区间也跟随原主节点。它已经被自动提升成新的主节点了。这就是Redis Cluster的自动故障转移。如果你要手动触发一次故障转移来做演练可以在从节点上执行redis-cli -p 6383 cluster failover这个命令会让该从节点主动联系主节点协商角色切换整个过程无人工干涉适合在低峰期进行演练。测完以后你就可以把node-1重新拉起来docker start redis-node-1但注意重启回来的老节点在集群里已经是个“前任”它的角色信息要等集群重新同步。用cluster nodes查看它会显示为handshake或fail这时不用紧张等一会儿让它重新握手。如果一直处于握手状态还反复掉线直接把node-1从集群里清出去再重新添加方法我在第五章节细说。4.3 扩容与缩容操作集群建好以后总有容量不够的一天。回归到实际工作里你要给集群增加节点流程一般分四步启动新容器、加入集群节点、迁移槽位、验证数据分布。首先启动一个全新的节点容器端口选6385docker run -d --name redis-node-7 --net host \ -v /usr/local/redis-cluster/node-7/conf/redis.conf:/etc/redis/redis.conf \ -v /usr/local/redis-cluster/node-7/data:/data \ redis:7.0 \ redis-server /etc/redis/redis.conf redis-cli --cluster add-node 192.168.1.10:6385 192.168.1.10:6379add-node后面第一个参数是新节点的地址第二个参数是集群中任意一个在线节点的地址作用是让集群内部知道有个新成员来了。刚加入的节点不持有任何槽位它处于“空转”状态必须手动迁移一部分槽位给它redis-cli --cluster reshard 192.168.1.10:6379 \ --cluster-from 源节点ID \ --cluster-to 新节点ID \ --cluster-slots 1024执行过程中会询问你迁移的槽位来源。你可以填all从所有主节点均摊出一部分给新节点也可以指定某个特定的节点ID。填完后再确认执行redis-cli会一批一批地把槽位搬过去同时迁移对应的数据。这个过程在线上会消耗一定IO建议低峰期操作。缩容则相对简单先把它负责的槽位迁移走然后redis-cli --cluster del-node 192.168.1.10:6379 节点IDdel-node执行完成后该节点就彻底退出集群了。注意如果该节点还持有槽位del-node会拒绝执行所以缩容前必须先迁移干净。如果执行时报错可以用cluster nodes找到节点ID再核对一遍很多时候是节点ID找错了。5. 常见问题排查与避坑记录5.1 集群创建卡住的典型原因我搭建过程中最常用的一个排查对象就是“Waiting for the cluster to join”这个提示。它出现在redis-cli --cluster create执行以后然后程序一直卡着不动也不报错也不继续非常磨人。这个问题的根源通常是集群内部节点之间互相握手但Gossip所依赖的集群总线端口没有打通。你可以先检查一下所有节点的总线端口是否监听ss -tlnp | grep 1637看到16379到16384都已经LISTEN才正常。如果在Linux上配了firewalld或iptables还得确认这些端口放行。优先级最高的是如果你在云服务器上搭那安全组里也必须放行我亲眼见过有人本地全通、云端死活不join最后在控制台多开了个端口就解决了。另一个原因跟DNS有关。有些Docker网络环境会改写容器的/etc/hosts导致节点用主机名解析别人时拿到错误IP。host模式一般没有这个问题如果你用了自定义bridge网络还卡握手检查容器内部是否能够互相ping通。5.2 节点重挂载与集群脑裂问题节点宕机恢复以后重新加入集群经常出现不稳定的现象。比如旧节点的操作被拒、死循环握手等。这时候最省事的办法就是先把节点踢出去清理干净再拉回来。踢节点的命令之前讲过redis-cli --cluster del-node 192.168.1.10:6384 节点ID踢完以后进入该容器的数据目录把旧的nodes.conf和AOF文件挪走或者删除rm -rf /usr/local/redis-cluster/node-6/data/nodes.conf这个文件一旦存在新启动的实例会沿用旧的集群身份信息跟集群当前记录的节点ID对不上就会一直抱团失败。清完之后重新启动容器再用add-node把它加回去就行。脑裂问题听起来吓人但一般出现在集群划分成两个局部小组、互相丢失通信又各自继续运行的情况下。对付这个问题的关键不在于事后补救而在于配置上把cluster-node-timeout和cluster-replica-validity-factor调合理。生产环境建议不要让验证因子过大也就是尽量避免老干部分裂以后长时间不响应。5.3 生产环境的几个关键提醒到这里你的集群已经能跑、能切换、能扩容了但我还是要掏心窝子说几条生产级的经验第一容器配置里一定要加资源限制别让容器无上限地吃内存。Docker可以为容器设置memory limit没限制的话一个OOM能拉跨整个宿主机到时候一起走的就不只是Redis了。第二数据目录必须挂载到宿主机持久化磁盘绝不能放在容器可写层。容器一旦被删可写层连带消失数据全部报销。这一点我强调一万遍也不嫌多网上不少人搭完集群容器升级或者误删数据直接蒸发那真是欲哭无泪。第三集群不是解决一切问题的银弹。单个Redis实例本身已经很优秀绝大多数场景下单机加主从已经够用。真正需要集群时是数据量过百G、写入QPS持续走高、单机内存和带宽成为瓶颈之后。盲目堆集群只会增加复杂度和运维成本。第四追加一个基于精简运维视角的习惯给每个节点配置健康检查。比如用Prometheus的redis_exporter采集集群指标主从切换、槽位失衡、延迟飙升时及时告警。没有监控的集群就跟没装仪表的飞机一样看似平稳实际全靠感觉。最后再分享一个我实际操作中的体会。很多人以为集群搭完就完事大吉其实真正的考验是故障演练。我从这套docker方案里最受益的一点就是随便删节点、随便破坏配置、随便折腾数据几十秒重启全部恢复。你完全可以把这套环境当做一个“分布式系统实验室”反复练习主从切换、槽位迁移、节点重挂载练到闭着眼都能处理为止。等你把这里面的每个坑都踩过一遍再到生产环境里去管真正的集群底气完全不一样。
返回列表