一、复制(主从复制)
命令:slaveof host port 或者slaveof选项
1.实现
redis复制分为同步(sync)和命令传播(command propagate)
1.同步:
由从服务器发起,将从服务器的数据库状态同步至主服务器当前的数据库状态。
2.命令传播:
有主服务器发起,解决在主服务器的数据库状态被修改从而导致主从数据库状态不一致。
同步完成后,主从服务器的数据库状态一致。当主服务器执行客户端的写命令时,主服务器就会对从服务器执行命令传播,将写命令发送给从服务器执行。
从服务器对主服务器的复制有两种情况:
初次复制:同步阶段。
断线后复制:命令传播阶段,主从服务器由于网络原因中断复制。网络恢复后,从会使用SYNC命令重新复制主服务器(重同步),这个是十分低效的:
- 主服务器使用BGSAVE命令生成RDB文件,耗费主服务器大量的CPU,内存和磁盘I/O资源。
- 主向从发送RDB文件,耗费主大量的网络资源。影响响应命令请求的时间
- 从载入RDB文件,载入期间从会阻塞,从而无法处理命令请求。
3.2.8及以后的版本复制:
使用PSYNC命令代替SYNC,有两种模式:
完全重同步:同
部分重同步:断线重连后,如何符合条件,主只需将连接断开期间执行的写命令发送给从服务器。
这样显然比SYNC断线后复制好很多。
2.部分重同步的实现
1.复制偏移量
主服务器和从服务器都会维持一个复制偏移量。
- 主每次向从传播N个字节的数据时,就将自己的复制偏移量的值加上N。
- 从每次收到主传来的N个字节的数据时,就将自己的复制偏移量加上N。
若从断线重连后,发现和主的数据库状态不一致,那么从向主发送PSYNC命令后,应该对从服务器执行完重同步还是部分重同步呢?
2.复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度的FIFO队列。默认大小为1MB。
一般设置为 2 * second(重连主所需的平均时间)* write_size_per_second(主每秒产生的写命令数据量,即协议格式的命令长度总和) 来估算。
当从重连上主时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主,若offset偏移量+1的数据仍然存放在复制缓冲积压区时,执行部分重同步操作。否则执行完全重同步操作。
3.服务器的运行ID
每个redis服务器在启动时自动生成,由40个随机的十六进制字符组成。初次复制时,主服务器将自己的运行ID传送给从。当从断线重连上主时,会向主发送初次复制保存的运行ID。若相同并且条件满足,那么主可以对从执行部分重同步,否则就是完全重同步。
3.PSYNC命令的实现
4.复制的完整实现过程
1.从保存主的ip和port
slaveof 127.0.0.1:6380
struct redisServer {
char *masterhost;
int masterport;
}
当执行此命令时,从会将ip和port保存到自己的服务器状态。然后向客户端返回OK,实际的复制工作此时才正式开始,显然slaveof命令是一个异步命令。
2.建立套接字连接
从根据ip和port创建连向主的套接字连接,从将为这个套接字关联一个专门处理复制工作的文件事件处理器(接收RDB文件,接收写命令等);主接受从的套接字连接后将为此从服务器创建相应的客户端状态,并将从看作自己的一个客户端,即从服务器是主服务器的一个客户端。这时从服务器就有两个身份,既是客户端也是服务器。
3.发送PING命令
从成为主的客户端后,第一件事就是发送PING命令:
- 检查套接字的读写是否正常
- 检查主服务器是否能正常的处理命令
4.身份验证
从收到主的PONG回复后,决定是否进行身份验证
5.发送端口信息
从将执行命令REPLCONF listening-port port,向主发送自己的监听端口。
typedef struct redisClient{
//此属性唯一的目的作用就是在主服务器执行命令`INFO replication`时,打印出从服务器的端口
int slave_listening_port;
}
6.同步
从向主发送PSYNC命令,执行同步操作。
执行同步操作之前,从是主的客户端,执行之后,主也就成为从的从的客户端(这样,若执行完全重同步,主才能将保存在缓冲区里面的写命令发送给从执行;若执行部分重同步,主才能向从发送保存在复制积压缓冲区里面的写命令)。在同步操作之后,主从双方都是对方的客户端,这样才能互相进行命令请求和命令回复。
7.命令传播
完成同步之后,主从服务器便进入命令传播阶段。
心跳检测
在命令传播阶段,从默认每秒一次向主发送命令REPLCONF ACK {replication_offset}
作用为:
- 检测主从服务器的连接状态,若主超过1s没有收到从发来的命令,那么连接可能就出现问题。可通过向主发送
INFO replication
命令查看从最后一次向主发送命令距现在的时间 - 辅助实现min-slaves。min-slaves-to-write 3和min-slaves-max-lag 10选项,若从服务器的数量小于3或者三个服务器的延迟(lag,就是1所说INFO命令查看的lag值)值都大于或等于10s时,主拒绝执行写命令。
- 检测命令丢失
由于网络故障,主向从传播的命令半路丢失,当从向主发送REPLCONF ACK {replication_offset}
时,主就会发现数据库状态不一致,从而将根据offset将复制积压缓冲区中对应的数据再次传播。
虽然和部分重同步相似,但是前者是网络没有断线,后者是断线重连。
二、哨兵
Sentinel,实现Redis高可用。
1.sentinel的启动
sentinel的启动命令:redis-sentinel sentinel.conf
or redis-server sentinel.conf
步骤:
- 初始化服务器。哨兵本质上也是一个特殊的redis服务器(不使用redis数据库)
- 将普通Redis服务器使用的代码替换成Sentinel的专用代码,如使用sentinel.c/sentinelcmds作为服务器的命令列表,所以客户端只能使用七个命令:PING,SENTINEL,INFO,SUBSCRIBE。UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE
- 初始化Sentinel状态
struct sentinelState {
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被这个sentinel 监视的主服务器,字典的键是主服务器的名字,字典的值则是一个指向 sentinelRedisInstance 结构的指针
dict *masters;
// 是否进入了 TILT 模式?
int tilt;
// 目前正在执行的脚本的数量
int running_scripts;
// 进入 TILT 模式的时间
mstime_t tilt_start_time;
// 最后一次执行时间处理器的时间
mstime_t previous_time;
// 一个FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
- 根据给定的配置文件,初始化Sentinel的监视主服务器列表
sentinel的masters字典记录了所有被sentinel监视的主服务器的相关信息。字典的键是被监视主服务器的名字;而字典的值则是被监视主服务器对应的 sentinel.c/sentinelRedisInstance 结构(重要)。
typedef struct sentinelRedisInstance {
// 标识值,记录了实例的类型,以及该实例的当前状态,主服务器为SRI_MASTER,从服务器为SRI_SLAVER
int flags;
// 实例的名字
// 主服务器的名字由用户在配置文件中设置,从服务器以及 Sentinel 的名字由 Sentinel 自动设置.格式为 ip:port ,例如 "127.0.0.1:26379"
char *name;
// 实例的运行 ID
char *runid;
// 配置纪元,用于实现故障转移
uint64_t config_epoch;
//主服务器实例所有,key为ip:port,value为sentinelRedisInstance,记录从服务器信息
dict *slavers;
//主服务器实例所有,key为ip:port,value为sentinelRedisInstance,记录其他的sentinel信息
dict *sentinels;
// 实例的地址,指向sentinel.c/sentinelAddr {ip,port}
sentinelAddr *addr;
int parallel_syncs;
mstime_t failover_timeout;
// ...
} sentinelRedisInstance;
- 创建连向主服务器的网络连接
2.sentinel与主从服务器的交互
2.1获取主从信息
默认10s一次分别向主,从服务器发送 info
通过命令回复,提取所需要的信息。 得到主服务器的命令回复后,会从中获取从服务器信息,并建立与从服务器的两个异步连接。
2.2向主从发送和接收频道信息
默认2s一次,通过命令连接向所有被监视的主和从服务器发送命令:
PUBLISH _ _ sentinel _ _:hello “sip,sport,srunid,sepoch,sname,mip,mport,mrunid,mepoch,mname”
当sentinel与服务器建立起连接后,sentinel通过订阅连接,向服务器发送以下命令:
SUBSCRIBE _ _ sentinel _ _:hello
总的来说,
- 对于每个和sentinel连接的服务器,sentinel通过命令连接向服务器的sentinel_ _
_:hello发送信息,并同时通过订阅连接从服务器的此频道接收信息。 - 对于监视同一个服务器的多个sentinel来说,一个sentinel向服务器频道发送的信息,其他sentinel也可以订阅此频道接收(已经订阅),并更新服务器的实例结构(sentinelRedisInstance
)。当然自己本身也会接收到,但通过对比runid相等,对此信息进行丢弃。
目标sentinel接收源sentinel频道信息后:
3. 更新主服务器实例结构中的sentinel字典,如果字典中有源,则进行更新;如果没有,则证明是源是刚刚开始监视主服务器的,那么目标就会为源创建一个sentinel实例,并添加到主服务器的sentinels字典中。
4. 当发现新的sentinel时,会创建连向这个sentinel的命令连接,新的sentinel也会创建同目标sentinel的连接,最终多个sentinel两两相互命令连接,并同时监视主服务器。如下图示:
?sentinel之间为什么不像sentinel和服务器之间一样,创建订阅连接?
sentinel需要通过服务器发出的频道信息来发现未知的sentinel,而相互已知的sentinel只需命令连接即可。
3.哨兵机制
3.1主观下线
sentinel默认每秒一次向所有与它创建了命令连接的实例(主,从,其他sentinel)发送PING,并通过回复判断实例是否在线:
有效回复:+PONG、-LOADING、-MASTERDOWN
无效回复:之外的回复或固定时间内没有回复。
若sentinel配置down-after-millseconds的时间内都是无效回复,那么对此实例进行主观下线并将对应实例的flags置为SRI_S_DOWN。
不同的sentinel有着不同的下线时间配置,所以称为主观下线。
3.2客观下线
客观下线是主服务器才有的功能,当从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。
当sentinel将一个服务器标记为主观下线后,它会向同样监视此主服务器的其他sentinel节点询问主服务器的状态,若得到下线状态(包括主观下线和客观下线)达到足够数量(配置quorum)后,sentinel就会将此主服务器判定为客观下线(打开实例的flags属性的‘‘SRI_O_DOWN”标识),随后进行故障转移操作。
询问:
发送snetinel is-master-down-by-addr ip port current_epoch runid。runid为*,表示检测客观下线;为sentinel的runid,则用于选举领头sentinel。
接收回复,down_state
leader_runid,同上
leader_epoch,仅在runid不为时有效,若为runid为,则一直为0。
3.3选举leader sentinel
选举算法:Raft。
当主服务器被判断为客观下线后,监视这个下线服务器的各个sentinel会进行协商,选举出一个leader sentinel,并由leader sentinel对下线的主服务器执行故障转移操作。
- 向其他snetinel发送snetinel is-master-down-by-addr ip port current_epoch
runid,要求目标sentinel将自己设置为leader(先到先得,之后的都会被目标sentinel拒绝)
epoch:目标sentinel会将epoch设置为局部leader的epoch,只会设置一次,就是在第一次接收此命令的时候 - 接收到回复后,若epoch相同,后runid也相同,那么这个目标sentinel已经将源sentinel设置为leader
若有max(sum/2+1,quorum)个sentinel将源sentinel设置为leader(包括自己),那么选举成功。 - 若在给定的时间内,没有选举成功,那么就会在一段时间后重新选举,直到成功。
epoch:每次选举后,无论成功失败,epoch都会自增1。epoch实际上就是一个计数器。
3.4故障转移
选举出leader sentinel后,它会对已下线的主服务器进行故障转移操作
1.选出新的主服务器
leader sentinel 向被选中的从服务器发送SLAVEOF no one
,然后以每秒一次的频率发送INFO命令,直到role从slave变为master。
选取规则:
将所有从服务器保存在list中,删除下线,最近五秒内没有回复leader sentinel的INFO命令,与主服务器连接断开超过down-after-milliseconds * 10 ms的;再对list中剩余的所有从服务器进行排序(优先级高-> 复制偏移量大-> runid小)
2.修改从服务器的复制目标
向其他从服务器发送SLAVEOF ip port
,复制新的主服务器
3.将旧的主服务器变为从服务器
leader sentinel中会保存旧的主服务器实例信息,当主服务器重新上线时(一直去ping),leader snetinel就会向他发送SLAVEOF ip port
?epoch没有详细的说
三、集群
redis-cluster 是redis提供的分布式数据库方案,通过分片(sharding)来数据共享,并提供复制和故障转移功能。集群管理软件 redis-trib
1.Node
cluster有多个node构成,可使用命令CLUSTER MEET ip port
,可将目标node和此ip:port节点连接,再像其他节点同样发送此命令。
redis-cli -c,以节点模式登录redis客户端。
redis-cli,以单机模式登录客户端。
1.1启动node
1.2 CLUSTER MEET命令
2.哈希槽(SLOT)
redis-cluster通过分片的方式保存数据库的键值对,集群的数据库被分为16384个slot。
当16384个slot全部都被都被指派(assign)到一个或多个节点时,集群才处于上线状态;通过在相应的节点上执行命令CLUSTER ADDSLOTS <slot> [slot ...]
来指派slot。
2.1slot信息的存储
2.1.1 clusterState和clusterNode
Anode会将自己的slots数组通过消息发送给集群中的其他节点(clusterNode.slots),其他节点收到消息后,会在自己的clusterNodes.nodes字典中查看Anode的结构,并对结构中的slots数组进行保存或更新。
2.1.2 CLUSTER ADDSLOTS命令实现
clusterState.slots数组在对应slot上的指针指向当前节点的clusterNodes
clusterNode.slots数组在对应slot上的值变为1。
2.2 在集群中执行命令
2.3 节点数据库的实现
节点只能使用0号数据库不同于单机,除了将键值保存在数据库中,还会将slot-key保存在clusterState.slots_to key这个跳跃表中。
2.4 重新分片
重新分片可以重新将已经指派了的slot改为指派为另一个,并且相关的键值对也会别移动到目标节点。
redis通过redis-trib集群管理软件向源节点和目标节点发送命令来进行重新分片。
2.4.1 重新分片的原理
2.4.2 重新分片中可能的情况
1.ASK错误 和 ASKING命令
在重新分片期间,某个slot中的键值对一部分在目标节点中,一部分还在源节点中。
2.ASK错误和MOVED错误
两者都会导致客户端转向,区别在于前者是在slot迁移过程中使用的一种措施;后者是迁移slot已经结束后的一种错误。
2.5 复制与故障转移
集群的复制和故障转移就是在哨兵机制上构建的。redis的节点分为主节点和从节点,主节点用于处理slot,从节点复制主节点,在主下线时,被选举为主节点,实现故障迁移。
2.5.1 设置主节点
向master发送命令 CLUSTER REPLICATE <node_id>
一个节点成为从节点后,这个信息会通过消息发送给集群中的其他节点。这些节点都会将主节点的clusterNode.slaves属性和numslaves属性记录此从节点的相关信息。
2.5.2 故障检测
每个节点都会定时向其他节点发送PING消息,若接受PING的节点没有在规定时间内返回PONG消息,那么会将此节点标记为PFAIL(疑似下线)
每个节点都会相互交换下线信息,并将下线信息统计在自己clusterState.nodes中对应节点clusterNode.fail_reports链表中:
struct clusterNodeFailReport {
//已经将此节点下线的节点
struct clusterNode *node;
//最后一次从节点收到下线报告的时间,用此时间戳来检查下线报告是否过期
mstime_t time;
}
n/2+1数量的主节点将某个主节点标记为PFAIL,那么这个主节点会被标记为FAIL(已下线),并向集群广播此主节点已经下线的消息,收到此消息的节点都会将此节点标记为已下线。
2.5.3 故障转移
- 从下线主节点的从节点中选举出一个节点(执行SLAVEOF no one)成为新的主节点
- 将旧主节点的所有slots全部指派给自己
- 向集群广播一条PONG消息,表明自己已经成为新的主节点。
如何选举?
集群配置纪元初始值为0
- slave发现自己的master变为FAIL
- 将自己记录的集群currentEpoch(选举轮次标记)加1,并广播信息给集群中其他节点
- 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送结果
- 尝试选举的slave收集master返回的结果,收到超过半数master的统一后变成新Master
- 广播Pong消息通知其他集群节点。
- 如果这次选举不成功,并在一定时间后重新选举,直到选出新的主节点
为了避免多次选举不成功,从节点发现master挂掉后,为了确保Fail在集群中传播,会在一定的延迟后再尝试发送选举请求
•延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。
3.消息
集群中各个节点通过发送和接收消息来进行通信。message主要分为五种:
MEET、PING、PONG、FAIL、PUBLISH
PING:默认每隔1s从已知节点列表中随机选取5个,向其中最长消息没有发送过PING消息的节点发送PING;或者A接收B最后一次发送PONG消息的时间超过A的cluster-node-timeout的一半,那么也会向B发送PING消息。
3.1消息头
节点发送的所有消息都由一个信息头cluster.sh/clusterMsg结构所包裹。
3.1.1 消息头
3.1.2 消息的实现