
那天晚上 23:47,我正准备下班,工单里忽然涌进一堆“购物车数据丢失”的反馈。前端监控显示结算页的 cart:item_count 急剧下降,应用日志里满屏的 ECONNREFUSED 127.0.0.1:6379。我们当时只有一台单点 Redis,当机后 systemd 自启没拉起来,AOF 也没开——典型“能跑就行”的临时架构,终于在促销流量下露了馅。
那一夜,我决定把 Redis 高可用彻底一次性解决掉:同城(香港机房)三节点 Redis + Sentinel 仲裁,保证主挂从顶、秒级恢复、不丢购物车。下面就是我第二天到第三天在 CentOS 7 上一步步落地的完整过程、踩坑和优化细节。
拓扑与硬件:先把“地基”打牢
拓扑设计
- 三台服务器、同城不同机柜/不同 PDU(降低同源风险)
- 1 主 2 从(其实是 1 主 1 从 + 1 只跑 sentinel 的仲裁也行,但我更偏好“三个都是 redis+sentinel”,灵活性更高)
- 同一 VLAN,静态内网 IP。应用通过 Sentinel 名称发现主节点。
机器与网络参数(实测稳定的组合)
| 角色 | 主机名 | 内网 IP | CPU | 内存 | 磁盘 | 网卡 | 机柜/电源 | 备注 |
|---|---|---|---|---|---|---|---|---|
| redis-01(初始 master) | hk-redis-01 | 10.10.0.11 | Xeon E5-2697 v4(或同档) | 64GB | NVMe 1TB x2 (RAID1) | 2x10GbE | A 柜 / PDU-A | 跑 redis + sentinel |
| redis-02(replica) | hk-redis-02 | 10.10.0.12 | 同上 | 64GB | NVMe 1TB x2 (RAID1) | 2x10GbE | B 柜 / PDU-B | 跑 redis + sentinel |
| redis-03(replica) | hk-redis-03 | 10.10.0.13 | 同上 | 64GB | NVMe 1TB x2 (RAID1) | 2x10GbE | C 柜 / PDU-C | 跑 redis + sentinel |
说明:CentOS 7 老,但稳。NVMe 用 RAID1 主要是顶单盘故障,AOF everysec + NVMe 可把落盘延迟压很低。
架构取舍:为什么选 Sentinel 而不是 Cluster?
- 业务形态:购物车是“单数据集、Key 不大、写多读多、强一致偏好”。暂不需要分片。
- 目标:高可用 + 自动主备切换,应用侧通过 sentinel 透明感知主从变更。
- 结论:用 Redis Sentinel 比 Redis Cluster 更轻量,心智负担更低;未来如果需要分片再做迁移。
部署前系统与内核调优(CentOS 7)
我先把三台机器统一做了系统基线,保证 Redis 不被“系统默认值”拖后腿。
# 1) 基础包
yum install -y epel-release yum-utils chrony lsof jq
# 2) 关闭透明大页(THP)
echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled
echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag
grep -q transparent_hugepage /etc/rc.local || cat >> /etc/rc.local <<'EOF'
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
chmod +x /etc/rc.local
# 3) 内核参数(网络队列、连接 backlog、内存过量分配等)
cat > /etc/sysctl.d/99-redis.conf <<'EOF'
vm.overcommit_memory = 1
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 1000000
EOF
sysctl --system
# 4) limits(句柄数)
cat > /etc/security/limits.d/redis.conf <<'EOF'
* soft nofile 1000000
* hard nofile 1000000
EOF
# 5) 时间同步
systemctl enable --now chronyd
# 6) firewalld 基本放行(按需)
yum install -y firewalld
systemctl enable --now firewalld
firewall-cmd --permanent --add-port=6379/tcp
firewall-cmd --permanent --add-port=26379/tcp
firewall-cmd --reload
# 7) SELinux(视环境,生产我一般改成 permissive)
setenforce 0
sed -ri 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
安装 Redis 7.x(Remi 源)并准备多实例
CentOS 7 自带的 Redis 太老,我用 Remi 源安装较新的稳定版。
yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum install -y yum-utils
yum-config-manager --enable remi
yum install -y redis
安装后主配置在 /etc/redis.conf,可直接用。redis-sentinel 与 redis-server 是同一二进制,启动参数不同。
配置主从(以 redis-01 为初始 master)
/etc/redis.conf(master 节点示例:redis-01)
bind 0.0.0.0
protected-mode yes
port 6379
# 安全
requirepass "S3curePassw0rd!"
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
# 持久化:购物车我偏向 AOF everysec,兼顾 RDB 以便快速冷启动
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite no
aof-use-rdb-preamble yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
# 资源与性能
tcp-backlog 65535
timeout 0
tcp-keepalive 300
daemonize no
supervised systemd
maxmemory 24gb
maxmemory-policy volatile-lru
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
dir /var/lib/redis
logfile /var/log/redis/redis.log
/etc/redis.conf(replica 节点示例:redis-02 / redis-03)
# 与 master 基本一致,额外指定主节点与认证
replicaof 10.10.0.11 6379
masterauth "S3curePassw0rd!"
# 读写策略:根据业务,购物车我强制应用只写主、只读主,避免读旧
replica-read-only yes
- 坑 1:masterauth/requirepass 不一致会导致复制失败,INFO replication 里会看到 auth 错误。
- 坑 2:maxmemory-policy 对购物车建议 volatile-lru(只淘汰有 TTL 的 Key),否则可能误删无 TTL 的会话数据。
启动与自启
systemctl enable --now redis
systemctl status redis
配置 Sentinel:三台都跑,形成仲裁
我把 三台都配上 sentinel,保证仲裁有 3 票,容忍 1 节点故障。
/etc/redis-sentinel.conf(三台都类似)
port 26379
bind 0.0.0.0
daemonize no
supervised systemd
dir /var/lib/redis
# 监控一个名为 mymaster 的主,quorum=2 表示至少两票认定故障
sentinel monitor mymaster 10.10.0.11 6379 2
# 主从认证(与 redis.conf 保持一致)
sentinel auth-pass mymaster "S3curePassw0rd!"
sentinel auth-user mymaster default # 若启用 ACL,可指定用户
# 故障检测与切换参数(实测均衡)
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
# 客户端通知(可选,写事件到日志/频道)
sentinel notification-script mymaster /usr/local/bin/sentinel-notify.sh
notification-script 我写了一个简单的脚本打 webhook/企业微信,这里略过细节。
systemd 单元(有的包自带,如果没有就加)
/etc/systemd/system/redis-sentinel.service
[Unit]
Description=Redis Sentinel
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/redis-sentinel /etc/redis-sentinel.conf --supervised systemd
User=redis
Group=redis
LimitNOFILE=1000000
TimeoutStartSec=0
Restart=always
[Install]
WantedBy=multi-user.target
启动:
systemctl daemon-reload
systemctl enable --now redis-sentinel
systemctl status redis-sentinel
健康检查与基础验证
1)复制状态
redis-cli -a 'S3curePassw0rd!' INFO replication | egrep 'role|connected_slaves|master|slave'
2)Sentinel 感知与主节点发现
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL slaves mymaster
3)模拟主挂
在 redis-01:
systemctl stop redis
在任一 sentinel:
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 应几秒内从 10.10.0.11 切到 10.10.0.12(或 10.10.0.13)
我的实测数据(业务低峰)
| 场景 | down-after | 选主耗时 | 总体恢复(客户端可写) |
|---|---|---|---|
| 正常负载,主直接停服务 | 5s | 1.5~3s | 6.5~9s |
| 高负载,AOF 重写中 | 5s | 2~5s | 7~12s |
提示:failover 期间写会失败,所以应用层要做重试 or 幂等保障。我们设置了 3 次指数退避重试,平均对用户无感。
业务侧落地:购物车 Key 模型与 TTL 策略
购物车我用 hash 存储,按用户 ID 分 Key,并设置 TTL(比如 7 天)。这样在 volatile-lru 下,压力大时优先淘汰最近少访问的临时购物车,不会伤及无 TTL 的关键数据。
- Key:cart:{uid}
- Field:{sku_id} → 数量/属性 JSON
- TTL:EXPIRE cart:{uid} 604800
示例(Node.js):
// ioredis + sentinel
const Redis = require('ioredis');
const client = new Redis({
sentinels: [
{ host: '10.10.0.11', port: 26379 },
{ host: '10.10.0.12', port: 26379 },
{ host: '10.10.0.13', port: 26379 },
],
name: 'mymaster',
password: 'S3curePassw0rd!',
// 我们只写主,读也走主,避免读旧
role: 'master',
sentinelRetryStrategy: times => Math.min(times * 1000, 5000),
maxRetriesPerRequest: 3,
});
async function addToCart(uid, sku, qty) {
const key = `cart:${uid}`;
// 原子自增:用 Lua 保证 hash 设置与过期同时成功
const lua = `
redis.call('HINCRBY', KEYS[1], ARGV[1], ARGV[2])
redis.call('EXPIRE', KEYS[1], ARGV[3])
return 1
`;
return client.eval(lua, 1, key, sku, qty, 7 * 24 * 3600);
}
坑 3:客户端直连主从 IP 容易写到从库。一定通过 Sentinel 的 name 接入,让驱动在主从切换时自动更新连接。
安全与合规:别让“开放的 6379”变成下一个事故
- 只对内网开放 6379/26379,外网严格禁入。
- 开启 requirepass/masterauth,危险命令 rename-command 隐藏。
- 日志与 AOF/RDB 按天归档并异地备份(我们用对象存储 + rclone 同步到另一个区域)。
- 将 CONFIG, SHUTDOWN 等命令改名或禁用,运维通过堡垒机 + Ansible 执行。
观测与告警:问题要“预警式”暴露
我接了 redis_exporter + Prometheus + Grafana,关键监控项:
| 指标 | 阈值/关注点 | 说明 |
|---|---|---|
uptime_in_seconds |
异常重启 | 异常抖动及时看日志 |
connected_clients |
阈值 > 2/3 高水位 | 连接泄漏/风暴 |
instantaneous_ops_per_sec |
峰值与均值偏差 | 流量突变 |
latency_ms(基于 LATENCY 采样) |
> 10ms 抖动 | THP/磁盘抖动/GC |
aof_current_size / rewrite 指标 |
大幅上升 | 触发重写窗口 |
master_link_status(从库) |
down 告警 |
复制中断 |
role 变更事件 |
通知应用侧 | 触发灰度开关/降级策略 |
压测与演练:不上“演练”的高可用都是幻觉
- 数据准备:100 万个用户、每人 3~10 个 SKU,写入后设 TTL。
- QPS 目标:写 10k/s,读 30k/s(压测工具用 memtier_benchmark)。
- 演练脚本:在高峰压测 10 分钟后 systemctl stop redis,观察 Sentinel 切主与客户端恢复时间;随后 systemctl start redis 观察自动回切策略(我不建议自动回切,让它保持新主,老主作为从库)。
演练结果(节选)
| 轮次 | 写 QPS | 读 QPS | 切主耗时 | 客户端错误比 | 数据一致性 |
|---|---|---|---|---|---|
| 1 | 9.5k | 28k | 7.2s | 0.18% | OK(无显著丢失) |
| 2(AOF 重写中) | 9.2k | 27k | 10.8s | 0.43% | OK(有极少重试) |
要点:客户端重试 + 幂等,避免用户“加购失败”;购物车是弱一致业务,短暂不可用优先于脏写。
运维手册(我自己在 Confluence 上的条目摘录)
日常
- 每周检查 redis.log/sentinel.log 中的 -sdown/-odown 事件是否异常。
- 每周验证一次 SENTINEL get-master-addr-by-name mymaster 三机一致性。
- 每月恢复演练:AOF 恢复、RDB 冷启动恢复各一次。
- 定期手动触发 BGREWRITEAOF 窗口在低峰时段。
应急
- 故障发生:先确定是否是 network partition,再看 Sentinel 是否已切主;严禁手工双写。
- Split-brain 风险:如果网络抖动导致双主,先隔离旧主(停服务/隔离网口),再由从库接管。
- 复制追赶:落后太多时,直接全量复制;磁盘 IO 吃紧时改到低峰执行。
我踩过的坑(以及我怎么把坑填平)
- THP 未关:延迟随机飙升,AOF rewrite 更明显。关掉 + 复查开机脚本。
- masterauth 漏配:复制失败但是不明显,Sentinel 试图切主会反复。把 requirepass/masterauth 放在同一个密钥管理里。
- firewalld 忘放行 26379:Sentinel 互相看不见,永远达不到 quorum。统一做基线模板。
- logrotate 忽略 AOF:磁盘被 AOF 顶满。设置磁盘阈值告警与 AOF 周期重写;同时分区空间富余(>= 3 倍 RDB)。
- 客户端直连旧主:漏用 Sentinel 名称接入,切主后写失败。统一驱动与连接模板,发版拦截检查。
FAQ:几个关键参数我怎么选的?
- sentinel down-after-milliseconds:我选 5000ms,折中“误判”与“恢复速度”。业务对 5~10s 不可写可接受。
- sentinel parallel-syncs:1,避免切主瞬间从库同步对磁盘/网络的锤击。
- appendfsync:everysec,购车允许 1s 理论丢失窗口,实际客户端重试兜底。
- maxmemory-policy:volatile-lru,配合购物车 TTL 策略最友好。
- replica-read-only:yes,统一通过主读写,避免读旧。读多可以另外建读缓存层。
第二次凌晨,告警响了,但我没再心慌
两周后,又是凌晨,机房 A 柜 PDU 例行维护,hk-redis-01 电源掉了一路。告警响起,我看着 Grafana 上主从切换的曲线,7 秒后写入恢复,应用层错误峰值只有寥寥几百个请求,重试全都成功。运营群里一句“购物车没问题”,我才关上电脑,去给自己加了杯冰美式。
这套 Sentinel 架构不是“银弹”,但它足够稳、够简单、易演练。对“购物车”这种核心但不需要分片的数据集来说,在 CentOS 7 的香港机房里,它配得上“省心”二字。
附:可复制的“一次性部署清单”
三台都执行(IP、角色按需替换)
# 系统基线(参考前文)
# ...
# 安装 Redis
yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum install -y yum-utils
yum-config-manager --enable remi
yum install -y redis
# 配置 /etc/redis.conf(主/从各自版本)
vi /etc/redis.conf
# 启动 redis
systemctl enable --now redis
# 配置 /etc/redis-sentinel.conf(三台一致)
vi /etc/redis-sentinel.conf
# systemd 单元(如需)
cat >/etc/systemd/system/redis-sentinel.service <<'EOF'
[Unit]
Description=Redis Sentinel
After=network.target
[Service]
Type=notify
ExecStart=/usr/bin/redis-sentinel /etc/redis-sentinel.conf --supervised systemd
User=redis
Group=redis
LimitNOFILE=1000000
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now redis-sentinel
# 基本验证
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
如果你也刚好在夜里被“购物车丢了”叫醒,照着这篇一步步做,第二天你就会拥有一套可演练、可恢复、可量化的 Redis 哨兵高可用。