Redis Cluster 与 Sentinel 高可用完全指南:从一次"3 哨兵全在同机房光纤挖断脑裂数据对账两天"看懂为什么 redis-cli cluster create 远远不够

2023 年我们公司业务量翻倍单 Redis 实例 32GB 内存撑不住决定上 Redis Cluster 6 主 6 从 18 个 hash slot 我们以为只是配置一下集群就行三个节点跑 redis-cli cluster create 自动分槽结果上生产第一周连续出 5 次 P1 故障第一种最让我傻眼的是有个开发用 MULTI EXEC 事务跨 slot 操作报错 CROSSSLOT Keys in request dont hash to the same slot 整个下单流程崩了我们才意识到 Cluster 模式下事务必须保证所有 key 在同一 slot 必须用 hashtag user 123 cart 这种写法第二种最难缠是 fail over 切主后客户端疯狂报 MOVED 错误日志爆炸排查发现 Jedis 客户端版本太老 cluster topology refresh 阈值默认是 5 次错误才刷新切主期间几千个请求全报错第三种最离谱是我们做 SCAN 操作以为像单机一样一次扫完结果发现 Cluster 的 SCAN 是 per-node 必须遍历所有主节点而且分页 cursor 是按节点维护的漏一个节点就漏一批 key 第四种最致命是 Sentinel 集群我们只部署了 3 个哨兵节点之一所在的机房网络抖动 quorum 从 3 个 majority 2 个变成 2 个 majority 2 个触发脑裂同时有 2 个 master 数据写两边恢复后差异手工对账第五种最莫名其妙是大 key 迁移我们有一个 hash 200 万字段 cluster slot 迁移走 MIGRATE 命令单次 timeout 30 秒整个迁移失败集群进入 fail 状态业务报 CLUSTERDOWN 半小时第六种最坑是我们用 KEYS pattern 调试直接把主节点扫死集群心跳超时被踢出 fail over 又来一遍

2023 年我们公司业务量翻倍 单 Redis 实例 32GB 内存撑不住 决定上 Redis Cluster 6 主 6 从 18 个 hash slot 我们以为只是配置一下集群就行 三个节点跑 redis-cli --cluster create 自动分槽 结果上生产第一周连续出 5 次 P1 故障第一种最让我傻眼的是有个开发用 MULTI/EXEC 事务 跨 slot 操作报错 CROSSSLOT Keys in request don't hash to the same slot 整个下单流程崩了我们才意识到 Cluster 模式下事务必须保证所有 key 在同一 slot 必须用 hashtag {user:123}:cart 这种写法;第二种最难缠是 fail over 切主后客户端疯狂报 MOVED 错误日志爆炸 排查发现 Jedis 客户端版本太老 cluster topology refresh 阈值 默认是 5 次错误才刷新 切主期间几千个请求全报错;第三种最离谱是我们做 SCAN 操作以为像单机一样一次扫完 结果发现 Cluster 的 SCAN 是 per-node 必须遍历所有主节点 而且分页 cursor 是按节点维护的 漏一个节点就漏一批 key;第四种最致命是 Sentinel 集群我们只部署了 3 个 哨兵节点之一所在的机房网络抖动 quorum 从 3 个 majority 2 个变成 2 个 majority 2 个 触发脑裂 同时有 2 个 master 数据写两边 恢复后差异手工对账;第五种最莫名其妙是大 key 迁移 我们有一个 hash 200 万字段 cluster slot 迁移走 MIGRATE 命令 单次 timeout 30 秒整个迁移失败 集群进入 fail 状态业务报 CLUSTERDOWN 半小时;第六种最坑是我们用 KEYS pattern 调试 直接把主节点扫死 集群心跳超时被踢出 fail over 又来一遍。真正能稳定撑住生产负载的 Redis 高可用是一个 Cluster 数据分片设计 + Sentinel 哨兵部署 + 客户端配置精调 + 大 key 治理 + 慢命令防御 + 持久化策略 + 监控告警 + 故障演练的完整方法论,任何一环失守都可能让你的 Redis 从"缓存层"变成"业务雪崩源"。本文从头梳理 Redis 高可用与 Cluster 部署的要点,Cluster 怎么设计 Sentinel 怎么部署 客户端怎么配 大 key 怎么治 慢命令怎么防 持久化怎么选 监控怎么搭,以及一些把 Redis 撑过亿级 QPS 要避开的工程坑。

问题背景:为什么"redis-cli cluster create"远远不够

很多团队上 Redis Cluster 跑通三节点就以为完事,但生产化 Cluster 远比想象的复杂:

  • Slot 设计与 hashtag:不懂 16384 slot 与 hashtag 的关系,事务/pipeline 直接崩。
  • Sentinel quorum 与脑裂:哨兵数量与 quorum 配置错,跨机房网络抖动直接脑裂。
  • 客户端 cluster 感知:Jedis/Lettuce/redis-py 不同客户端对 MOVED/ASK 的处理差异巨大。
  • 大 key 与热 key:单 key 几 MB 阻塞主线程,迁移 timeout,热 key 把单 shard 打爆。
  • 持久化选型:RDB / AOF / 混合的内存与恢复时间 trade-off 选错恢复几小时。
  • 慢命令防御:KEYS / SMEMBERS / LRANGE 大列表都是隐藏炸弹。

一 Cluster 数据分片与 Hashtag 设计

Redis Cluster 用 CRC16 把 key 映射到 16384 个 slot 槽 然后分配给各个 master。同一 key 必须在同一 slot 才能事务/pipeline 操作。生产中靠 hashtag 控制相关 key 共槽。

# 1 部署 6 节点 cluster(3 主 3 从)
# 各节点 redis.conf
"""
port 6379
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000      # 15s 节点超时
cluster-require-full-coverage no  # 关键 部分 slot 不可用时仍可服务
cluster-allow-reads-when-down no  # fail 时拒绝读 防脏数据
cluster-replica-no-failover no    # 允许从节点 failover
appendonly yes
appendfsync everysec
maxmemory 16gb
maxmemory-policy allkeys-lru
"""

# 2 创建集群
redis-cli --cluster create \
    10.0.1.10:6379 10.0.1.11:6379 10.0.1.12:6379 \
    10.0.1.13:6379 10.0.1.14:6379 10.0.1.15:6379 \
    --cluster-replicas 1 \
    --cluster-yes

# 3 检查 slot 分布
redis-cli --cluster check 10.0.1.10:6379
# [OK] 16384 slots covered

# 4 扩容(加新主从)
redis-cli --cluster add-node 10.0.1.16:6379 10.0.1.10:6379
redis-cli --cluster add-node 10.0.1.17:6379 10.0.1.10:6379 \
    --cluster-slave --cluster-master-id <new_master_id>
# reshard 把部分 slot 迁到新节点
redis-cli --cluster reshard 10.0.1.10:6379 \
    --cluster-from all \
    --cluster-to <new_master_id> \
    --cluster-slots 2048 \
    --cluster-yes

# 5 在线 reshard 注意事项
# - reshard 期间业务正常 但延迟略升
# - 大 key 迁移可能 timeout 必须提前清理
# - 跨机房 reshard 网络带宽吃紧 限速 --pipeline 10
# 6 Hashtag 让相关 key 同 slot
# 错误:user:123:cart 与 user:123:order 不同 slot 不能事务
r.mset({"user:123:cart": "...", "user:123:order": "..."})   # CROSSSLOT 错误

# 正确:用 {} 强制 hash 同一部分
r.mset({"{user:123}:cart": "...", "{user:123}:order": "..."})   # 同 slot OK

# 配合 pipeline / 事务
pipe = r.pipeline()
pipe.hset("{user:123}:cart", "item1", "...")
pipe.hset("{user:123}:order", "ord1", "...")
pipe.execute()

# 7 hashtag 设计原则
# - 业务粒度 必须事务一致的放一组 (用户级 / 订单级 / 会话级)
# - 不能放太大粒度 例如 {tenant:1} 会让一个租户所有 key 在一 slot 单节点崩
# - 不能放太细 失去事务能力

# 8 检查 hashtag 是否正确
def hashtag(key):
    """提取 hashtag 部分 用于 CRC16"""
    if "{" in key and "}" in key:
        start = key.index("{")
        end = key.index("}", start)
        if end > start + 1:
            return key[start+1:end]
    return key

print(hashtag("{user:123}:cart"))    # user:123
print(hashtag("user:123:cart"))      # user:123:cart (整个 key 参与 hash)

# 9 keyslot 命令验证
print(r.execute_command("CLUSTER KEYSLOT", "{user:123}:cart"))   # 5694
print(r.execute_command("CLUSTER KEYSLOT", "{user:123}:order"))  # 5694 同 slot

实战经验:cluster-require-full-coverage 必须设 no 否则一个 master 挂全集群拒服务;hashtag 设计是 Cluster 模式核心 必须按业务事务粒度规划;reshard 前必须清大 key 否则 MIGRATE timeout 集群进 fail 状态;扩容用 cluster add-node + reshard 不要直接改 nodes.conf;hashtag 太大会让单 slot 数据倾斜 太细失去事务能力 需平衡。

二 Sentinel 哨兵部署与脑裂防御

Sentinel 是 Redis 高可用的另一套方案 (非 Cluster) 适合中小规模主从+故障切换。错误配置最容易脑裂 多主同时写数据。

# 1 Sentinel 配置
# sentinel.conf
"""
port 26379
sentinel monitor mymaster 10.0.1.10 6379 2      # quorum=2 至少 2 个哨兵认同才切
sentinel down-after-milliseconds mymaster 5000  # 5s 无响应认定主观下线
sentinel failover-timeout mymaster 30000        # 30s failover 超时
sentinel parallel-syncs mymaster 1              # 一次只让 1 个从同步新主 防新主压力
sentinel auth-pass mymaster yourpassword

# 关键 防脑裂参数(在 master redis.conf)
min-replicas-to-write 1                         # 至少 1 个从同步成功才写
min-replicas-max-lag 10                         # 从滞后超 10s 不算
"""

# 2 哨兵数量与 quorum 设计原则
# - 至少 3 个哨兵 否则一个挂 quorum 不足
# - 推荐 5 个哨兵跨 3 机房 任一机房挂仍能 majority
# - quorum 设 N/2+1 (3 个哨兵设 2 5 个哨兵设 3)
#
# 反模式:
# 3 哨兵 quorum=2 但 2 个在同一机房 → 该机房挂 quorum 不足无法 failover
# 4 哨兵 quorum=2 → 网络分区 2v2 可能脑裂

# 3 跨机房 5 哨兵部署
"""
机房 A: master + sentinel-1 + sentinel-2
机房 B: replica-1 + sentinel-3 + sentinel-4
机房 C: replica-2 + sentinel-5

quorum=3 (5/2+1)
机房 A 全挂: B+C 还有 3 个哨兵 quorum 满足 failover 到 replica-1
机房 B 全挂: A+C 还有 3 个哨兵 quorum 满足
任一机房挂都能恢复
"""

# 4 防脑裂:min-replicas-to-write
# 老主与新主同时存在时 老主必须至少有 1 从同步成功才允许写
# 网络分区时老主孤立无从同步 拒绝写 防数据分裂
# redis-cli -h 10.0.1.10 -p 6379
CONFIG SET min-replicas-to-write 1
CONFIG SET min-replicas-max-lag 10

# 5 测试 failover
# 模拟主挂
redis-cli -h 10.0.1.10 -p 6379 DEBUG SLEEP 30
# 哨兵会发现主无响应 5s 后主观下线 quorum 达成后客观下线 选举新主
# 业务侧客户端 (Jedis SentinelPool) 自动重连新主

# 6 哨兵集群状态查看
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster
redis-cli -p 26379 SENTINEL ckquorum mymaster

实战经验:哨兵数量必须奇数 至少 3 个 推荐 5 个跨 3 机房;quorum 必须 N/2+1 偶数哨兵容易脑裂;min-replicas-to-write 是防脑裂神器 必配 + min-replicas-max-lag;parallel-syncs=1 避免新主被 N 个从同步压垮;每季度 chaos test 模拟机房挂 验证 failover 真能跑通。我们曾因 3 哨兵全部署同一机房 该机房光纤挖断 Redis 直接不可用 后改 5 哨兵跨机房再无此问题。

三 客户端配置:MOVED/ASK 与连接池

Cluster 模式下客户端必须懂 MOVED/ASK 协议自动重定向 否则 fail over 期间疯狂报错。客户端配置错是事故重灾区。

// 1 Java Jedis Cluster 配置
import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Set;

Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("10.0.1.10", 6379));
nodes.add(new HostAndPort("10.0.1.11", 6379));
nodes.add(new HostAndPort("10.0.1.12", 6379));

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(50);
poolConfig.setMinIdle(20);
poolConfig.setMaxWaitMillis(1000);
poolConfig.setTestOnBorrow(false);   // 关键 别打满连接 borrow 时测试
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);

JedisCluster cluster = new JedisCluster(
    nodes,
    5000,           // connectionTimeout
    5000,           // soTimeout
    10,             // maxAttempts 关键 fail over 期间重试
    "password",
    "redisCluster",
    poolConfig
);

// 2 用法
cluster.set("{user:123}:cart", "value");
cluster.hset("{user:123}:order", "ord1", "data");

// 3 pipeline 必须 hashtag 同一 slot
// JedisCluster 不直接支持 pipeline 跨 slot
// 改用 ClusterPipeline (Lettuce 也支持)

// 4 Lettuce(Spring Boot 默认)
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import java.time.Duration;

ClusterTopologyRefreshOptions refreshOpts = ClusterTopologyRefreshOptions.builder()
    .enablePeriodicRefresh(Duration.ofSeconds(30))   // 定期刷拓扑
    .enableAllAdaptiveRefreshTriggers()              // failover 自动触发刷
    .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(15))
    .build();

ClusterClientOptions clientOpts = ClusterClientOptions.builder()
    .topologyRefreshOptions(refreshOpts)
    .maxRedirects(5)                                  // MOVED 重定向次数
    .autoReconnect(true)
    .build();

RedisClusterClient client = RedisClusterClient.create("redis://password@10.0.1.10:6379");
client.setOptions(clientOpts);
# 5 Python redis-py cluster
from redis.cluster import RedisCluster, ClusterNode

startup_nodes = [
    ClusterNode("10.0.1.10", 6379),
    ClusterNode("10.0.1.11", 6379),
    ClusterNode("10.0.1.12", 6379),
]

rc = RedisCluster(
    startup_nodes=startup_nodes,
    password="password",
    decode_responses=True,
    socket_timeout=5,
    socket_connect_timeout=5,
    max_connections_per_node=50,
    retry_on_timeout=True,
    retry_on_error=[ConnectionError, TimeoutError],
    cluster_error_retry_attempts=5,    # 关键 fail over 期间重试
    reinitialize_steps=5,              # 5 次错误后刷新 topology
    read_from_replicas=True,           # 读走从 提升吞吐
)

# 6 跨 slot 操作必须 hashtag
pipe = rc.pipeline(transaction=False)
pipe.hset("{user:123}:cart", "item1", "phone")
pipe.expire("{user:123}:cart", 3600)
pipe.zincrby("{user:123}:order", 1, "ord_a")
pipe.execute()

# 7 SCAN 全集群遍历
def scan_all_keys(rc, pattern, count=1000):
    """SCAN 必须遍历所有 master node"""
    all_keys = []
    for node in rc.get_primaries():
        node_client = node.redis_connection
        cursor = 0
        while True:
            cursor, keys = node_client.scan(cursor, match=pattern, count=count)
            all_keys.extend(keys)
            if cursor == 0:
                break
    return all_keys

# 8 监控连接池状态
def pool_stats(rc):
    stats = {}
    for node in rc.get_nodes():
        pool = node.redis_connection.connection_pool
        stats[f"{node.host}:{node.port}"] = {
            "created": pool._created_connections,
            "available": len(pool._available_connections),
            "in_use": len(pool._in_use_connections),
        }
    return stats

实战经验:cluster_error_retry_attempts / maxAttempts 必须设 5+ failover 期间几秒重试期不能直接报错;enablePeriodicRefresh + adaptiveRefresh 让 Lettuce 自动感知拓扑变化 不刷新会持续打挂的节点;read_from_replicas 在 Cluster 模式下让读走从 主节点压力立降;SCAN 必须 per-node 遍历 不能像单机一次扫完;testOnBorrow=false 否则高 QPS 下 borrow 时测试连接成为瓶颈。

四 大 key 与热 key 治理

大 key (单 key 几 MB) 与热 key (单 key QPS 几万) 是 Redis 最致命的两类问题 阻塞主线程 + 单 shard 打爆。生产必须有治理手段。

# 1 扫描大 key
redis-cli --bigkeys -h 10.0.1.10 -p 6379
# 输出 top 大 key 按类型分组
# - String: 大 value
# - List: 长 list
# - Hash: 多 field
# - Set/Zset: 多 member

# 2 精细扫描 用 redis-rdb-tools 离线分析
# pip install rdbtools python-lzf
rdb -c memory /var/lib/redis/dump.rdb --bytes 10240 > bigkeys.csv
# 找出 > 10KB 的所有 key

# 3 用 MEMORY USAGE 实时查
redis-cli -h 10.0.1.10 -p 6379 MEMORY USAGE mykey SAMPLES 0
# samples=0 精确计算 (慢) > 0 采样估算 (快)

# 4 大 key 治理
# 拆分:hash 200 万字段 → 按 hash bucket 拆 1000 份
def write_split_hash(rc, base_key, field, value, bucket_count=1000):
    bucket = hash(field) % bucket_count
    rc.hset(f"{{{base_key}}}:b{bucket}", field, value)

def read_split_hash(rc, base_key, field, bucket_count=1000):
    bucket = hash(field) % bucket_count
    return rc.hget(f"{{{base_key}}}:b{bucket}", field)

# 删除大 key 用 UNLINK(异步)不要用 DEL(阻塞)
redis-cli UNLINK mykey
# 5 热 key 识别
# 4.0+ 用 redis-cli --hotkeys (前提 maxmemory-policy = allkeys-lfu/volatile-lfu)
"""
redis-cli --hotkeys -h 10.0.1.10 -p 6379
"""

# 6 在线热 key 探测 用 monitor 抽样
def detect_hotkeys(rc, sample_seconds=10):
    """抽样 monitor 几秒钟统计 key 频率"""
    from collections import Counter
    counter = Counter()
    start = time.time()
    pubsub = rc.pubsub()
    # 用 client.monitor 在低 QPS 时段抽样
    with rc.monitor() as m:
        for cmd in m.listen():
            if time.time() - start > sample_seconds:
                break
            # 提取 key
            parts = cmd["command"].split()
            if len(parts) >= 2:
                key = parts[1]
                counter[key] += 1
    return counter.most_common(20)

# 7 热 key 治理
# a) 多副本读  read_from_replicas + 增加从节点
# b) 本地 cache  Caffeine / guava cache 一级缓存 Redis 二级
# c) 拆分热 key  hot_key_xxx → hot_key_xxx_0 hot_key_xxx_1 ... 随机后缀

class HotKeyClient:
    def __init__(self, rc, hot_keys, shard_count=10):
        self.rc = rc
        self.hot_keys = set(hot_keys)
        self.shard_count = shard_count

    def get(self, key):
        if key in self.hot_keys:
            # 随机选一个 shard
            shard = random.randint(0, self.shard_count - 1)
            value = self.rc.get(f"{key}:shard:{shard}")
            return value
        return self.rc.get(key)

    def set(self, key, value, ex=None):
        if key in self.hot_keys:
            # 写所有 shard
            pipe = self.rc.pipeline()
            for s in range(self.shard_count):
                pipe.set(f"{key}:shard:{s}", value, ex=ex)
            pipe.execute()
        else:
            self.rc.set(key, value, ex=ex)

# 8 本地缓存兜底
from cachetools import TTLCache

local_cache = TTLCache(maxsize=10000, ttl=10)

def get_with_local_cache(rc, key):
    if key in local_cache:
        return local_cache[key]
    value = rc.get(key)
    local_cache[key] = value
    return value
# 高频读热 key 90% 在本地命中 Redis 压力降 10 倍

实战经验:bigkeys / hotkeys 命令必须定期跑(每天一次) 异常立即告警;大 key 删除必须用 UNLINK 不要 DEL DEL 阻塞主线程几秒;大 hash 必须拆 bucket(1000 份左右)避免单 key 几 MB;热 key 多副本 + 本地缓存双管齐下 单 shard 才不会被打爆;monitor 不能长期开 抽样几秒即可 monitor 自身就是性能杀手。我们曾因一个 200 万字段的 hash 删 DEL 时主线程阻塞 8 秒 整个集群 fail over 灾难性。

[mermaid]
flowchart TD
A[业务请求] --> B[客户端连接池]
B --> C{key 是否热点}
C -->|是| D[本地缓存 L1]
D -->|命中| E[直接返回]
D -->|未命中| F[多 shard 随机读]
C -->|否| G[CRC16 计算 slot]
F --> H[Redis Cluster]
G --> H
H --> I{slot 在哪个 master}
I -->|本节点| J[直接执行]
I -->|其他节点| K[返回 MOVED]
K --> L[客户端刷新拓扑]
L --> H
J --> M[结果返回]
H --> N[Sentinel 监控]
N --> O{主节点存活}
O -->|否| P[quorum 投票]
P --> Q[选举新主]
Q --> R[拓扑变更通知]
R --> L

五 慢命令防御与持久化策略

KEYS / SMEMBERS / LRANGE 大列表都是隐藏炸弹 一条慢命令能把整个 Redis 阻塞几秒。持久化 RDB / AOF / 混合 选错恢复几小时。生产必须有防御与选型。

# 1 慢日志配置
CONFIG SET slowlog-log-slower-than 10000   # 10ms 算慢
CONFIG SET slowlog-max-len 1000

# 查慢日志
SLOWLOG GET 100
SLOWLOG RESET

# 2 ACL 禁用危险命令
# Redis 6+
ACL SETUSER appuser on >password ~* +@all -KEYS -FLUSHALL -FLUSHDB -DEBUG -CONFIG -SHUTDOWN
# 业务账号禁用 KEYS / FLUSH 防误操作

# Redis 5 用 rename-command
"""
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command DEBUG ""
"""

# 3 慢命令替代
# KEYS pattern → SCAN cursor MATCH pattern COUNT 100
def scan_keys(rc, pattern, count=100):
    cursor = 0
    keys = []
    while True:
        cursor, batch = rc.scan(cursor, match=pattern, count=count)
        keys.extend(batch)
        if cursor == 0:
            break
    return keys

# SMEMBERS big_set → SSCAN
def scan_set(rc, set_key, count=100):
    cursor = 0
    members = []
    while True:
        cursor, batch = rc.sscan(set_key, cursor, count=count)
        members.extend(batch)
        if cursor == 0:
            break
    return members

# LRANGE long_list 0 -1 → 分页 LRANGE
def page_list(rc, list_key, page_size=100):
    length = rc.llen(list_key)
    for offset in range(0, length, page_size):
        yield rc.lrange(list_key, offset, offset + page_size - 1)

# 4 持久化配置 RDB + AOF 混合
"""
# RDB 快照
save 900 1
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes

# AOF 增量
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec     # 每秒 fsync 平衡性能与安全
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 7.0+ 混合持久化(RDB + AOF 增量)
aof-use-rdb-preamble yes
"""

# 5 持久化策略选型
# 仅缓存场景:可关闭持久化 (appendonly no save "")
# 数据可丢失 1s:appendfsync everysec(性能好)
# 强一致:appendfsync always(性能差 50%)
# 大数据量:混合持久化 RDB 加载快 + AOF 增量补丁

# 6 子线程做 BGSAVE 防主线程阻塞
# bgsave 用 fork() 子进程做 不阻塞主
# 但 fork 本身可能慢(大内存实例 fork 几秒)
# 解决:Linux 关 THP (Transparent Huge Pages)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 7 监控关键指标
redis-cli -h 10.0.1.10 -p 6379 INFO stats
# instantaneous_ops_per_sec        当前 QPS
# rejected_connections             连接被拒次数 应为 0
# expired_keys                     过期 key 数
# evicted_keys                     淘汰 key 数 持续增长说明 maxmemory 不够
# keyspace_hits/misses             命中率

redis-cli INFO replication        # 主从状态
redis-cli INFO persistence        # 持久化状态
redis-cli INFO memory             # 内存碎片率 mem_fragmentation_ratio > 1.5 需 MEMORY PURGE

实战经验:slowlog-log-slower-than 设 10ms 收集慢命令 每天 review 发现新隐患;ACL 禁用 KEYS/FLUSH 防误操作 这是生产铁律 多少事故都是手抖 KEYS *;SCAN/SSCAN/HSCAN 替代所有"全量"命令 cursor 分批不阻塞;持久化 everysec 是大多数场景最佳平衡 always 性能差 50% 大多场景过度;混合持久化(7.0+)是最佳选择 启动快 + 增量精确;THP 必须关 否则 fork 阻塞秒级。我们曾因没关 THP 一个 32GB 实例 BGSAVE fork 阻塞 4 秒 客户端 timeout 雪崩。

六 监控告警与故障演练

不监控等于裸奔 不演练等于纸上谈兵。生产 Redis 必须 Prometheus + Grafana + chaos engineering 定期演练 fail over。

# 1 redis_exporter 部署
# docker-compose.yml
services:
  redis-exporter:
    image: oliver006/redis_exporter:latest
    environment:
      REDIS_ADDR: "redis://10.0.1.10:6379,redis://10.0.1.11:6379,redis://10.0.1.12:6379"
      REDIS_PASSWORD: "password"
    ports:
      - "9121:9121"

# 2 Prometheus scrape
scrape_configs:
  - job_name: "redis"
    static_configs:
      - targets: ["redis-exporter:9121"]

# 3 关键告警规则
groups:
- name: redis_alerts
  rules:
  - alert: RedisDown
    expr: redis_up == 0
    for: 1m
    annotations:
      summary: "Redis 实例 {{ $labels.instance }} 不可达"

  - alert: RedisMemoryHigh
    expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
    for: 5m
    annotations:
      summary: "Redis {{ $labels.instance }} 内存使用 > 85%"

  - alert: RedisRejectedConnections
    expr: increase(redis_rejected_connections_total[5m]) > 0
    annotations:
      summary: "Redis {{ $labels.instance }} 连接被拒"

  - alert: RedisReplicationLag
    expr: redis_master_repl_offset - on(instance) redis_slave_repl_offset > 1000000
    for: 2m
    annotations:
      summary: "Redis 主从复制延迟 > 1MB"

  - alert: RedisHighEvictionRate
    expr: rate(redis_evicted_keys_total[5m]) > 1000
    annotations:
      summary: "Redis 高频淘汰 maxmemory 可能不够"

  - alert: RedisHighLatency
    expr: redis_commands_duration_seconds{quantile="0.99"} > 0.1
    annotations:
      summary: "Redis P99 延迟 > 100ms"

  - alert: RedisClusterDown
    expr: redis_cluster_state != 1
    annotations:
      summary: "Redis Cluster 状态异常"
# 4 Chaos Engineering 故障演练
import subprocess
import time

class RedisChaosTest:
    def __init__(self, cluster_nodes):
        self.nodes = cluster_nodes

    def test_master_kill(self, master_node):
        """杀掉一个主节点 验证 fail over 时间与业务影响"""
        # 记录开始
        start = time.time()

        # SSH 上去 kill
        subprocess.run(["ssh", master_node, "pkill -9 redis-server"])

        # 持续 ping 业务 观察恢复时间
        recovered = False
        while time.time() - start < 60:
            try:
                rc.set("chaos_test", "ok", ex=10)
                if not recovered:
                    print(f"业务恢复 耗时 {time.time() - start:.1f}s")
                    recovered = True
                    break
            except Exception as e:
                pass
            time.sleep(0.5)

        return time.time() - start

    def test_network_partition(self, node, duration=30):
        """模拟网络分区 验证脑裂防御"""
        subprocess.run(["ssh", node, f"iptables -A INPUT -p tcp --dport 6379 -j DROP"])
        time.sleep(duration)
        subprocess.run(["ssh", node, f"iptables -D INPUT -p tcp --dport 6379 -j DROP"])

    def test_slow_query(self, node):
        """注入慢命令 验证慢日志与告警"""
        subprocess.run(["ssh", node, "redis-cli DEBUG SLEEP 5"])

# 5 月度演练计划
"""
每月第一周:
- 周一: 杀主节点 验证 fail over 时间 < 30s
- 周三: 网络分区 验证防脑裂(数据无分裂)
- 周五: 注入大 key 验证迁移超时与告警
"""

实战经验:redis_exporter + Prometheus + Grafana 是生产标配 关键指标必告警 内存/连接/延迟/复制/淘汰五件套;每月 chaos test 杀主节点验证 fail over 真能用 不演练就等于没有;脑裂演练特别重要 一次脑裂数据对账几天;慢命令注入告警链路是否真能 page on-call 别等真出事才发现告警漏配。我们建立月度演练后 真实事故响应时间从 30 分钟降到 3 分钟 因为团队对 fail over 流程已经形成肌肉记忆。

关键概念速查

概念 关键参数/命令 推荐 备注
Cluster 槽 16384 slot + CRC16 必懂 分片基础
Hashtag {tag}:key 强制同槽 必用 事务 pipeline 必需
Sentinel 数量 5 个跨 3 机房 推荐 quorum=3 防脑裂
防脑裂 min-replicas-to-write 1 必配 + max-lag 10
客户端重试 cluster_error_retry_attempts 5+ 必配 fail over 期间不报错
SCAN 替代 SCAN/SSCAN/HSCAN cursor 必用 禁 KEYS / SMEMBERS
大 key 删除 UNLINK 异步 必用 禁 DEL 阻塞主线程
热 key 拆分 + 本地 cache + 多 shard 必做 防单节点打爆
持久化 RDB + AOF 混合 推荐 everysec 平衡
THP 关闭 echo never > ...thp 必做 防 fork 阻塞

避坑清单

  1. 不要忘 hashtag 跨 slot 操作直接报 CROSSSLOT 业务崩。
  2. 不要 3 哨兵全部署同机房 跨 3 机房 5 个起 quorum 3。
  3. 不要省 min-replicas-to-write 网络分区脑裂数据分裂对账噩梦。
  4. 不要用老版本客户端 fail over 期间不会刷拓扑 疯狂报错。
  5. 不要 KEYS pattern 调试 直接把主节点扫死。
  6. 不要 DEL 大 key 必须 UNLINK 否则主线程阻塞秒级。
  7. 不要 reshard 前不清大 key MIGRATE timeout 集群进 fail。
  8. 不要忽视 THP 大内存实例 fork 阻塞秒级 业务雪崩。
  9. 不要不开慢日志 + ACL 禁危险命令 防误操作铁律。
  10. 不要不演练 fail over 真出事手忙脚乱响应时间 30 分钟以上。

总结

把 Redis 高可用与 Cluster 部署这套从我们踩过的所有坑里反过来看 你会发现真正影响 Redis 稳定性的不是机器配置 而是工程化的全栈高可用能力。同样一个 Redis Cluster 直接 redis-cli cluster create 跑通 上生产一周连 5 次 P1 故障;hashtag 设计 + 5 哨兵跨机房 + min-replicas-to-write + 客户端 retry 配齐 + 大 key 治理 + chaos 月度演练 同样的硬件半年零事故 P99 延迟稳定 5ms。Redis 高可用不是"装个 Cluster"的活儿 它是一个分片设计 + 哨兵部署 + 客户端精调 + 大热 key 治理 + 持久化选型 + 监控告警 + 故障演练的完整系统工程。

另一个常见的认知误区是把 Redis 当万能缓存 觉得加 Redis 就能解决所有性能问题。但事实是 Redis 是单线程命令处理 一条慢命令阻塞全部 一个大 key 操作影响所有客户端 一次脑裂数据可能分裂对账几天。Redis 工程化的核心是 假设每条命令都可能慢 每个 key 都可能成大 key 每个节点都可能挂 用多层防御 + 持续演练保证整体可用性。

打个比方 Redis Cluster 像一个大型物流分拣中心。Slot 槽是固定的传送带编号 Hashtag 是包裹分组标签(同一个客户的件必须走同一传送带)Sentinel 是值班调度员(发现某条传送带停了立即切到备用)客户端 retry 是司机的应变(临时路线变化能跟上)大 key 治理是超大件单独处理(不能塞进普通传送带)慢命令防御是禁运清单(危险品不许进)持久化是货品记录(出意外能追溯重建)监控告警是中控室大屏(异常立即响)chaos 演练是消防演习(真出事不慌)。哪一环没做 这个分拣中心可能短期跑得动 但长期一定出大事故 要么传送带瘫痪 要么货品丢失 要么调度混乱整个系统停摆。

所以下一次再有人跟你说"Redis 加个 Cluster 就高可用了"你可以反问他 hashtag 设计了吗 哨兵 quorum 算清了吗 min-replicas-to-write 配了吗 客户端 retry 设了吗 大 key 监控了吗 慢日志开了吗 ACL 禁危险命令了吗 chaos 演练做了吗 这些工作没做完 Redis Cluster 只是一个能跑通 demo 的玩具 不是一个能扛真实流量的生产基础设施。从踩坑到稳如老狗 中间隔着一整套高可用工程方法论 这条路没有捷径 但走完之后 你的 Redis 会从间歇 P1 变成 99.99% 可用 从每月几次脑裂对账变成半年零事故 从客户端疯狂报错变成 fail over 业务无感。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

LLM 微调与部署完全指南:从一次"LLaMA-3-8B + LoRA 训完上线全是幻觉客户说这条文不存在"看懂为什么跑通 peft 脚本远远不够

2026-5-25 11:06:38

技术教程

Embedding 模型选型与向量数据库完全指南:从一次"5 万判例 ada-002 + Pinecone 召回 62% 律师骂返回的全不沾边"看懂为什么向量库 + embedding 远远不够

2026-5-25 11:20:01

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索