- pika支持m-s的复制方式,跟redis主从复制命令一样,通过slave执行slaveof命令来触发。
- slave的trysync线程向master发起trysync,同时将同步位点信息发送给master
- master处理trysync命令,发起对slave的同步过程,从同步点开始顺序发送binlog或进行全同步
全量同步
pika在需要进行全量同步的时候,会将数据文件进行dump后通过rsync的deamon模式发送给slave。
实现逻辑
- slave在trysnc前启动rsync进程启动rsync服务
- master发现需要全同步时,判断是否有备份文件可用,如果没有先dump一份
- master通过rsync向slave发送dump出的文件
- slave用收到的文件替换自己的db
- slave用最新的偏移量再次发起trysnc
- 完成同步
slave连接状态:
- No Connect:不尝试成为任何其他节点的slave
- Connect:Slaveof后尝试成为某个节点的slave,发送trysnc命令和同步点
- Connecting:收到master回复可以slaveof,尝试跟master建立心跳
- Connected: 心跳建立成功
- WaitSync:不断检测是否DBSync完成,完成后更新DB并发起新的slaveof
增量同步
Pika的主从同步是通过Binlog来完成的,在一主多从的结构中,master节点也可以给多个slave复用一个Binlog,不同的slave拥有不同的偏移量。
工作过程
- 当WorkerThread接收到客户端的命令,按照执行顺序,添加到Binlog里
- BinglogSenderThread判断它所负责的从节点在主节点的Binlog里是否有需要同步的命令,若有则发送给从节点
- BinglogReceiverModule模块则做以下三件事情:接收主节点的BinlogSenderThread发送过来的同步命令;把接收到的命令应用到本地的数据上;把接收到的命令添加到本地Binlog里 至此,一条命令从主节点到从节点的同步过程完成
- WorkerThread:接受和处理用户的命令
- BinlogSenderThread:负责顺序地向对应的从节点发送在需要同步的命令
- BinlogReceiverModule: 负责接受主节点发送过来的同步命令
- Binglog:用于顺序的记录需要同步的命令
上图中的BinLogReceiverModule
是为了更好的说明方便而抽象出来的一个对象,从图中可以看出BinLogReceiverModule
是由一个BinlogReceiverThread
和多个BinlogBGWorker
组成。
- BinlogReceiverThread: 负责接受由主节点传送过来的命令,并分发给各个BinlogBGWorker,若当前的节点是只读状态(不能接受客户端的同步命令),则在这个阶段写Binlog
- BinlogBGWorker:负责执行同步命令;若该节点不是只读状态(还能接受客户端的同步命令),则在这个阶段写Binlog(在命令执行之前写)
BinlogReceiverThread接收到一个同步命令后,它会给这个命令赋予一个唯一的序列号(这个序列号是递增的),并把它分发给一个BinlogBGWorker;而各个BinlogBGWorker则会根据各个命令的所对应的序列号的顺序来执行各个命令,这样也就保证了命令执行的顺序和主节点执行的顺序一致了 之所以这么设计主要原因是:
- 配备多个BinlogBGWorker是可以提高主从同步的效率,减少主从同步的滞后延迟
- BinlogBGWorker在执行执行之前写Binlog可以提高命令执行的并行度
- 在当前节点是非只读状态,让BinglogReceiverThread来写Binlog,是为了让Binglog里保存的命令顺序和命令的执行顺序保持一致
Binlog结构
由于主从偏移量一样,所以一旦发生网络或节点故障需要重连主从时,slave只需要将当前的偏移量发送给master,master找到后从该偏移量开始同步后续命令。
理论上命令不做处理一条一条追加到Binlog文件中,这也就意味着如果文件写错一个字节就会导致整个文件不可用,所以pika采用了类似leveldb log的格式来进行存储。
leveldb log
在leveldb中,所有的写操作都必须先成功的append到操作日志中,然后再更新内存memtable
- 可以将随机的写IO变成append,极大的提高写磁盘速度
- 防止在节点down机导致内存数据丢失,造成数据丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14The log file contents are a sequence of 32KB blocks. The only
exception is that the tail of the file may contain a partial block.
Each block consists of a sequence of records:
block := record* trailer?
record :=
checksum: uint32 // crc32c of type and data[] ; little-endian
length: uint16 // little-endian
type: uint8 // One of FULL, FIRST, MIDDLE, LAST
data: uint8[length]
A record never starts within the last six bytes of a block (since it
won't fit). Any leftover bytes here form the trailer, which must
consist entirely of zero bytes and must be skipped by readers.
日志文件由连续的大小为32KB的block组成,block又由连续的record组成,record的格式为 | CRC(4 byte) | Length(2 byte) | type(1 byte) | data |