如何在香港服务器的 CentOS 7 环境里把 Redis 哨兵模式跑稳:一次“购物车丢了”的复盘到落地
技术教程 2025-09-18 13:48 178


那天晚上 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 哨兵高可用。