主从(master-slave)
在Redis中可以通过执行SLAVEOF命令来进行服务器的复制,其中执行命令的被称为从服务器(SLAVE),而另一个为主服务器(MASTER)其命令格式为(SLAVEOF 主服务器id 端口号)。
当成为该服务器的从服务器后会发送PING命令,确认通信是否正常。当且仅当主服务器返回PONG时为正常操作。下一步进行身份认证,发送AUTH命令携带自己masterauth参数,如果主服务器没有设置requirepass从服务器没有masterauth则照常进行。如果发送密码与requirepass相同也正常进行,密码不同会返回 invalid password错误,当两个分别缺少一个时分别返回NOAUTH错误以及no password is set错误。并进行重试。当正常之后将会发送REPLCONF listening-port < port-number >命令来发送监听端口号 紧接着会进行同步操作PSYNC命令。
PSYNC
从2.8开始Redis使用PSYNC来替代SYNC命令其具有完整重同步与部分重同步两种模式。
- 完整重同步主要用于处理初次复制情况,其执行步骤为让主服务器创建并发送RDB文件以及向从服务器发送保存在缓冲区的写命令来同步。
- 部分重同步是用于处理断线后的复制情况。当从服务器断线重连后主服务器会将断开期间的写命令发送给从服务器,从服务器只用接受命令即可恢复同步。
- 部分重同步由主服务其复制偏移量 复制积压缓冲区以及服务器运行ID来构成,
- 主从服务器都拥有自己的复制偏移量,分别发送接受时都会在自己的复制偏移量加上对应字节数量值,通过比较主从的复制偏移量即可确认是否处于主从一致状态。
- 主服务器的复制积压缓冲区是一个FIFO队列,其默认大小为1MB当命令传播时不仅将命令发送给从服务器还会保存在复制积压缓冲区里,在这个复制积压缓冲区会记录每个字节的复制偏移量。
- 当服务器断线重连后会发送自己的复制偏移量给主服务器,如果可以从缓冲区中补齐该偏移量则进行部分重同步操作,否则执行完全重同步操作
缓冲区的最小大小可以通过 second*write_size_per_second来计算(second为平均重新上线时间,write_size_per_second为平均每秒写命令数据量)。一般可以来个2倍值安全些。修改其参数可以在配置文件中repl-backlog-size选项。
# The bigger the replication backlog, the longer the time the slave can be
# disconnected and later be able to perform a partial resynchronization.
#
# The backlog is only allocated once there is at least a slave connected.
#
# repl-backlog-size 1mb
- 当然为了识别哪个服务器还需要其运行ID,当执行完第一次的复制操作时从服务器会保存主服务器的运行ID。在断线重连时发送回主服务器,主服务器将会对其比对,如果不同直接执行完整重同步操作。
- 发送PSYNC格式为 PSYNC < RUNID > < OFFSET > 分别为 主服务器ID与自身偏移量。如果是第一次发送 主服务器ID为? 偏移量为-1
- 主服务器返回值由以下部分 :
1.FULLRESYNC < RUNID > < OFFSET > 表示执行完整重同步,并保存主服务器id,将以该偏移量作为参照值。
2. 返回 CONTINUE 即为部分重同步
3. 返回 ERR表示版本低于2.8将会发送SYNC执行完整重同步操作。
心跳检测
在命令传播阶段,从服务器会默认每秒1次的频率发生 REPLCONF ACK < replication_offset >命令,其主要目的是为了检测连接状态,检测命令丢失,辅助实现min-slaves选项。
- min-slaves选项。Redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令。该选项意思为 当从服务器数量小于3或者延迟都大于10秒时主服务器将拒绝执行写命令。
# min-slaves-to-write 3
# min-slaves-max-lag 10
Sentinel
Sentinel(哨岗)是Redis的高可用性的解决方案。通过Sentinel系统可以监视多个主服务器以及其下的从服务器。当主服务器下线时会自动将从服务器升级为主服务器来替代主服务器。主服务器(旧)重新上线时将会成为主服务器(新)的从服务器。
- 启动并初始化Sentinel
命令一:$ redis -sentinel /path/to/your/sentinel.conf(C:\Users\95\Desktop\redis-3.0-annotated-unstable\sentinel.conf)
命令二:$ redis-server /path/to/your/sentinel.conf --sentinel(C:\Users\95\Desktop\redis-3.0-annotated-unstable\sentinel.conf)
当启动时将会执行以下步骤。
- 初始化服务器
- 基本一致但是有差别
- 数据库部分功能不使用
- 复制命令 发布订阅命令 文件时间事件处理器使用
- 将普通Redis服务器使用代码替换成Sentinel专用代码
// sentinel 的默认端口号 #define REDIS_SENTINEL_PORT 26379
- 客户端可对Sentinel命令仅有{PING,SENTINEL,INFO,SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE}
- 初始化Sentinel状态
- 初始化masters属性,其数据结构为字典,字典的键名为主服务器的名字值为对应的监视结构(sentinelRedisInstance结构,完整版如下,后面接着各别特殊的代码)
typedef struct sentinelRedisInstance {
// 标识值,记录了实例的类型,以及该实例的当前状态
int flags; /* See SRI_... defines */
// 实例的名字
// 主服务器的名字由用户在配置文件中设置
// 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
// 格式为 ip:port ,例如 "127.0.0.1:26379"
char *name; /* Master name from the point of view of this sentinel. */
// 实例的运行 ID
char *runid; /* run ID of this instance. */
// 配置纪元,用于实现故障转移
uint64_t config_epoch; /* Configuration epoch. */
// 实例的地址
sentinelAddr *addr; /* Master host. */
// 用于发送命令的异步连接
redisAsyncContext *cc; /* Hiredis context for commands. */
// 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接
// 仅在实例为主服务器时使用
redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */
// 已发送但尚未回复的命令数量
int pending_commands; /* Number of commands sent waiting for a reply. */
// cc 连接的创建时间
mstime_t cc_conn_time; /* cc connection time. */
// pc 连接的创建时间
mstime_t pc_conn_time; /* pc connection time. */
// 最后一次从这个实例接收信息的时间
mstime_t pc_last_activity; /* Last time we received any message. */
// 实例最后一次返回正确的 PING 命令回复的时间
mstime_t last_avail_time; /* Last time the instance replied to ping with
a reply we consider valid. */
// 实例最后一次发送 PING 命令的时间
mstime_t last_ping_time; /* Last time a pending ping was sent in the
context of the current command connection
with the instance. 0 if still not sent or
if pong already received. */
// 实例最后一次返回 PING 命令的时间,无论内容正确与否
mstime_t last_pong_time; /* Last time the instance replied to ping,
whatever the reply was. That's used to check
if the link is idle and must be reconnected. */
// 最后一次向频道发送问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */
// 最后一次接收到这个 sentinel 发来的问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
we received a hello from this Sentinel
via Pub/Sub. */
// 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_master_down_reply_time; /* Time of last reply to
SENTINEL is-master-down command. */
// 实例被判断为 SDOWN 状态的时间
mstime_t s_down_since_time; /* Subjectively down since time. */
// 实例被判断为 ODOWN 状态的时间
mstime_t o_down_since_time; /* Objectively down since time. */
// SENTINEL down-after-milliseconds 选项所设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
mstime_t down_after_period; /* Consider it down after that period. */
// 从实例获取 INFO 命令的回复的时间
mstime_t info_refresh; /* Time at which we received INFO output from it. */
/* Role and the first time we observed it.
* This is useful in order to delay replacing what the instance reports
* with our own configuration. We need to always wait some time in order
* to give a chance to the leader to report the new configuration before
* we do silly things. */
// 实例的角色
int role_reported;
// 角色的更新时间
mstime_t role_reported_time;
// 最后一次从服务器的主服务器地址变更的时间
mstime_t slave_conf_change_time; /* Last time slave master addr changed. */
/* Master specific. */
/* 主服务器实例特有的属性 -------------------------------------------------------------*/
// 其他同样监控这个主服务器的所有 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 如果这个实例代表的是一个主服务器
// 那么这个字典保存着主服务器属下的从服务器
// 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
dict *slaves; /* Slaves for this master instance. */
// SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum; /* Number of sentinels that need to agree on failure. */
// SENTINEL parallel-syncs <master-name> <number> 选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs; /* How many slaves to reconfigure at same time. */
// 连接主服务器和从服务器所需的密码
char *auth_pass; /* Password to use for AUTH against master & slaves. */
/* Slave specific. */
/* 从服务器实例特有的属性 -------------------------------------------------------------*/
// 主从服务器连接断开的时间
mstime_t master_link_down_time; /* Slave replication link down time. */
// 从服务器优先级
int slave_priority; /* Slave priority according to its INFO output. */
// 执行故障转移操作时,从服务器发送 SLAVEOF <new-master> 命令的时间
mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
// 主服务器的实例(在本实例为从服务器时使用)
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
// INFO 命令的回复中记录的主服务器 IP
char *slave_master_host; /* Master host as reported by INFO */
// INFO 命令的回复中记录的主服务器端口号
int slave_master_port; /* Master port as reported by INFO */
// INFO 命令的回复中记录的主从服务器连接状态
int slave_master_link_status; /* Master link status as reported by INFO */
// 从服务器的复制偏移量
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
/* 故障转移相关属性 -------------------------------------------------------------------*/
// 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
// 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
// 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。
char *leader; /* If this is a master instance, this is the runid of
the Sentinel that should perform the failover. If
this is a Sentinel, this is the runid of the Sentinel
that this Sentinel voted as leader. */
// 领头的纪元
uint64_t leader_epoch; /* Epoch of the 'leader' field. */
// 当前执行中的故障转移的纪元
uint64_t failover_epoch; /* Epoch of the currently started failover. */
// 故障转移操作的当前状态
int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
// 状态改变的时间
mstime_t failover_state_change_time;
// 最后一次进行故障迁移的时间
mstime_t failover_start_time; /* Last failover attempt start time. */
// SENTINEL failover-timeout <master-name> <ms> 选项的值
// 刷新故障迁移状态的最大时限
mstime_t failover_timeout; /* Max time to refresh failover state. */
mstime_t failover_delay_logged; /* For what failover_start_time value we
logged the failover delay. */
// 指向被提升为新主服务器的从服务器的指针
struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
/* Scripts executed to notify admin or reconfigure clients: when they
* are set to NULL no script is executed. */
// 一个文件路径,保存着 WARNING 级别的事件发生时执行的,
// 用于通知管理员的脚本的地址
char *notification_script;
// 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,
// 需要执行的脚本的地址
char *client_reconfig_script;
} sentinelRedisInstance;
该结构保存着实例的IP地址以及端口号,
// 实例的地址
sentinelAddr *addr; /* Master host. */
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
-
根据配置文件初始化监视主服务器列表
会将Sentinel配置文件中的配置进行载入格式为# sentinel monitor <master-name> <ip> <redis-port> <quorum> # sentinel down-after-milliseconds <master-name> <milliseconds> # sentinel parallel-syncs <master-name> <numslaves> # sentinel failover-timeout <master-name> <milliseconds>
-
创建连向主服务器的网络链接
- 每个主服务器将会接受到来自Sentinel的连接。一个是命令连接,一个是订阅连接订阅__sentinel__:hello 频道
- Sentinel默认每10秒一次的频率发送INFO命令,获得主服务器当前信息。用以分析主服务器本身的信息,以及所属从服务器的信息。如果服务器重启会更新对应信息
- 同样的Sentinel也会连接到对应从服务器,处理与主服务器相同。
- 默认以两秒一次的频率发送以下命令
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
s开头的是Sentinel本身的信息m则为主服务器的信息,其意义分别为:IP地址,端口号,运行id,当前配置纪元。 - Sentinel如果有多个同时监控主服务器时 他们订阅的频道为同个,也就是说他们能接收到其余Sentinel发送的信息,接受信息并提取runid如果相同便说明是自身发送,不处理,否则进行处理。提取出其他参数IP地址,端口号,运行id,当前配置纪元。检测Sentinels字典,该更新更新,该添加添加。当发现新Sentinel时会创建一个新的命令链接。
- 默认情况下会以1秒一次的频率发送PING命令(所有有命令连接的存在),通过返回回复判断该存在是否在线(有效回复为:PONG、LOADING、MASTERDOWN)判断时间长度限度为down-after-milliseconds
sentinel down-after-milliseconds mymaster 30000
当超时会修改SRI_S_DOWN结构表示其主观下线。当判断为主观下线时会询问其余Sentinel判断是否主观下线,命令为SENTINEL is-master-down-by-addr < ip > < port> <current_epoch > <runid >
其意义分别为:IP地址、端口、自身当前配置纪元、自身运行id或者*号,其余Sentinel收到后会检测该服务器状态并返回一条回复,格式如下< down_state >< leader_runid>< leader_epoch >
意义分别为:是否下线(1/0)、局部领头Sentinel runid或者 * 、局部领头Sentinel配置纪元(当leader_runid不为 * 有效)否则恒为0。根据该返回信息,将统计判断下线数量 如果超过即判断该服务器已下线其值为< quorum > (看上面配置信息) - 客观下线。当被判断客观下线时,Sentinel会协商推举领头Sentinel来进行故障转移操作。每次选举之后所有Sentinel配置纪元会自增一次。每当发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为领头Sentinel。当发送判断主观下线命令时,runid不为*便要求目标Sentinel将自己设置为领头Sentinel。当目标设置领头Sentinel时将会拒绝其余Sentinel设置要求,返回的leader_runid,leader_epoch 分别为设置的领头SentinelID和配置纪元。源Sentinel接受到时将检测leader_epoch 与自身配置纪元是否相等,相等将取出leader_runid判断自身id是否一致,一致即表示被设置为局部Sentinel,当被超过半数的Sentinel设置其为局部领头Sentinel,其将会成为领头Sentinel,给定期限内如果没有达到要求,则在一段时间后再次选举直至推选出领头Sentinel为止。
- 故障转移:在已下线主服务器中挑选从服务器设置为主服务器,步骤入下。
- 将下线主服务器的从服务器保存到一个列表
- 删除已下线或者断线的从服务器
- 删除最近5秒内未回复INFO的从服务器
- 删除与下线主服务器断开链接超过down-after-milliseconds*10毫秒的从服务器该选项为指定判断主服务器下线所需时间。
- 根据优先级选出优先级高的从服务器
- 如果选出多台则通过复制偏移量的大小选择
- 如果还是有多台就通过运行ID进行排序,选择最小的从服务器。
- 发送SLAVEOF no one命令
- 发送INFO检测其角色
- 发送SLAVEOF < new_master_ip > < new_master_port > 命令给其余从服务器。
集群
当各个Redis节点连接起来便成为Redis集群。连接节点的命令为CLUSTER MEET < ip > < port >
(加入对方集群)CLUSTER NODES
(检测当前集群拥有多少节点)。
- 启动节点,服务器启动时根据cluster-enabled配置判断是否开启集群模式,如果为YES成为一个节点,否则为单机模式。
# cluster-enabled yes
节点能够使用单机服务器的所有功能。以及只用集群模式才会用到的数据。结构为clusterNode、clusterLink、clusterState。
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 */
// 由这个节点负责处理的槽
// 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长
// 每个字节的每个位记录了一个槽的保存状态
// 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理
// 比如 slots[0] 的第一个位保存了槽 0 的保存情况
// slots[0] 的第二个位保存了槽 1 的保存情况,以此类推
unsigned char slots[REDIS_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 */
// 最后一次发送 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 */
};
clusterNode :用以记录自身的状态
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;
clusterLink :保存链接节点所需的有关信息
typedef struct clusterState {
// 指向当前节点的指针
clusterNode *myself; /* This node */
// 集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
// 集群当前的状态:是在线还是下线
int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */
// 集群中至少处理着一个槽的节点的数量。
int size; /* Num of master nodes with at least one slot */
// 集群节点名单(包括 myself 节点)
// 字典的键为节点的名字,字典的值为 clusterNode 结构
dict *nodes; /* Hash table of name -> clusterNode structures */
// 节点黑名单,用于 CLUSTER FORGET 命令
// 防止被 FORGET 的命令重新被添加到集群里面
// (不过现在似乎没有在使用的样子,已废弃?还是尚未实现?)
dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
// 记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点
// migrating_slots_to[i] = NULL 表示槽 i 未被迁移
// migrating_slots_to[i] = clusterNode_A 表示槽 i 要从本节点迁移至节点 A
clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];
// 记录要从源节点迁移到本节点的槽,以及进行迁移的源节点
// importing_slots_from[i] = NULL 表示槽 i 未进行导入
// importing_slots_from[i] = clusterNode_A 表示正从节点 A 中导入槽 i
clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];
// 负责处理各个槽的节点
// 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
clusterNode *slots[REDIS_CLUSTER_SLOTS];
// 跳跃表,表中以槽作为分值,键作为成员,对槽进行有序排序
// 当需要对某些槽进行区间(range)操作时,这个跳跃表可以提供方便
// 具体操作定义在 db.c 里面
zskiplist *slots_to_keys;
/* The following fields are used to take the slave state on elections. */
// 以下这些域被用于进行故障转移选举
// 上次执行选举或者下次执行选举的时间
mstime_t failover_auth_time; /* Time of previous or next election. */
// 节点获得的投票数量
int failover_auth_count; /* Number of votes received so far. */
// 如果值为 1 ,表示本节点已经向其他节点发送了投票请求
int failover_auth_sent; /* True if we already asked for votes. */
int failover_auth_rank; /* This slave rank for current auth request. */
uint64_t failover_auth_epoch; /* Epoch of the current election. */
/* Manual failover state in common. */
/* 共用的手动故障转移状态 */
// 手动故障转移执行的时间限制
mstime_t mf_end; /* Manual failover time limit (ms unixtime).
It is zero if there is no MF in progress. */
/* Manual failover state of master. */
/* 主服务器的手动故障转移状态 */
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
/* 从服务器的手动故障转移状态 */
long long mf_master_offset; /* Master offset the slave needs to start MF
or zero if stil not received. */
// 指示手动故障转移是否可以开始的标志值
// 值为非 0 时表示各个主服务器可以开始投票
int mf_can_start; /* If non-zero signal that the manual failover
can start requesting masters vote. */
/* The followign fields are uesd by masters to take state on elections. */
/* 以下这些域由主服务器使用,用于记录选举时的状态 */
// 集群最后一次进行投票的纪元
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
// 在进入下个事件循环之前要做的事情,以各个 flag 来记录
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
// 通过 cluster 连接发送的消息数量
long long stats_bus_messages_sent; /* Num of msg sent via cluster bus. */
// 通过 cluster 接收到的消息数量
long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/
} clusterState;
clusterState :节点总网络信息。
CLUSTER MEET命令执行后步骤:
- 创建一个clusterNode 结构将其添加到自身的clusterState.nodes字典中同时返回一条MEET信息,对方节点执行同样操作并返回PONG。自身再次返回PING消息。
- 通过Gossip指令传播给其他节点。依次握手
槽指派
集群采用分片的方式来保存数据库中的键值对,整个数据库将被分为16384个槽,每个键都属于这16384个槽的其中一个。
- 为什么是16384(2的14次方)?
The reason is:
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 with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
1、正常的心跳包携带节点的完整配置,可以用幂等方式替换旧节点以更新旧配置。 这意味着它们包含原始形式的节点的插槽配置,它使用带有16k插槽的2k空间,但使用65k插槽时将使用高达8k的空间。
2、同时,由于其他设计权衡,Redis Cluster不太可能扩展到超过1000个主节点。
因此,16k处于正确的范围内,以确保每个主站有足够的插槽,最多1000个主站,但足够小的数字可以轻松地将插槽配置传播为原始位图。 请注意,在小型集群中,位图难以压缩,因为当N很小时,位图将设置插槽/ N位,这是设置的大部分位。
每个节点都会处理0-16384个槽。当16384个槽都有节点处理时,该集群将处于上线状态,只要有一个槽无节点处理,将会处于下线状态。 指派槽的命令为CLUSTER ADDSLOTS <slot> [slot..]
- 记录槽的信息是clusterNode 结构中的slots属性以及numslots属性。
- slots:该属性为二进制位数组。长度为16384/8=2048个字节,共包含16384个二进制位,Redis以0为起始索引,16384为终止索引。当前索引表示的1或0表示是否处理对应槽。
- numslots:则记录处理槽的数量。
- 节点会向其他节点发送自己的slots数组告知其余节点自身处理哪些节点。其余节点接收到信息时会在clusterState.nodes对其clusterNode 进行数据更新。
- 其余节点的clusterState本身的slots为clusterNode 数组。其指向对应处理节点节点。因此也会进行更新。
- 当集群上线时。客户端发送命令,接受命令的节点会计算出该键属于哪个槽。并检查是否属于自己所管理的槽。如果是直接执行,否则返回MOVED错误并导向正确节点。
- 计算键属于那个槽的算法
def slot_number(key) return CRC16(key)&16383;
采用CRC16计算key的校验和&16383的值为键的槽号。可通过CLUSTER KEYSLOT < KEY > 查看属于哪个槽。 - TIPS:集群只能使用0号数据库。
- 节点使用跳跃表保存槽与键的关系,跳跃表的分值为槽号。每个节点的成员都是一个数据库键。因此可以方便的使用CLUSTER GETKEYSINSLOT < slot > < count >命令来执行属于该槽的键。
重新分片
集群采用redis-trib程序来进行重新分片的操作。(id均为runid)
- redis-trib向目标节点发送CLUSTER SETSLOT < SLOT > IMPORTING < SOURCE_ID >命令
- 向源节点发送CLUSTER SETSLOT < SLOT > MIGRATING < target_id >命令
- 向源节点发送CLUSTER GETKEYSINSLOT < SLOT > < count >命令 获得属于slot的键名
- 再次发送MIGRATE < TARGET_IP > < TARGET_PORT> < KEY_NAME> 0 < TIMEOUT >
- 重复3.4直至迁移完毕。
- 发送CLUSTER SETSLOT < SLOT > NODE < TARGET_ID > 通知其余节点槽分配已修改。
ASK
在重新分片时,客户端发送命令。将会在源节点将会在自己数据库中查找。如没查到返回ASK错误并引导至目标节点执行命令。
集群中的复制与故障转移
与单机类似
指定从节点的命令 CLUSTER REPLICATE < NODE_ID >使接受命令的节点成为NODE_ID节点的从节点。并进行复制步骤如下。
- 将自己的slaveof指针指向主节点。
- 修改flags属性
- 正式复制
与Sentinel类似的进行节点下线操作(由节点进行)。与Sentinel类似的进行投票(由从节点发起),获得大于一半的主节点支持的从节点将会成为新的主节点。
消息
节点发送的消息主要有:MEET、PING、PONG、FAIL、PUBLISH.
MEET:请求接受者加入发送者集群中。
PING:检测是否在线
PONG:返回通知
FAIL:判断进入下线状态
PUBLISH:广播命令
憋了那么多天终于憋完了。应该还有两篇就结束该系列!