Redis红宝书 部署架构

119 阅读10分钟

Redis 部署架构

概述

Redis 提供了多种部署架构来满足不同的业务需求,从简单的主从复制到复杂的分布式集群。本文将详细介绍 Redis 的三种主要部署架构:主从复制、哨兵模式和集群模式。

1. Redis 主从复制

1.1 架构原理

Redis 主从复制是一种数据冗余和读写分离的解决方案。在主从架构中,有一个主节点(Master)和一个或多个从节点(Slave/Replica)。

graph TD
    A[Master 主节点] --> B[Slave1 从节点]
    A --> C[Slave2 从节点]
    A --> D[Slave3 从节点]
    
    E[Client 写请求] --> A
    F[Client 读请求] --> B
    G[Client 读请求] --> C
    H[Client 读请求] --> D

1.2 复制机制

1.2.1 全量复制(Full Resynchronization)

当从节点首次连接主节点或重新连接时,会进行全量复制:

  1. 建立连接:从节点向主节点发送 PSYNC 命令
  2. 生成快照:主节点执行 BGSAVE 生成 RDB 文件
  3. 传输数据:主节点将 RDB 文件发送给从节点
  4. 加载数据:从节点清空现有数据,加载 RDB 文件
  5. 同步增量:传输期间的写命令通过复制缓冲区同步
1.2.2 增量复制(Partial Resynchronization)

Redis 2.8+ 引入了增量复制机制:

  • 复制偏移量:主从节点维护复制偏移量
  • 复制积压缓冲区:主节点维护固定大小的环形缓冲区
  • 服务器运行ID:每个 Redis 实例的唯一标识
# 主节点配置
bind 0.0.0.0
port 6379

# 从节点配置
bind 0.0.0.0
port 6380
replicaof 127.0.0.1 6379
# 或使用命令:SLAVEOF 127.0.0.1 6379

1.3 复制流程详解

sequenceDiagram
    participant S as Slave
    participant M as Master
    
    S->>M: PSYNC runid offset
    alt 首次连接或runid不匹配
        M->>S: +FULLRESYNC runid offset
        M->>M: BGSAVE 生成RDB
        M->>S: 发送RDB文件
        M->>S: 发送缓冲区命令
    else 增量复制
        M->>S: +CONTINUE
        M->>S: 发送缺失的命令
    end
    
    loop 持续同步
        M->>S: 实时命令同步
    end

1.4 优点

  1. 读写分离:主节点处理写操作,从节点处理读操作
  2. 数据冗余:提供数据备份,提高可用性
  3. 负载均衡:分散读请求压力
  4. 简单易用:配置简单,维护成本低

1.5 缺点

  1. 单点故障:主节点故障时需要手动切换
  2. 数据一致性:异步复制可能导致数据丢失
  3. 复制延迟:网络延迟影响数据同步
  4. 写操作瓶颈:所有写操作集中在主节点

1.6 配置示例

# 主节点 redis-master.conf
port 6379
bind 0.0.0.0
requirepass "master_password"
masterauth "master_password"

# 从节点 redis-slave.conf
port 6380
bind 0.0.0.0
replicaof 127.0.0.1 6379
masterauth "master_password"
requirepass "slave_password"
replica-read-only yes

2. Redis 哨兵模式(Sentinel)

2.1 解决的问题

哨兵模式主要解决主从复制架构中的以下问题:

  1. 主节点故障检测:自动发现主节点故障
  2. 自动故障转移:自动将从节点提升为主节点
  3. 配置管理:动态更新客户端连接信息
  4. 通知机制:故障转移时通知管理员和应用程序

2.2 架构设计

graph TD
    subgraph "Sentinel 集群"
        S1[Sentinel 1]
        S2[Sentinel 2]
        S3[Sentinel 3]
    end
    
    subgraph "Redis 集群"
        M[Master]
        R1[Replica 1]
        R2[Replica 2]
    end
    
    S1 -.-> M
    S1 -.-> R1
    S1 -.-> R2
    S2 -.-> M
    S2 -.-> R1
    S2 -.-> R2
    S3 -.-> M
    S3 -.-> R1
    S3 -.-> R2
    
    M --> R1
    M --> R2
    
    C[Client] --> S1
    C --> S2
    C --> S3

2.3 工作机制

2.3.1 监控(Monitoring)
  • 哨兵定期向主从节点发送 PING 命令
  • 检查节点的响应时间和状态
  • 维护节点的在线状态信息
2.3.2 故障检测

主观下线(Subjective Down, SDOWN)

  • 单个哨兵认为节点不可达
  • 超过 down-after-milliseconds 时间未响应

客观下线(Objective Down, ODOWN)

  • 多个哨兵确认节点不可达
  • 达到 quorum 数量的哨兵同意
2.3.3 故障转移流程
sequenceDiagram
    participant S1 as Sentinel 1
    participant S2 as Sentinel 2
    participant S3 as Sentinel 3
    participant M as Master
    participant R1 as Replica 1
    participant R2 as Replica 2
    
    S1->>M: PING (超时)
    S1->>S1: 标记主观下线
    S1->>S2: 询问Master状态
    S1->>S3: 询问Master状态
    S2->>S1: 确认下线
    S3->>S1: 确认下线
    S1->>S1: 标记客观下线
    
    S1->>S2: 请求成为Leader
    S1->>S3: 请求成为Leader
    S2->>S1: 投票同意
    S3->>S1: 投票同意
    
    S1->>R1: 选择新Master
    S1->>R1: SLAVEOF NO ONE
    S1->>R2: SLAVEOF new_master
    S1->>S2: 通知配置更新
    S1->>S3: 通知配置更新

2.4 配置示例

2.4.1 哨兵配置文件
# sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster your_password
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000

# 通知脚本
sentinel notification-script mymaster /opt/notify.sh
sentinel client-reconfig-script mymaster /opt/reconfig.sh
2.4.2 启动命令
# 启动哨兵
redis-sentinel /path/to/sentinel.conf
# 或
redis-server /path/to/sentinel.conf --sentinel

2.5 优点

  1. 高可用性:自动故障检测和转移
  2. 无单点故障:哨兵集群保证可靠性
  3. 自动化运维:减少人工干预
  4. 配置管理:动态服务发现

2.6 缺点

  1. 复杂性增加:需要额外的哨兵节点
  2. 脑裂风险:网络分区可能导致多个主节点
  3. 资源消耗:额外的监控开销
  4. 写操作限制:仍然只有一个主节点处理写操作

3. Redis 集群模式(Cluster)

3.1 分布式架构

Redis 集群是一个分布式的 Redis 实现,提供了数据分片和高可用性。

graph TD
    subgraph "Redis Cluster"
        subgraph "Node 1 (0-5460)"
            M1[Master 1]
            S1[Slave 1]
        end
        
        subgraph "Node 2 (5461-10922)"
            M2[Master 2]
            S2[Slave 2]
        end
        
        subgraph "Node 3 (10923-16383)"
            M3[Master 3]
            S3[Slave 3]
        end
    end
    
    M1 --> S1
    M2 --> S2
    M3 --> S3
    
    M1 -.-> M2
    M2 -.-> M3
    M3 -.-> M1
    
    C[Client] --> M1
    C --> M2
    C --> M3

3.2 Slot 机制演进

3.2.1 分布式哈希算法演进

在分布式系统中,数据分片算法经历了从简单到复杂的演进过程:

1. 普通取模算法

最简单的分片方式是对节点数量取模:

# 普通取模
node = hash(key) % N  # N为节点数量

# 示例:3个节点
hash("user:1001") % 3 = 1  # 存储在节点1
hash("user:1002") % 3 = 2  # 存储在节点2
hash("user:1003") % 3 = 0  # 存储在节点0

普通取模的问题:

  • 节点数量变化时,大量数据需要重新分布
  • 扩容或缩容成本极高
  • 数据迁移量巨大

2. 一致性哈希算法

为解决普通取模的问题,引入了一致性哈希:

graph TD
    subgraph "一致性哈希环"
        A[Node A<br/>Hash: 100]
        B[Node B<br/>Hash: 200]
        C[Node C<br/>Hash: 300]
        
        K1[Key1<br/>Hash: 150]
        K2[Key2<br/>Hash: 250]
        K3[Key3<br/>Hash: 50]
    end
    
    K1 --> B
    K2 --> C
    K3 --> A
    
    style A fill:#ffcccc
    style B fill:#ccffcc
    style C fill:#ccccff

一致性哈希特点:

  • 节点和数据都映射到哈希环上
  • 数据存储在顺时针方向第一个节点
  • 节点变化时只影响相邻节点的数据
  • 引入虚拟节点解决数据倾斜问题

一致性哈希的局限:

  • 实现复杂度较高
  • 虚拟节点管理复杂
  • 数据分布仍可能不均匀
  • 节点故障时数据重分布逻辑复杂

3. Redis 哈希槽方案

Redis 采用了更简单有效的哈希槽方案:

graph TD
    subgraph "哈希槽分配"
        S1["Slots 0-5460<br/>(5461个槽)"]
        S2["Slots 5461-10922<br/>(5462个槽)"]
        S3["Slots 10923-16383<br/>(5461个槽)"]
    end
    
    subgraph "节点映射"
        N1[Node 1]
        N2[Node 2]
        N3[Node 3]
    end
    
    S1 --> N1
    S2 --> N2
    S3 --> N3
    
    K["Key: user:1001<br/>CRC16: 9842<br/>Slot: 9842"] --> S2
3.2.2 哈希槽分配机制

Redis 集群将整个数据空间分为 16384 个哈希槽(slot):

# 槽位计算公式
slot = CRC16(key) % 16384

# 示例
CRC16("user:1001") % 16384 = 9842
# 该键存储在负责 9842 槽位的节点上

哈希槽的优势:

  1. 固定槽位数量:16384个槽位数量固定,不随节点变化
  2. 简单映射:槽位到节点的映射关系清晰
  3. 灵活迁移:可以精确控制数据迁移粒度
  4. 负载均衡:可以根据需要调整槽位分配
3.2.3 为什么是 16384 个槽位?

Redis 选择 16384 个槽位的设计考虑:

1. 心跳包大小限制

Redis 集群节点间通过 Gossip 协议进行通信,每个心跳包都需要携带槽位信息:

# 槽位信息在心跳包中的表示
# 使用位图(bitmap)表示槽位分配状态
# 16384 slots = 16384 bits = 2048 bytes = 2KB

# 如果使用 65536 个槽位:
# 65536 slots = 65536 bits = 8192 bytes = 8KB

官方设计考虑(来源:Redis 官方文档):

"The reason is that normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16384 slots, but would use a prohibitive 8k of space using 65536 slots."

2. 网络带宽优化

sequenceDiagram
    participant N1 as Node 1
    participant N2 as Node 2
    participant N3 as Node 3
    
    Note over N1,N3: 心跳包结构
    
    N1->>N2: PING<br/>包含:节点信息+槽位bitmap(2KB)
    N2->>N1: PONG<br/>包含:节点信息+槽位bitmap(2KB)
    
    N1->>N3: PING<br/>包含:节点信息+槽位bitmap(2KB)
    N3->>N1: PONG<br/>包含:节点信息+槽位bitmap(2KB)
    
    Note over N1,N3: 每秒多次心跳,2KB vs 8KB差异巨大

3. 集群规模考虑

  • 理论最大节点数:16384 个(每个节点至少负责1个槽位)
  • 实际推荐节点数:1000 个以内
  • 槽位分配粒度:平均每个节点 16-32 个槽位(500-1000节点场景)

4. 内存使用优化

// Redis 源码中的槽位表示
// 每个节点维护的槽位信息
typedef struct clusterNode {
    // ...
    unsigned char slots[CLUSTER_SLOTS/8]; // 16384/8 = 2048 bytes
    // ...
} clusterNode;
3.2.4 心跳机制与槽位信息传播

心跳包结构:

// Redis 集群心跳包结构(简化版)
typedef struct {
    char sig[4];        // 签名 "RCmb"
    uint32_t totlen;    // 总长度
    uint16_t ver;       // 协议版本
    uint16_t port;      // 端口
    uint16_t type;      // 消息类型 (PING/PONG/MEET/FAIL)
    uint16_t count;     // 节点数量
    uint64_t currentEpoch;  // 当前纪元
    uint64_t configEpoch;   // 配置纪元
    char sender[CLUSTER_NAMELEN]; // 发送者ID
    unsigned char myslots[CLUSTER_SLOTS/8]; // 槽位bitmap
    // ... 其他节点信息
} clusterMsg;

槽位信息传播流程:

sequenceDiagram
    participant N1 as Node 1<br/>(Slots: 0-5460)
    participant N2 as Node 2<br/>(Slots: 5461-10922)
    participant N3 as Node 3<br/>(Slots: 10923-16383)
    
    Note over N1,N3: 定期心跳(每秒1次)
    
    N1->>N2: PING<br/>myslots[0-5460]=1, others=0
    N2->>N1: PONG<br/>myslots[5461-10922]=1, others=0
    
    N1->>N3: PING<br/>myslots[0-5460]=1, others=0
    N3->>N1: PONG<br/>myslots[10923-16383]=1, others=0
    
    N2->>N3: PING<br/>myslots[5461-10922]=1, others=0
    N3->>N2: PONG<br/>myslots[10923-16383]=1, others=0
    
    Note over N1,N3: 每个节点都知道全局槽位分配

心跳频率与网络开销:

# 心跳配置参数
cluster-node-timeout 15000  # 节点超时时间(ms)

# 心跳频率计算
# ping_interval = min(node_timeout/2, 1000ms)
# 默认每秒1次心跳

# 网络开销计算(100个节点的集群):
# 每个节点每秒发送心跳:99次(向其他所有节点)
# 每个心跳包大小:约2KB(槽位信息)+ 节点信息
# 单节点每秒网络开销:99 * 2KB = 198KB
# 集群总网络开销:100 * 198KB = 19.8MB/s
3.2.5 槽位分配策略
# 3个主节点的槽位分配
Node 1: 0-5460     (5461 slots)
Node 2: 5461-10922 (5462 slots)
Node 3: 10923-16383 (5461 slots)

3.3 集群拓扑

3.3.1 节点发现
sequenceDiagram
    participant N1 as Node 1
    participant N2 as Node 2
    participant N3 as Node 3
    participant N4 as Node 4
    
    N1->>N2: MEET 命令
    N2->>N1: 握手确认
    N1->>N3: 传播节点信息
    N2->>N4: 传播节点信息
    
    loop Gossip 协议
        N1->>N2: PING (节点状态)
        N2->>N3: PING (节点状态)
        N3->>N4: PING (节点状态)
        N4->>N1: PING (节点状态)
    end
3.3.2 Gossip 协议

每个节点定期向其他节点发送 Gossip 消息,包含:

  • 节点状态信息
  • 槽位分配信息
  • 故障检测信息
  • 配置更新信息

3.4 故障检测与转移

3.4.1 故障检测
# 节点状态
- PFAIL: 疑似故障(主观判断)
- FAIL: 确认故障(客观判断)
- HANDSHAKE: 握手状态
- NOADDR: 无地址状态
3.4.2 自动故障转移
sequenceDiagram
    participant M as Master
    participant S1 as Slave 1
    participant S2 as Slave 2
    participant N1 as Other Node 1
    participant N2 as Other Node 2
    
    M->>X: 节点故障
    N1->>N1: 检测到Master故障
    N2->>N2: 检测到Master故障
    N1->>S1: 通知故障
    N2->>S1: 通知故障
    
    S1->>S1: 发起选举
    S1->>N1: 请求投票
    S1->>N2: 请求投票
    N1->>S1: 投票确认
    N2->>S1: 投票确认
    
    S1->>S1: 提升为Master
    S1->>N1: 广播新配置
    S1->>N2: 广播新配置

3.5 集群配置

3.5.1 节点配置
# redis-cluster.conf
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
appendonly yes

# 启动节点
redis-server redis-cluster.conf
3.5.2 创建集群
# Redis 5.0+ 使用 redis-cli
redis-cli --cluster create \
  127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
  127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
  --cluster-replicas 1

# 检查集群状态
redis-cli --cluster check 127.0.0.1:7000

# 查看集群信息
redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER INFO
127.0.0.1:7000> CLUSTER NODES

3.6 数据迁移

3.6.1 槽位迁移
# 迁移槽位 1000 从节点 A 到节点 B
redis-cli --cluster reshard 127.0.0.1:7000

# 手动迁移步骤
# 1. 在目标节点导入槽位
CLUSTER SETSLOT 1000 IMPORTING source_node_id

# 2. 在源节点导出槽位
CLUSTER SETSLOT 1000 MIGRATING target_node_id

# 3. 迁移键值
MIGRATE target_host target_port "" 0 5000 KEYS key1 key2 ...

# 4. 确认迁移完成
CLUSTER SETSLOT 1000 NODE target_node_id

3.7 优点

  1. 水平扩展:支持动态添加/删除节点
  2. 高可用性:自动故障检测和转移
  3. 数据分片:自动数据分布和负载均衡
  4. 无单点故障:去中心化架构
  5. 线性扩展:性能随节点数量线性增长

3.8 缺点

  1. 复杂性:运维和故障排查复杂
  2. 事务限制:跨槽位事务不支持
  3. 客户端复杂:需要集群感知的客户端
  4. 数据倾斜:可能出现热点数据问题
  5. 网络开销:节点间通信开销

4. 架构选择指南

4.1 场景对比

特性主从复制哨兵模式集群模式
高可用性
自动故障转移
水平扩展
运维复杂度
数据一致性
事务支持部分
客户端复杂度

4.2 选择建议

主从复制适用场景:

  • 读多写少的应用
  • 对可用性要求不高
  • 简单的数据备份需求

哨兵模式适用场景:

  • 需要高可用性
  • 数据量不大(单机可承载)
  • 自动化运维需求

集群模式适用场景:

  • 大数据量存储
  • 高并发读写
  • 需要水平扩展
  • 对性能要求极高

5. 最佳实践

5.1 部署建议

  1. 网络规划:确保节点间网络稳定
  2. 资源配置:合理分配 CPU、内存、磁盘
  3. 监控告警:完善的监控和告警机制
  4. 备份策略:定期数据备份和恢复测试
  5. 安全配置:启用认证、网络隔离

5.2 性能优化

# 内存优化
maxmemory 2gb
maxmemory-policy allkeys-lru

# 网络优化
tcp-keepalive 300
timeout 0

# 持久化优化
save 900 1
appendonly yes
appendfsync everysec

5.3 监控指标

  • 连接数:connected_clients
  • 内存使用:used_memory
  • 命令统计:total_commands_processed
  • 键空间:keyspace_hits/keyspace_misses
  • 复制延迟:master_repl_offset - slave_repl_offset

总结

Redis 提供了从简单到复杂的多种部署架构,每种架构都有其适用场景和特点。选择合适的架构需要综合考虑业务需求、数据规模、可用性要求和运维能力。随着业务的发展,也可以逐步从简单架构向复杂架构演进。