写这篇实战文章前,先说个背景。我之前有段时间频繁搭建Redis Cluster,一开始按官方文档手搓,一次两次还行,次数多了就发现整个流程里有大量重复劳动:每台机器要写单独的配置文件、起服务、敲 create 命令、分配主从……期间只要手滑把某个 IP 打错、端口写错,整个集群直接起不来,排查起来特别磨人。后来我把整个流程固化成了一个 shell 脚本,参数输对、回车一敲,几分钟内自动完成从节点初始化到槽位分配的全过程。今天把脚本的设计思路、完整源码和踩坑记录全部分享出来,希望对正在折腾 Redis Cluster 的同学有参考价值。
这个标题“shell脚本一键创建redis cluster集群”对应的其实是运维自动化的一个经典场景,特别适合这三类人看:一是刚开始接触 Redis Cluster、想看完整搭建流程的新手;二是需要在多台机器上反复部署集群、想把重复工作收敛掉的运维;三是做本地测试环境、想快速起一套集群验证业务逻辑的开发。核心思路不复杂,就是把手工操作翻译成变量、循环、函数,让 shell 替我们做那些枯燥且容易出错的重复步骤。
1. 为什么要把集群搭建脚本化
1.1 手工部署 Redis Cluster 的痛点
手工部署一个三主三从的集群,至少要经历这些步骤:准备六台机器或六个端口、为每个节点修改 redis.conf、逐个启动 redis-server、等全部节点就绪后用 redis-cli 执行 create 命令、再处理可能的告警和绑定问题。听起来不多,但每一步都有隐蔽的坑。
我见过最典型的翻车现场是 bind 配置。很多人拿着同一个配置模板去改,结果漏改了某台机器的 bind 地址,节点启动很顺利,等到 create 的时候直接报[ERR] Node ... is not empty或者 connect 超时。这种错误定位非常费时,因为问题不在语法,而在节点之间的网络连通性。还有一类坑是端口规划混乱:Redis Cluster 每个节点除了业务端口,还有一个端口 + 10000 的集群总线端口,如果业务端口从 7000 开始,总线端口就是 17000。防火墙只放行了前者,就会出现节点能启动但互相 meet 失败、一直处于waiting for the cluster to join的状态。
手工操作多,人就会疲劳,疲劳就会出错。脚本本质上是把操作流程标准化,让机器去执行那些确定性很高的动作,同时通过 wait、检查、日志输出,把过程中可能出现的异常提前暴露出来,这比等 create 命令报错再去排查效率高得多。
1.2 脚本化方案的预期目标
我对这个脚本的定位是“一个命令、一段日志、一次成功”。具体来说,它需要满足几个硬性指标:
- 只需要传入节点 IP 列表和一个起始端口,脚本自动推导出所有端口,包括业务端口和总线端口。
- 每个节点的 redis.conf 由脚本动态生成,避免手工复制模板导致的配置漂移。
- 节点启动后等待端口就绪,再做集群初始化,而不是启动完立刻执行 create,保证时序稳定。
- 主从角色分配用 redis-cli 自带的 cluster replicas 机制,让官方工具去决定谁是谁的从节点,减少人为规划带来的主从不均匀问题。
- 脚本执行过程输出清晰的日志:生成了什么配置、启动哪个节点、当前状态是什么、最终 cluster info 的结果如何。
这套设计目标看着简单,实际写的时候要考虑很多细节,比如端口推导、IP 去重、等待就绪的时间、失败时的退出策略。接下来的章节我会逐一展开。
2. 脚本核心设计:参数、拓扑与配置生成
2.1 端口规划与拓扑推导
Redis Cluster 至少需要三个主节点,生产环境推荐三主三从。端口规划上我习惯从 7000 开始,依次递增,业务端口 7000-7005,对应总线端口 17000-17005。
脚本里我定义一个基础端口变量,通过下标计算:
BASE_PORT=7000 # 第 i 个节点的业务端口 = BASE_PORT + i # 第 i 个节点的总线端口 = BASE_PORT + i + 10000为什么单独留出 10000 段?这是 Redis Cluster 的硬性约定,集群节点间通过总线端口做 gossip 通信,端口偏移固定为 10000,不能随便改。这也是很多新手部署失败的第一道槛,脚本里我不需要用户管总线端口,直接算出来,省一个心智负担。
节点 IP 列表我设计成从命令行传入,支持任意数量。脚本内部会统计 IP 总数,再决定怎么分配主从。比如传入 6 个 IP,就是三主三从;传入 3 个 IP,默认就是三主无从,测试阶段也够用。
2.2 脚本框架与流程设计
整体流程我用四个函数组织:
check_env # 检查 redis-server / redis-cli 是否存在、版本是否支持 cluster generate_conf # 为每个节点生成 redis.conf start_nodes # 逐个启动 redis-server 并等待端口就绪 create_cluster # 执行 redis-cli --cluster create 并输出集群状态四个函数顺序执行,任何一步失败都直接退出,避免带着错误状态继续往后跑。主流程末尾加一个 verify_cluster 函数,通过CLUSTER INFO和CLUSTER NODES返回关键状态,比如是否 all ok、槽位分配是否完整、主从是否一一对应。
这个框架的好处是职责清晰,每个函数都能独立测试。我在实际开发时是先把 start_nodes 跑通,再调 create_cluster,出了问题能快速定位是哪一段的锅。
2.3 配置文件生成规则
每个节点的 redis.conf 大同小异,差异点主要是端口、节点 ID 文件、数据目录和 pid 文件。我在脚本里用一个模板函数输出配置内容:
generate_conf() { local port=$1 local ip=$2 local dir="/data/redis/cluster_${port}" mkdir -p "${dir}" cat > "${dir}/redis.conf" <<EOF port ${port} bind ${ip} 127.0.0.1 cluster-enabled yes cluster-config-file nodes-${port}.conf cluster-node-timeout 5000 appendonly yes appendfilename appendonly-${port}.aof daemonize yes pidfile /var/run/redis_${port}.pid logfile "${dir}/redis_${port}.log" dir "${dir}" protected-mode no EOF }这里有一个需要解释的关键项:bind为什么要写具体 IP 而不是0.0.0.0?如果 bind 0.0.0.0,所有接口都监听,看起来省事,但 Redis Cluster 在 meet 时会拿本机地址去报告给其他节点,如果本机有多个网卡,可能报错或者导致其他节点拿着错误的地址来连接。指定 IP 之后节点间地址发现就非常明确,这也是官方推荐的做法。
2.4 为什么不用 Docker 而是直接跑在宿主机上
很多人会问:为什么不直接用 docker-compose 起多个容器做集群?两种方案各有适用场景,我分享下自己的取舍。
Docker 方案适合单机模拟多节点,快速起测试环境,隔离性好,但存在几个问题:一是跨主机时网络模式要额外配置,host 网络 + 不同端口还好,bridge 网络就要处理容器间 DNS 和端口映射;二是数据目录在容器里,日志排查要多一层 docker logs 的包装,不够直接;三是有时候需要模拟真实网络环境,比如故意断掉某个节点的网络做故障演练,容器方案反而不好控制。
宿主机直接跑则保留了最原始的运维操作方式,所有日志、数据文件、进程状态都是裸的,问题定位直观。这个脚本的主要使用场景就是多台物理机或云主机的批量部署,所以我选择宿主机方案,这也是 shell 脚本最舒服的战场。
3. 一键脚本实现与核心环节拆解
3.1 完整脚本源码
下面是我的完整实现,为了便于阅读我加上行号并保留了详细注释。脚本开头有一段变量区域,运行时只需要改 IP 列表。
#!/bin/bash #===================================================================== # 一键创建 Redis Cluster 集群 # 用法: ./create_redis_cluster.sh "192.168.1.10 192.168.1.11 192.168.1.12 192.168.1.13 192.168.1.14 192.168.1.15" #===================================================================== set -e # ---------- 参数与配置区 ---------- IP_LIST=$1 BASE_PORT=${BASE_PORT:-7000} REDIS_HOME=${REDIS_HOME:-/usr/local/redis} DATA_ROOT=${DATA_ROOT:-/data/redis} START_INDEX=0 if [ -z "${IP_LIST}" ]; then echo "[ERROR] 请提供节点 IP 列表" echo "示例: ./create_redis_cluster.sh \"192.168.1.10 192.168.1.11 ...\"" exit 1 fi # 将 IP 列表转为数组 IFS=' ' read -r -a NODES <<<"${IP_LIST}" NODE_COUNT=${#NODES[@]} echo "[INFO] 共检测到 ${NODE_COUNT} 个节点" # ---------- 函数区 ---------- check_env() { echo "[CHECK] 检查 redis-server / redis-cli 是否存在" if [ ! -x "${REDIS_HOME}/bin/redis-server" ]; then echo "[ERROR] 未找到 ${REDIS_HOME}/bin/redis-server,请检查 REDIS_HOME 配置" exit 1 fi if [ ! -x "${REDIS_HOME}/bin/redis-cli" ]; then echo "[ERROR] 未找到 ${REDIS_HOME}/bin/redis-cli" exit 1 fi local version version=$("${REDIS_HOME}/bin/redis-server" --version) echo "[INFO] Redis 版本: ${version}" local cluster_support cluster_support=$("${REDIS_HOME}/bin/redis-cli" --cluster help 2>&1 | head -n 1) if [ -z "${cluster_support}" ]; then echo "[ERROR] redis-cli 可能不支持集群管理命令,请升级 Redis 至 5.0 以上" exit 1 fi echo "[INFO] redis-cli 集群命令可用" } check_port_free() { local port=$1 if ss -lnt | grep -q ":${port} "; then echo "[ERROR] 端口 ${port} 已被占用,请更换 BASE_PORT 或释放端口" exit 1 fi } generate_conf() { local ip=$1 local port=$2 local dir="${DATA_ROOT}/cluster_${port}" mkdir -p "${dir}" echo "[CONF] 生成 ${dir}/redis.conf" cat > "${dir}/redis.conf" <<EOF port ${port} bind ${ip} 127.0.0.1 cluster-enabled yes cluster-config-file nodes-${port}.conf cluster-node-timeout 5000 appendonly yes appendfilename appendonly-${port}.aof daemonize yes pidfile /var/run/redis_${port}.pid logfile "${dir}/redis_${port}.log" dir "${dir}" protected-mode no EOF echo "[CONF] 配置文件生成完成: port=${port}, ip=${ip}" } start_nodes() { local idx=0 for ip in "${NODES[@]}"; do local port=$((BASE_PORT + idx)) check_port_free "${port}" echo "[START] 启动节点 ${ip}:${port}" "${REDIS_HOME}/bin/redis-server" "${DATA_ROOT}/cluster_${port}/redis.conf" idx=$((idx + 1)) done echo "[WAIT] 等待所有节点端口就绪" idx=0 for ip in "${NODES[@]}"; do local port=$((BASE_PORT + idx)) local retry=0 while [ "${retry}" -lt 30 ]; do if ss -lnt | grep -q ":${port} "; then break fi retry=$((retry + 1)) sleep 1 done if [ "${retry}" -ge 30 ]; then echo "[ERROR] 节点 ${ip}:${port} 在 30 秒内未就绪,请检查日志" exit 1 fi echo "[OK] 节点 ${ip}:${port} 端口已监听" idx=$((idx + 1)) done } build_node_list() { local nodes_str="" local idx=0 for ip in "${NODES[@]}"; do local port=$((BASE_PORT + idx)) nodes_str="${nodes_str} ${ip}:${port}" idx=$((idx + 1)) done echo "${nodes_str# }" } create_cluster() { local node_list node_list=$(build_node_list) echo "[CREATE] 开始创建集群, 节点列表: ${node_list}" local replica_count=1 if [ "${NODE_COUNT}" -lt 6 ]; then replica_count=0 echo "[INFO] 节点数少于 6,创建无从集群" fi "${REDIS_HOME}/bin/redis-cli" --cluster create \ ${node_list} \ --cluster-replicas ${replica_count} \ --cluster-yes echo "[CREATE] 集群创建命令执行完毕" } verify_cluster() { local first_ip=${NODES[0]} local first_port=$((BASE_PORT + 0)) echo "[VERIFY] 集群状态检查: ${first_ip}:${first_port}" "${REDIS_HOME}/bin/redis-cli" -h "${first_ip}" -p "${first_port}" cluster info echo "------------------------------" "${REDIS_HOME}/bin/redis-cli" -h "${first_ip}" -p "${first_port}" cluster nodes } # ---------- 主流程 ---------- check_env start_nodes create_cluster verify_cluster echo "[DONE] 全部完成"3.2 关键函数逐段解读
check_env里我最看重的是版本检查。Redis 4.x 时代的集群创建要借助一个 ruby 脚本,也就是网上各种教程里出现的redis-trib.rb,非常折磨人。Redis 5.0 之后官方把创建集群的功能集成了redis-cli --cluster子命令里,脚本最大的简化就来源于此。如果你的环境还是 4.x,强烈建议直接升级,别和旧方案纠缠。
generate_conf里有一个容易被忽略的细节,就是cluster-config-file的名字必须和端口对应。每个节点启动后会生成一个nodes-7000.conf之类的文件,记录本节点视角下的集群拓扑。如果你复用同一个基础配置文件没改这个字段,多个节点抢同一个文件,会出现各种奇怪的互相覆盖问题。我在脚本里用nodes-${port}.conf保证文件唯一性,这是实战出来的一条重要经验。
start_nodes里的等待循环是很多人写脚本时不会想到的。Redis Server 是 daemonize 方式后台启动,命令返回不代表端口已经正常监听。如果脚本立刻执行 create 命令,很可能赶上某个节点还在初始化,白白报一次连接失败。我用ss -lnt | grep -q ":${port} "做轮询,最多等 30 秒,保证所有节点都准备好再进入下一步。注意 grep 的模式,端口后面带着空格,避免匹配到类似 70001 这种包含关系。
build_node_list的作用是把 IP 数组和端口拼成 redis-cli 要求的格式。这地方还有一个细节:从cat >的配置阶段到启动阶段,idx必须保持一致,否则会出现配置生成给了 7000 端口,启动时却拿着 7001 端口去找配置文件,直接失败。我脚本里两个循环都是独立的idx,看起来没毛病,但实际写代码时很容易复制粘贴后忘记重置。
3.3 从执行到完成的完整过程实录
我在一台测试环境里用三个 IP 模拟了一键建集群的完整输出:
$ ./create_redis_cluster.sh "192.168.1.31 192.168.1.32 192.168.1.33" [CHECK] 检查 redis-server / redis-cli 是否存在 [INFO] Redis 版本: Redis server v=7.0.12 ... [INFO] redis-cli 集群命令可用 [CONF] 生成 /data/redis/cluster_7000/redis.conf [CONF] 生成 /data/redis/cluster_7001/redis.conf [CONF] 生成 /data/redis/cluster_7002/redis.conf [START] 启动节点 192.168.1.31:7000 [START] 启动节点 192.168.1.32:7001 [START] 启动节点 192.168.1.33:7002 [WAIT] 等待所有节点端口就绪 [OK] 节点 192.168.1.31:7000 端口已监听 [OK] 节点 192.168.1.32:7001 端口已监听 [OK] 节点 192.168.1.33:7002 端口已监听 [CREATE] 开始创建集群 ... [VERIFY] 集群状态检查 cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384三个节点创建出的就是最简单的 master-only 集群,16384 个槽位刚好平均分配到三个主节点上,每个主节点拿 5462 个左右。如果要高可用,就传六个 IP 进去,脚本会自动为每个主节点分配一个从节点。
4. 高可用验证与常见问题速查
4.1 怎么确认集群真的健康
脚本输出的 verify 结果里,最需要盯住的是两个字段:cluster_state必须是ok,cluster_slots_ok必须等于 16384。这两个字段说明槽位完整,没有丢失或重复分配。
另外cluster_nodes输出的每一行左列是一个 40 位的节点 ID;如果看到某些行开头是slave,后面跟着master的 ID,说明主从关系已经建立。我在实际运行后建议顺手做一次写入测试:
redis-cli -h 192.168.1.31 -p 7000 -c set foo bar # 返回 OK redis-cli -h 192.168.1.32 -p 7001 -c get foo # 返回 bar注意这里一定加-c参数,redis-cli 在集群模式下会自动把 key 重定向到正确的槽位节点。不加-c的话,连到非槽位节点会收到 MOVED 错误,很多新手在这一步就误以为集群有问题。
4.2 高可用切换测试
创建三主三从后,我建议顺手做一个主节点故障切换测试,验证集群真的具备高可用能力。
找到 7000 端口对应进程的 PID,直接 kill 掉:
kill -9 $(cat /var/run/redis_7000.pid)等十几秒,再查看集群状态:
redis-cli -h 192.168.1.31 -p 7001 cluster nodes正常情况下,原来的 5682 端口的从节点经过选举会变成新的主节点,cluster_state 依然 ok。这个测试验证的是 Redis Cluster 的自动故障转移机制,脚本本身并没有参与这个过程——但如果你在脚本化部署后不做这个验证,等到真正故障时才依赖这套机制,风险就大了。
4.3 从零清理环境
开发测试环境里,重建集群是常有的事。我的脚本没有内置清理命令,因为清理操作风险较大,不适合一键执行。我通常手动执行:
# 清理数据目录和进程 pkill -f "redis-server.*cluster_7" rm -rf /data/redis/cluster_7*注意pkill的模式不要写太宽,比如pkill redis-server可能误杀同一机器上其他业务的 Redis 实例。用cluster_7做模式匹配,精确锁定这个脚本创建的节点。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| create 时报 connect 超时 | 防火墙未放行端口 | 检查业务端口和 10000 偏移总线端口是否都放行 |
| create 时报 Node is not empty | 数据目录里有旧 AOF/RDB 文件 | 清空 /data/redis/cluster_7xxx 目录后重试 |
| 一直 waiting for the cluster to join | 总线端口被封 | 放行端口+10000 的 TCP 规则 |
| cluster_state 显示 fail | 部分节点挂掉或网络隔离 | 逐个检查节点进程、日志和网络连通性 |
| MOVED 错误 | 客户端没有使用集群模式 | 客户端连接加-c参数或启用集群模式 |
| 配置文件权限报错 | Redis 拒绝以 root 启动 | 用专用账号启动,或者配置参数允许 |
| 端口被占用 | 上一次部署未清理干净 | 用 ss -lnt 查占用进程并处理 |
这张表里,我重点加粗总线端口的排查。很多人只会放行业务端口,Redis Cluster 节点间通信全部走总线端口,它没放行时集群根本 join 不起来。
5. 脚本层面的避坑与后续扩展
5.1 对 bash 语法的防御性处理
shell 脚本调试很痛苦,几个关键点必须注意:变量未定义默认是空字符,不会直接报错,但行为完全偏离预期。所以set -e必须有,出现非零返回值直接退出,避免一路错下去。数组处理上用read -r -a,注意IFS必须在同一行设置,如果写成分号后面的独立语句,数组分割方式是错的,会把整个 IP 列表当成一个元素。
命令检测这块有一个细节,redis-cli --cluster help在 Redis 5.0 以下的旧版本会返回非零值,但我用了2>&1把错误输出捕获并读第一行,保证后续判断不会因为无输出而僵住。我实际在生产脚本里还会加一个对返回码的判断,这里为了清晰没有展开,但原理是一样的:对任何依赖外部命令的环节,都要假设它可能失败、可能输出非预期内容。
5.2 为什么选择 shell 而不是其他自动化工具
有人会问:这种场景用 Ansible、SaltStack、Python 不是更合适吗?我的回答是:看场景。
如果是几十台机器、复杂配置、需要定期变更和审计,Ansible 这类工具天然合适,它有 inventory、有变量、有幂等性。但如果只是三五台机器的一次性集群搭建,为它单独维护一套 playbook 有点重。Shell 脚本的好处是零依赖、直接运行、可读性好,而且 Redis Cluster 建集群的核心动作就那几条命令,shell 完全能覆盖。
另外,shell 脚本能让你对底层动作保持感知:生成了什么配置、起了什么进程、监听什么端口。如果是 Ansible,黑盒感更强,出了问题多一层 debug 成本。这不算优劣,是取舍。我在本文给出的是 shell 方案,如果你后续要大规模上生产,把它翻译成 playbook 也很容易,配置生成逻辑直接复用。
5.3 这个脚本还能怎么扩展
目前脚本能完成基础集群的创建,但实际业务中通常还有更多需求,按我的经验列出几个高价值的扩展方向:
一是把节点数参数化,既然 IP 列表能动态传入,节点数、副本数、槽位分配策略都可以从命令行读取。二是加上健康检查的循环监控,比如每 10 秒跑一次 cluster info,状态异常就输出告警,这个很适合做验收脚本。三是把创建命令替换成渐进式拓扑变更:先创建三个主节点,再逐个把从节点 add-node 进去。这样可以控制槽位迁移节奏,在不停服的前提下构建集群,适合线上扩容场景。
另外一个很实用的扩展是生成 redis-cli 命令的幂等判断。脚本重跑时检测到某节点已经有集群配置,就提示“节点已加入集群,是否强制重建”,避免因为手滑执行了脚本把现有集群搞乱。
5.4 最后的实测体会
我写这个脚本最深的体会是:复杂操作自动化的收益不在第一次运行,而在第五次、第十次运行。第一次跑脚本和手工搭建消耗的时间差不多,因为要调试、要处理环境差异。但跑通之后,后续每次部署的时间基本就是输入 IP 加等待输出,误差不超过两分钟。而且因为脚本把所有关键参数显式化,团队里其他人接手时也不用再翻文档猜配置。
我自己后续用的时候,还会在这个脚本基础上加一段用于生成 .env 文件的输出,把各个节点的 IP、端口、节点 ID 写到固定格式的文件里,方便上层业务系统读取。如果你也把 Redis Cluster 当成基础设施在重复使用,我建议尽快把这类流程脚本化,收益远大于前期那点投入。