Redis 单节点存在单点故障问题,为了解决单点问题,一般都需要对 Redis 配置从节点,然后使用哨兵来监听主节点的存活状态,如果主节点挂掉,从节点能继续提供缓存功能。
主从复制一般用于实现数据的读写分离,主节点提供写操作,从节点提供读操作,适用于读多写少的场景。
字段解析:
slaveof命令的处理函数为replicaofCommand:
void replicaofCommand(client *c) {
// slaveof no one命令可以取消复制功能
if (!strcasecmp(c->argv[1]->ptr,"no") &&
!strcasecmp(c->argv[2]->ptr,"one")) {
} else {
// 记录主服务器的Ip地址和端口
server.masterhost = sdsnew(ip);
server.masterport = port;
server.repl_state = REPL_STATE_CONNECT;
}
addReply(c,shared.ok);
}
replicaofCommand函数只是记录主服务器IP地址与端口,并没有向主服务器发起连接请求。
说明连接建立是异步的,主从复制的相关操作是在时间事件处理函数serverCron中进行的:
// 以一秒为周期执行主从复制相关操作
run_with_period(1000) replicationCron();
在replicationCron中,从服务器向主服务器发起连接请求:
if (server.repl_state == REPL_STATE_CONNECT) {
// 在连接中,会创建对应的文件事件
if (connectWithMaster() == C_OK) {
serverLog(LL_NOTICE,"MASTER <-> REPLICA sync started");
server.repl_state = REPL_STATE_CONNECTING;
}
}
Ping包:ping包由一个包头 (type字段为CLUSTERMSG_TYPE_PING(0)) 和多个gossip section (记录的关于其他节点的状态信息,包括节点名称、IP地址、状态以及监听地址,等等)组成。Redis集群中每个节点通过心跳包可以知道其他节点的当前状态并且保存到本节点状态中。
Pong包:pong包格式同ping包,只是包头中的type字段写为CLUSTERMSG_TYPE_PONG(1)。
注意pong包除了在接收到ping包和meet包之后会作为回复包发送之外,当进行主从切换之后,新的主节点会向集群中所有节点直接发送一个pong包,通知主从切换后节点角色的转换。
主服务器处理psync命令的入口函数为syncCommand。
【Redis 2.8 之后使用 psync [runId] [offset] 命令;支持全量和部分复制;
Redis 4.0针对主从复制又提出了两点优化,提出了psync2协议】
psync2 他的好处在于 redis 主从切换后,不需要重新进行 重新 fullsync 同步,只需要部分同步,有点类似 binlog 那种
主服务器每次接收到写命令请求时,都会将该命令请求广播给所有从服务器,同时记录在复制缓冲区中。向从服务器广播命令请求的实现函数为replicationFeedSlaves,逻辑如下:
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
//如果与上次选择的数据库不相等,需要先同步select命令
if (server.slaveseldb != dictid) {
//将select命令添加到复制缓冲区
if (server.repl_backlog)
feedReplicationBacklogWithObject(selectcmd);
//向所有从服务器发送select命令
while((ln = listNext(&li))) {
addReply(slave,selectcmd);
}
}
server.slaveseldb = dictid;
if (server.repl_backlog) {
//将当前命令请求添加到复制缓冲区
}
while((ln = listNext(&li))) {
//向所有从服务器同步命令请求
}
}
1⃣️ 从节点发送 psync ? -1 命令(因为第一次发送,不知道主节点的 runId,所以为?,因为是第一次复制,所以 offset=-1)。
2⃣️ 主节点发现从节点是第一次复制,返回 FULLRESYNC {runId} {offset},runId 是主节点的 runId,offset 是主节点目前的 offset。
3⃣️ 从节点接收主节点信息后,保存到 info 中。
4⃣️ 主节点在发送 FULLRESYNC 后,启动 bgsave 命令,生成 RDB 文件(数据持久化)。
5⃣️ 主节点发送 RDB 文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。
6⃣️ 从节点清理自己的数据库数据。
7⃣️ 从节点加载 RDB 文件,将数据保存到自己的数据库中
8⃣️ 主节点再将缓冲区中的写命令发送给从节点
1⃣️ 部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync [runId] [offset] 命令实现。
当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,主节点的复制积压缓冲区将这部分数据直接发送给从节点。
这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。
2⃣️ 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内的复制积压缓冲区依然可以保存最近一段时间的写命令数据。
3⃣️ 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分复制。
4⃣️ 主节点接收到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点。
之后根据参数 offset 在复制积压缓冲区中查找,如果 offset 之后的数据存在,则对从节点发送+COUTINUE 命令,表示可以进行部分复制。因为缓冲区大小固定,若发生缓冲溢出,则进行全量复制。
5⃣️ 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态
int masterTryPartialResynchronization(client *c) {
//判断服务器运行ID是否匹配,复制偏移量是否合法
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
goto need_full_resync;
}
//判断复制偏移量是否包含在复制缓冲区
if (!server.repl_backlog ||
psync_offset < server.repl_backlog_off ||
psync_offset > (server.repl_backlog_off +
server.repl_backlog_histlen))
{
goto need_full_resync;
}
//部分重同步,标识从服务器
c->flags |= CLIENT_SLAVE;
c->replstate = SLAVE_STATE_ONLINE;
c->repl_ack_time = server.unixtime;
//将该客户端添加到从服务器链表slaves
listAddNodeTail(server.slaves,c);
//根据从服务器能力返回+CONTINU
if (c->slave_capa & SLAVE_CAPA_PSYNC2) {
buflen = snprintf(buf,sizeof(buf),"+CONTINUE %s\r\n", server.replid);
} else {
buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
}
if (write(c->fd,buf,buflen) != buflen) {
}
//向客户端发送复制缓冲区中的命令请求
psync_len = addReplyReplicationBacklog(c,psync_offset);
//更新有效从服务器数目
refreshGoodSlavesCount();
return C_OK; /* The caller can return, no full resync needed. */
need_full_resync:
return C_ERR;
}
一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。因此,可以使用哨兵机制来管理这个过程。
哨兵是Redis的高可用方案,可以在Redis Master发生故障时自动选择一个Redis Slave切换为Master,继续对外提供服务。
Redis Sentinel 最小配置是一主一从,该系统可以执行以下四个任务:
// 监控一个名称为mymaster的Redis Master服务,地址和端口号为127.0.0.1:6379,quorum为2
sentinel monitor mymaster 127.0.0.1 6379 2
// 如果哨兵60s内未收到mymaster的有效ping回复,则认为mymaster处于down的状态
sentinel down-after-milliseconds mymaster 60000
// 执行切换的超时时间为180s
sentinel failover-timeout mymaster 180000
// 切换完成后,同时向新的Redis Master发起同步数据请求的Redis Slave个数为1
// 即切换完成后依次让每个Slave去同步数据,前一个Slave同步完成后下一个Slave才发起同步数据的请求
sentinel parallel-syncs mymaster 1
// 监控一个名称为resque的Redis Master服务,地址和端口号为127.0.0.1:6380,quorum为4
// quorum:①将master标记客观下线所需的哨兵个数;②选举哨兵执行主从切换所需的票数
sentinel monitor resque 192.168.1.3 6380 4
① 每个 Sentinel 节点都需要定期执行以下任务:每个 Sentinel 以每秒一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 命令。
② 如果一个 slave 距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个slave 会被 Sentinel 标记为主观下线。
③ 如果一个 master 被标记为主观下线,那么正在监视这个 master 的所有 Sentinel 节点,要以每秒一次的频率确认 master 的确进入了主观下线状态。
④ 如果一个 master 被标记为主观下线,并且有足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个 master 被标记为客观下线。
⑤ 一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 mater 和 slave 发送 INFO 命令。
当一个 master 被标记为客观下线时,Sentinel 向下线 master 的所属 slave 发送 INFO 命令的频率,由 10 秒一次改为 1 秒一次。
⑥ 所有 Sentinel 协商客观下线的 master 的状态,如果处于 SDOWN 状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。
⑦ 当没有足够数量的 Sentinel 同意 master 下线时,master 的客观下线状态就会被移除。
当 master 重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。
redis-server /path/to/sentinel.conf --sentinel
执行命令后,执行的主要代码逻辑如下:
main(){
...
//检测是否以sentinel模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv);
...
if (server.sentinel_mode) {
initSentinelConfig(); // 将监听端口置为26379
initSentinel(); // 更改哨兵可执行命令。哨兵中只能执行有限的几种服务端命令,如ping,sentinel,subscribe,publish,info等等。该函数还会对哨兵进行一些初始化
}
...
sentinelHandleConfiguration(); // 解析配置文件,进行初始化
...
sentinelIsRunning(); // 随机生成一个40字节的哨兵ID,打印启动日志
...
}
在main函数的主流程中,只是对其进行了一些初始化。真正建立命令连接和消息连接的操作是在定时任务serverCron
中:
serverCron(){
// ...
// 哨兵模式下,用于建立连接,并且定时发送心跳包并采集信息
if (server.sentinel_mode) sentinelTimer();
// ...
}
该函数主要功能如下:
Redis中选择规则如下:
该状态需要把选择的Redis Slave切换为Redis Master,即哨兵向该Slave发送如下命令:
MULTI //开启一个事务
SLAVEOF NO ONE //关闭该从服务器的复制功能,将其转换为一个主服务器
CONFIG REWRITE //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
CLIENT KILL TYPE normal // 关闭连接到该服务的客户端(关闭之后客户端会重连,重新获取Redis Master的地址)
EXEC //执行事务
哨兵会依次向其他从服务器发送切换主服务器的命令,如下:
MULTI //开启一个事务
SLAVEOF IP PORT //将该服务器设置为向新的主服务器请求数据
CONFIG REWRITE //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
CLIENT KILL TYPE normal //关闭连接到该服务的客户端(关闭之后客户端会重连,重新获取Redis Master的地址)
EXEC //执行事务
同时,在此过程中,哨兵遵循相应的状态转换。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。