Redis集群机制分析

主从复制模式

Redis集群实现数据同步,保证服务高可用。

  • 一主多从

  • 数据流向是单向的,master->slave

开启指令:

slaveof 192.168.1.10 6379		//当前服务节点称为指定IP的从节点
slaveof no one                  //取消从属关系

配置文件:

slaveof ip port
slave-read-only yes             //从节点只读

全量复制的开销: - bgsave的时间 - RDB文件网络传输时间 - 从节点清空数据时间 - 从节点加载RDB的时间 - 可能的AOF重写时间

实现

Redis复制分为同步和命令传播两个操作:

  • 同步:将从服务器的数据库状态更新为主库状态
  • 命令传播:同步完后,主库状态又发生变化,此时使用命令传播完成状态同步。

Redis使用PSYNC可以同时解决全量复制和部分复制两种情况。

需要维护的变量:主从的复制偏移量,master的复制缓冲区(FIFO队列)、run ID

执行slaveof时,slave节点会将master的地址保存在:

struct redisServer {
    char *masterhost;
    int masterport;
}

slave节点会将该命令封装成通信协议并发给master,完成socket建立,并发送PING检查socket是否正常。

心跳检测

命令传播的阶段,slave默认每秒一次的频率,向master发送心跳:

REPLCONF ACK <replication_offset>

replication_offset为slave当前的复制偏移量

心跳主要有三个作用:

  • 检测主从之间连接状态:节点保活机制
  • 辅助实现min-slaves:防止master在不安全的情况下执行写命令。
    • min-slaves-to-write
    • min-slaves-max-log
  • 检测命令丢失:如果因为网络原因产生命令丢失,master发现slave的offset小于自己当前offset,则认为产生命令丢失,并按照slave的offset传递数据。

sentinel机制

主从复制存在的问题

  • 手动故障转移:master发生宕机,需要手动切换
  • 写能力和存储能力受限

Sentinel(机制)是Redis官方推荐的高可用性(HA)解决方案,由一个或多个sentinel组成的sentinel system,可以监视主从集群的状态,当master发生宕机,自动将master下面的某个slave升级为新的master。

启动

./redis-sentinel /path/to/sentinel.conf

启动流程会执行以下步骤:

  • 初始化server:sentinel本质上是一个运行在特殊模式的Redis服务器。
  • 代码:sentinel使用sentinel.c/REDIS_SENTINEL_PORT作为默认端口,使用sentinel.c/sentinelcmds作为命令表
  • 初始化sentinel状态:sentinel.c/sentinelState结构
  • 根据配置文件,初始化sentinel监视的主服务器列表:上面的结构使用dict保存master信息,key为master的名称,value为master对应的sentinel.c/sentinelRedisInstance,每个被监视的Redis服务器实例都被使用该结构存储。master的IP端口信息使用struct sentinelAddr进行存储。该数据sentinel初始化时从sentinel.conf完成加载。
  • 创建连向master的socket连接:sentinel节点对master创建两个异步连接:
    • 命令连接:专门向master发送命令,并接收回复
    • 订阅连接:订阅master的__sentinel__:hello频道,用于sentinel发现其他sentinel节点

获取信息

每10s每个sentinel会对master发送info命令,sentinel主要获取两方面信息:

  • master信息:run ID、role
  • master对应的slave信息,包括IP端口、状态,偏移量等

当获取到对应slave节点信息后,sentinel会创建到slave的两个连接,创建完命令连接后, 每10s对slave发送info命令,sentinel会获取以下信息:

  • slave:run ID、role、master_port、master_host、主从之间的连接状态

sentinel默认每2s向所有节点的__sentinel__:hello发送消息,主要是sentinel本身信息和master的信息。并且sentinel会通过订阅连接不断接收主从节点发来的消息,这里面是sentinel信息,如果与当前sentinel不同,说明是其他sentinel,那么当前sentinel会更新sentinel字典信息,并创建连向新sentinel的命令连接。

主观下线

sentinel每1s向所有实例发送PING,包括主从、其他sentinel,并通过实例是否返回PONG判断是否在线。配置文件中的down-after-milliseconds内,未收到回复,sentinel就会将该实例标记为主观下线状态。但是多个sentinel的主线下线阈值不同,可能存在某节点在sentinelA是离线,在sentinelB是在线。

客观下线

多个sentinel实例对同一台服务器做出SDOWN判断,并且通过sentinel is-master-down-by-addr ip命令互相交流之后,得出服务器下线的判断。客观下线是足够的sentinel都将服务器标记为主观下线之后,服务器才会标记为客观下线。

sentinel选举

当某个master被判断为客观下线后,各个sentinel会进行协商,选举出一个主sentinel,并由主sentinel完成故障转移等操作。选举算法是Raft,选举规则如下:

  • 所有sentinel拥有相同资格参与选举、每次完成选举后,epoch都会自增一次
  • 当A向B发送sentinel is-master-down-by-addr ip时,A要求B将其设置为主sentinel。最先向B发送的将会成为B的局部主sentinel。
  • B接收到A的命令,回复信息中分别由局部主sentinel的run ID和leader_epoch
  • A接收到回复,B的局部主sentinel是否为自己,如果A称为半数以上的局部主sentinel,则会成为全局主sentinel。
  • 规定时间内未完成选举,则会再进行。

故障转移

选举出主sentinel后,主sentinel会将已下线的master进行故障转移,步骤如下:

  • 已下线的master从属的slave列表中挑选一个,并将其转换为master。挑选其中状态良好、数据完整的slave。
  • 让其他slave改为复制新的master的数据。向其他slave发送SLAVEOF

Redis Cluster

Redis集群是Redis提供的分布式数据库方案,通过sharding完成数据共享,并通过复制、故障转移来提供高可用特性。

Node

一个集群由多个Node构成,刚运行时都是独立的Node,需要通过CLUSTER MEET <ip> <port>指令来构成集群。CLUSTER NODE可以查询集群信息。Node通过以下配置参数判断是否以集群模式启动:

cluster-enabled yes

Node使用以下struct来定义:

// 节点状态
struct clusterNode {

    // 创建节点的时间
    mstime_t ctime; /* Node object creation time. */

    // 节点的名字,由 40 个十六进制字符组成
    // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */

    // 节点标识
    // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    // 以及节点目前所处的状态(比如在线或者下线)。
    int flags;      /* REDIS_NODE_... */

    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    
    // 该节点负责处理的槽数量
    int numslots;   /* Number of slots handled by this node */

    // 如果本节点是主节点,那么用这个属性记录从节点的数量
    int numslaves;  /* Number of slave nodes, if this is a master */

    // 指针数组,指向各个从节点
    struct clusterNode **slaves; /* pointers to slave nodes */

    // 如果这是一个从节点,那么指向主节点
    struct clusterNode *slaveof; /* pointer to the master node */

    // 最后一次发送 PING 命令的时间
    mstime_t ping_sent;      /* Unix time we sent latest ping */

    // 最后一次接收 PONG 回复的时间戳
    mstime_t pong_received;  /* Unix time we received the pong */

    // 最后一次被设置为 FAIL 状态的时间
    mstime_t fail_time;      /* Unix time when FAIL flag was set */

    // 最后一次给某个从节点投票的时间
    mstime_t voted_time;     /* Last time we voted for a slave of this master */

    // 最后一次从这个节点接收到复制偏移量的时间
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */

    // 这个节点的复制偏移量
    long long repl_offset;      /* Last known repl offset for this node. */

    // 节点的 IP 地址
    char ip[REDIS_IP_STR_LEN];  /* Latest known IP address of this node */

    // 节点的端口号
    int port;                   /* Latest known port of this node */

    // 保存连接节点所需的有关信息
    clusterLink *link;          /* TCP/IP link with this node */

    // 一个链表,记录了所有其他节点对该节点的下线报告
    list *fail_reports;         /* List of nodes signaling this as failing */

};

typedef struct clusterLink {

    // 连接的创建时间
    mstime_t ctime;             /* Link creation time */

    // TCP 套接字描述符
    int fd;                     /* TCP socket file descriptor */

    // 输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;                 /* Packet send buffer */

    // 输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;                 /* Packet reception buffer */

    // 与这个连接相关联的节点,如果没有的话就为 NULL
    struct clusterNode *node;   /* Node related to this link if any, or NULL */

} clusterLink;

slot

集群通过分片的方式保存数据,整个数据库被分为16384个slot,slot被定义在clusterNode中,整个slot数组长度为2048 bytes,以bitmap形式进行存储。处理slot的node的映射信息存储在clusterState中。

// 槽数量
#define REDIS_CLUSTER_SLOTS 16384

// 由这个节点负责处理的槽
// 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长
// 每个字节的每个位记录了一个槽的保存状态
// 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理
// 比如 slots[0] 的第一个位保存了槽 0 的保存情况
// slots[0] 的第二个位保存了槽 1 的保存情况,以此类推
unsigned char slots[REDIS_CLUSTER_SLOTS/8];

// 该节点负责处理的槽数量
int numslots;


// 存储处理各个槽的节点
// 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
clusterNode *slots[REDIS_CLUSTER_SLOTS];

至于为什么需要存储slot->node的映射,是为了解决想知道slot[i]由哪个node存储,使用该映射能在\(O(1)\)内查询到,否则需要遍历clusterState.nodes字典中的所有clusterNode结构,检查这些结构的slots数组,直到找到负责处理slot{i]的节点为止。

# 将slot分配给指定节点
CLUSTER ADDSLOTS <slot> [slot ...]

命令处理

假设客户端向集群发送key相关指令,处理流程如下:

  1. 接受命令的Node计算出key属于哪个slot,并判断当前slot是否属于自己,是则执行
    • 通过CRC16(key) & 16383计算key的所属slot
  2. 否则给客户端返回MOVED错误(附带处理该slot的Node信息),客户端重新给正确Node发送请求。
    • MOVED <slot> <ip>:<port>

哈希分布

  • 数据分散度更高
  • 键值分布业务无关
  • 无法顺序访问
  • 支持批量操作
  • Redis Cluster,Memcache

顺序分布

  • 数据分散度易倾斜
  • 键值业务相关
  • 可顺序访问
  • 不支持批量操作
  • BigTable,HBase
  1. 节点取余分区
  2. 一致性哈希分区
  3. 虚拟槽分区

节点取余

  • 客户端分片:哈希 + 取余
  • 节点伸缩:数据节点关系变化,导致数据迁移
  • 迁移数量和添加节点数量有关:建议翻倍扩容

一致性哈希

  • 客户端分片:哈希 + 顺时针(优化取余)
  • 节点伸缩:只影响临近节点,但是还是有数据迁移
  • 翻倍伸缩:保证最小迁移数据和负载均衡

虚拟槽分区

  • 预设虚拟槽:每个槽映射一个数据子集,一般比节点数大
  • 良好的哈希函数
  • 服务端管理节点,槽,数据,例如Redis Cluster

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!