redis集群

无论是单机还是主从结构,都无法解决数据量过大导致内存/虚拟内存无法村下所有数据的问题。redis集群通过分片根据key计算出数据所属的槽(slot),通过指派使每个集群节点负责一部分槽内数据的处理,从而成为一种分布式数据库。

redis集群一共有16384个槽,集群内每个节点可以负责处理其中一部分槽。下图表示集群内有3个节点,分别在端口7000,7001,7002。其中0-5000号槽被指派给7000端口所在的节点,...


image.png

1.节点

1.1 构建集群

redis集群的节点在初始时都是互相独立的,要组建集群只需在某台主机上使用CLUSTER MEET 命令,当前主机就会与目的主机进行连接,连接成功后就会将对方添加到自己所在的集群内。

为了使7000-7002端口所在的3个服务主机构成一个[图片上传中...(image.png-891ba7-1632229388314-0)]
集群,只需要在7000端口的客户端分别输入
CLUSTER MEET <127.0.0.1> <7001>
CLUSTER MEET <127.0.0.1> <7002>
即可。

image.png

1.2 集群对应的数据结构

集群内每一个主机都对应一个clusterNode类型的数据。

typedef struct clusterNode {
    mstime_t ctime; /* Node object creation time. */
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* CLUSTER_NODE_... */
  ...
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by 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. Note that it
                                    may be NULL even if the node is a slave
                                    if we don't have the master node in our
                                    tables. */
  ...
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    int port;                   /* Latest known clients port of this node */
    int cport;                  /* Latest known cluster port of this node. */
    clusterLink *link;          /* TCP/IP link with this node */
  ...
} clusterNode;

clusterNode中比较关键的几个属性分别是: slots数组,长度为16384/8字节(16384 = 2^^14位,计算key应位于哪个slot时取余数方便),以一个bitmap的形式表示了当前节点处理的slot; 一个指向slave数组的指针(如果当前主机为master);一个指向master主机的指针(如果当前为slave);另外在clusterLink中包含了当前节点的tcp/ip连接信息,如文件描述符,输入、输出缓冲区等。

整个集群的信息由clusterState表示。 在每个节点中都会保存一个clusterState结构,表示在当前主机的视角下集群所处的状态。

typedef struct clusterState {
    // 指向表示当前节点自身的clusterNode
    clusterNode *myself;  /* This node */
    uint64_t currentEpoch;
    // 集群当前状态:在线 || 下线
    int state;
    // 集群中有多少节点至少处理一个slot
    int size;
    // 集群中所有节点的字典,以name : clusterNode形式存储
    dict *nodes;          /* Hash table of name -> clusterNode structures */
  ...
    clusterNode *slots[CLUSTER_SLOTS];
  ...
} clusterState;

在上述3个节点创建完集群后的初始状态下,每个节点都没有被分配任何负责处理的槽,在7000节点视角下状态如下图


image.png

1.3 槽指派

通过在某个节点的客户端发送CLUSTER ADDSLOTS [slot ...]命令,可以将命令中的slot分派给当前节点负责处理。分派主要需要完成这样几件事情:

  1. 记录节点的槽指派信息。 即将节点对应的clusterNode内的slots数组相应位置为1, 同时使clusterState中的slots数组的对应位存放的指针指向当前clusterNode。
  2. 传播节点的槽指派信息。当前节点不仅要自己记录一份指派信息,还需要将指派信息发送给其他节点。其他节点在收到后,修改自己的clusterState,将slots数组的对应位指向被分派的节点,同时修改该节点的slots数组。

在所有节点指派完成之后,每个节点内clusterState的slots数组状态大致如下图。同时,各个clusterNode内,slots数组中被该节点负责的槽被置位为1.


image.png

此时,想要知道某个slot被哪个节点负责处理,只需要通过clusterState的slots数组对应位的指针,即可访问到该节点的信息。而如果想要知道当前节点所负责的slot信息(例如发送当前节点负责处理的slot信息给其他节点),只需要取出myself指向的clusterNode,拿到它的slots数组,取出被置位1的位就行了。

2. 执行命令

clusterState.slots可以快速定位到slot被哪个节点负责处理,因此在集群中执行命令也就相对容易了:客户端连接集群中任一节点,向其发送对key的处理命令。收到命令的节点现根据key计算出该key应该配分配到哪个slot上,比如slot = CRC16(key)%16834根据clusterState.slots[slot]就能快速定位到对应的节点N。然后将节点N与myself指向的节点对比,如果是同一个,那么就可以直接处理。如果不是,那么说明应当由其他节点处理。此时向客户端返回MOVED错误,根据节点N的信息,指引客户端去请求对应的主机。
其流程如下图:

image.png

3.重新分片

重新分片可以将已经指派给某节点的任意数量的槽,重新分派给另外一个节点。重新分派后,槽和槽所属的键值对会被从源节点移动到目标节点。重新分片可以在线进行,并且再重新分片过程中,源节点和目标节点都可以继续处理命令请求。

重新分片由redis-trib负责执行,通过向源节点和目标节点发送命令来完成该操作。
对每个槽:

  1. 向目标节点发送CLUSTER SETSLOT IMPORTING 命令,让其准备好从源节点导入属于slot的键值对数据
  2. 向源节点发送CLUSTER SETSLOT MIGRATING 命令,让其准备好将slot内的键值对迁移到target
  3. 向源节点发送CLUSTER GETKEYSINSLOT 命令,获得最多count个key name
  4. 对于每个key_name, redis-trib向源节点发送MIGRATE 0 ,将key原子地迁移至目标节点
  5. 重复3和4,直到slot内所有的key都被迁移完成。
  6. 向集群内任一节点发送``CLUSTER SETSLOT NODE ```,将slot指派给target,然后通过消息发送到整个集群,每个节点更新自己的slotState。


    迁移slot内的键

4.复制与故障转移

1.集群设置从节点

向某个服务器发送 CLUSTER REPLICATE 命令,使之复制node_id对应的节点,成为其从节点。收到命令的服务器需要做以下几件事:

  1. 在自己的clusterState.nodes中找到node_id对应的clusterNode,将自己的slaveof指向该node。
  2. 修改自身flag, 由master 变为slave
    3.调用复制代码,根据master的ip:port发起复制请求。(这一过程和之前讲的主从复制相同,类似于SLAVEOF )
  3. 通知集群内其他节点,这些节点收到通知后修改自己的clusterState

2.故障检测

  • 集群内每个节点定期向集群内其他节点发送PING消息,如果某节点回复超时,那么发送的节点将其标记为疑似下线。(修改clusterState.nodes中对于clusterNode的标志)
  • 各节点间通过消息交换集群中各节点的状态信息。
  • 当某节点A得知B认为C疑似下线时,向C对应的clusterNode.fail_reports添加该报告。
    如果集群内半数以上负责处理槽的主节点都向节点A报告C为疑似下线,那么X就被A标记为已下线。A将C已下线的消息广播到集群内,所有收到消息的节点都将该节点标记为已下线。

3.故障转移

当已下线节点C的从节点C1收到C已下线时,开始进行故障转移。

  1. 选举新的主节点。 (基于raft领导选举算法)
    1.1 某个从节点开始故障转移时,先将自己内部clusterState.epoch加1,然后发起投票
    1.2 负责处理槽的主节点具有投票权,每个投票者根据发过来的epoch和自己当前的epoch对比,如果当前epoch小于请求的epoch,则投票给他,并更新自己的epoch。
    1.3 胜出规则: 得票数不小于 N/2 +1 则当选为新的主节点。
    1.4 如果没人胜出,则进行下一轮投票
  2. 将已下线主节点负责处理的槽指派给自己。
  3. 将自己成为主节点的消息广播给集群内的其他节点。
  4. 转移完成,C1开始处理落在原来由C负责的槽内的请求。

你可能感兴趣的:(redis集群)