zookeeper
kafka
presto
spark
zk选举机制
[toc]
三种选举方式#
-
LeaderElection
-
AuthFastLeaderElection
-
FastLeaderElection (最新默认)
默认的算法是FastLeaderElection,所以这篇主要分析它的选举机制
选举消息内容#
在投票完成后,需要将投票信息发送给集群中的所有服务器,它包含如下内容。
-
Serverid:服务器ID
比如有三台服务器,编号分别是1,2,3。
编号越大在选择算法中的权重越大。
-
Zxid:数据ID
服务器中存放的最大数据ID.
值越大说明数据越新,在选举算法中数据越新权重越大。
-
Epoch:逻辑时钟
或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
-
Server状态:选举状态
LOOKING,竞选状态。
FOLLOWING,随从状态,同步leader状态,参与投票。
OBSERVING,观察状态,同步leader状态,不参与投票。
LEADING,领导者状态。
选举流程:#
目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:
- 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking(选举状态)。
- 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
- 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为领导者,服务器1,2成为小弟。
- 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为小弟。
- 服务器5启动,后面的逻辑同服务器4成为小弟。
选举详细流程#
一、首先开始选举阶段,每个Server读取自身的zxid。
二、发送投票信息
a、首先,每个Server第一轮都会投票给自己。
b、投票信息包含 :所选举leader的Serverid,Zxid,Epoch。Epoch会随着选举轮数的增加而递增。
三、接收投票信息
-
如果服务器B接收到服务器A的数据(服务器A处于选举状态(LOOKING 状态)
1)首先,判断逻辑时钟值:
a)如果发送过来的逻辑时钟Epoch大于目前的逻辑时钟。首先,更新本逻辑时钟Epoch,同时清空本轮逻辑时钟收集到的来自其他server的选举数据。然后,判断是否需要更新当前自己的选举leader Serverid。判断规则rules judging:保存的zxid最大值和leader Serverid来进行判断的。先看数据zxid,数据zxid大者胜出;其次再判断leader Serverid,leader Serverid大者胜出;然后再将自身最新的选举结果(也就是上面提到的三种数据(leader Serverid,Zxid,Epoch)广播给其他server)
b)如果发送过来的逻辑时钟Epoch小于目前的逻辑时钟。说明对方server在一个相对较早的Epoch中,这里只需要将本机的三种数据(leader Serverid,Zxid,Epoch)发送过去就行。
c)如果发送过来的逻辑时钟Epoch等于目前的逻辑时钟。再根据上述判断规则rules judging来选举leader ,然后再将自身最新的选举结果(也就是上面提到的三种数据(leader Serverid,Zxid,Epoch)广播给其他server)。2)其次,判断服务器是不是已经收集到了所有服务器的选举状态:若是,根据选举结果设置自己的角色(FOLLOWING还是LEADER),退出选举过程就是了。
最后,若没有收到没有收集到所有服务器的选举状态:也可以判断一下根据以上过程之后最新的选举leader是不是得到了超过半数以上服务器的支持,如果是,那么尝试在200ms内接收一下数据,如果没有新的数据到来,说明大家都已经默认了这个结果,同样也设置角色退出选举过程。
- 如果所接收服务器A处在其它状态(FOLLOWING或者LEADING)。
a)逻辑时钟Epoch等于目前的逻辑时钟,将该数据保存到recvset。此时Server已经处于LEADING状态,说明此时这个server已经投票选出结果。若此时这个接收服务器宣称自己是leader, 那么将判断是不是有半数以上的服务器选举它,如果是则设置选举状态退出选举过程。
b) 否则这是一条与当前逻辑时钟不符合的消息,那么说明在另一个选举过程中已经有了选举结果,于是将该选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,如果可以也是保存逻辑时钟,设置选举状态,退出选举过程。
zk分布式锁原理
[toc]
Q: 如果同时希望加锁的客户端非常多,有一千多个,同时监听一个节点取锁的话会有什么问题#
A:
会发生羊群效应。
即节点被删除释放锁时, 需要借助watch机制通知所有客户端, 消耗大量资源
- 如果有1000个客户端watch 一个znode的exists调用,当这个节点被创建的时候,将会有1000个通知被发送。这种由于一个被watch的znode变化,导致大量的通知需要被发送,将会导致在这个通知期间的其他操作提交的延迟
Q: 那么如何避免监听过多?#
A:
znode不止一个, 但是会存在链式关系的锁,有点像是公平排队锁,先到先得。
lock之间会按顺序做监听, 而其他的客户端则单独监听自己需要的那个lock即可。
比如我们按照上述逻辑创建了有三个znodes.
/lock/lock-001,/lock/lock-002,/lock/lock-003.
/lock/lock-001 的这个客户端获得了lock
/lock/lock-002 的客户端watch /lock/lock-001
/lock/lock-003 的客户端watch /lock/lock-002
通过这种方式,每个节点只watch 一个变化
-
核心逻辑:不需要关心所有的事件,判断自己是否是所有节点中序号最小的。于是,很容易可以联想的到的是,每个节点的创建者只需要关注比自己序号小的那个节点。
redis主从复制
- Q: redis的主从复制主要是解决什么问题的?
- Q: 全量复制的全过程?
- Q: 全量复制后,就不再复制了,对嘛?
- Q: 那redis的增量复制又是解决什么问题的?
- Q: 主节点怎么实现的增量复制?
- Q: 主节点不做持久化的话,会有什么问题?
- Q: 为什么全量复制时,要用RDB而不是AOF?
- Q: 如果我的redis磁盘性能比较差,做主从全量复制时,每次从磁盘加载RDB的开销很大怎么办?
- Q: 从库太多了, 主库复制的压力很大怎么办?
- Q: redis主库和从库继续读操作时,数据可能会不一致, 但我的服务对一致性要求很高,不允许被人同时看到2个不一样的数据,怎么办?
- Q: 全量复制时,从节点读取到了过期的数据怎么办?
[toc]
Q: redis的主从复制主要是解决什么问题的?#
A:
-
故障恢复(主节点挂了, 启动从节点作为主节点)
-
负载均衡(读写分离, 主库写+读, 从库读)
-
高可用(哨兵和集群都依赖主从复制)
redis的主从复制 包含全量复制和增量复制
Q: 全量复制的全过程?#
A:
- 阶段1:连接建立
-
从库调用"replicaof/slaveof ip port"命令,将对应节点设置为自己的主节点。(即调用这个命令的是从库)
-
从库给主库发送 psync 请求
psync请求中包含runId(redis唯一id)和复制进度offset
当第一次复制,offset设置为-1.
-
主库收到 psync 命令后,如果是第一次复制,则会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset。
fullResync告知对方我会给你全量的数据。
- 阶段2:数据复制
- 主库调用bgsave命令,生成RDB文件, 然后把RDB文件发给从库。
- 从库收到RDB文件后,会先清空自己库内的所有数据,然后再根据RDB进行加载。
- 阶段3:持续同步
-
第4步主库生成完RDB文件后, 会生成一个 replication buffer,专门记录这之后收到的所有写命令。
-
当RDB文件发送完成后, 主库会把replication buffer中的写命令转发给备redis。
Q: 全量复制后,就不再复制了,对嘛?#
A: 错, 阶段3会持续同步, 一直存在的一个过程。
注意, 这个阶段3的过程不叫“增量复制”!
Q: 那redis的增量复制又是解决什么问题的?#
A:
专门用于解决刚才阶段3中的持续同步时,发生网络短时间断开,以至于主从不一致的问题。
当网络重新建立后, 从节点不希望全量复制,因此会借助增量复制机制,让主节点把特定offset后的写数据发给自己。(之前的psync中的offset就是用在这里的)
Q: 主节点怎么实现的增量复制?#
A:
主节点自身有一个repl_backlog_buffer, 写命令都会缓存到这里,每个命令都有个offset。
这个buffer是一个环形链表,因此有长度限制,过于老的写命令会被清理掉。
当从节点重新用psync连上,并告知自己的offset时, 主节点会检查一下,看看offset在repl_backlog_buffer中是否存在
- 如果存在,说明来得及,于是从offset处发送写命令给从节点。
- 如果offset不存在,说明时间太久了,offset到现在的数据之间已经有很多丢失了,需要重新全量复制了
Q: 主节点不做持久化的话,会有什么问题?#
A:
当主节点重启时,从节点正好继续增量复制,然后都继续了空的全量复制,并清空了原先的数据。
Q: 为什么全量复制时,要用RDB而不是AOF?#
A:
1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大。
\2. RDB文件直接加载key, 而AOF需要处理写命令。
\3. RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
Q: 如果我的redis磁盘性能比较差,做主从全量复制时,每次从磁盘加载RDB的开销很大怎么办?#
A:
使用repl-diskless-sync配置开启无磁盘复制
master创建一个新进程直接dump RDB到slave的socket,不经过主进程,不经过硬盘。适用于disk较慢,并且网络较快的时候。
Q: 从库太多了, 主库复制的压力很大怎么办?#
A: 改成主——从——从的模式。
即从库2把从库1认做主库, 从库3把从库2认作主库。
Q: redis主库和从库继续读操作时,数据可能会不一致, 但我的服务对一致性要求很高,不允许被人同时看到2个不一样的数据,怎么办?#
A:
(1)业务可以接受,系统不优化
(2)强制读主,高可用主库,用缓存提高读性能
(3)在cache里记录哪些记录发生过写请求,来路由读主还是读从
- 优化主从节点之间的网络环境, 将延时控制在不可感知的时间范围内
- 监控主从的当前offset或者网络延时,如果发现误差过大,则停止从从节点读,使用延时更低的集群扩展进行读写负载扩容。
- 从节点数据正在做同步时,不允许响应客户端命令,避免不一致。
Q: 全量复制时,从节点读取到了过期的数据怎么办?#
A:
Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。
redis备份和持久化
[toc]
redis持久化是做什么的?就是为了做备份, 避免redis重启后要重新从数据库里拿缓存。
redis当前有两种持久化机制: RDB和AOC
RDB(Redis DataBase) redis快照#
Q: RDB有哪两种触发方式?#
A:
-
手动触发。 调用save或者bgsave命令进行快照和备份。
也可以配置redis.conf,做一个周期性快照,但本质上是使用上面这2个命令
-
自动触发。m秒内有n次修改,或者主从复制时会触发。
Q: save命令和bgsave命令有什么区别?#
A:
-
save命令会阻塞当前redis线程,导致缓存暂时不可用(stopwolrd), 只能在开发环境或者测试环境使用。
-
bgsave 会fork创建一个子进程,在子进程中做RDB操作。
RDB快照存盘完成后,会通知主进程,主进程更新统计信息。
Q: 如果一次执行好几个bgsave命令会怎么样?#
A:
当检测到有子进程时, 后面几个bgsave命令会直接返回,不会做创建。
Q: bgsave的过程中,如果主进程收到新的写数据怎么办?#
A:
主进程会把写数据生成一个副本,交给bgsave子进程, 子进程会把这个修改作为最新修改写入RDB文件。
Q: 这个同步副本给子进程的时候进程崩溃怎么办?导致没有落盘到RDB#
A:
RDB完全写入成功,才算成功。
写一半的RDB文件,被认为无效文件,因此会用上一次的有效RDB文件进行恢复。
Q: 每秒做一次bgsave会有什么问题?如果数据量比较大的话#
A:
- 磁盘压力比较大,频繁竞争磁盘通过(注意每次快照的文件都是不同的文件,但磁盘写入器会共用)
- 创建fork子线程的操作,本身会阻塞一定的主进程时间。
Q: 如何解决上面的问题, 又能尽可能保证备份的实时可靠性?#
A:
做增量快照(只备份新增的修改),而增量快照的话RDB无法支持。
Q: RDB的优缺点?#
A:
优点:
-
使用了LZF算法做了压缩,数据量小于内存大小,很适合备份、全量复制。
-
备份恢复速度比AOF快。
缺点:
-
效率不高,如果数据量太大的话。
-
fork子进程操作比较重
-
文件属于二进制文件,可读性差。
AOF(append only file)持久化方式#
Q: AOF采用 ”先更新内存缓存, 再写备份日志“, 和”mysql日志先行“的思想不同,为什么?#
A:
-
错误命令得调用redisobject方法的时候才能感知,写日志时不方便做检查。
如果写了错误命令的日志,再执行命令,就不好恢复了(那mysql为什么没有这个顾虑?)
-
尽快加速写操作的响应
风险就是正巧写的时候挂了,那日志也丢了。
Q: AOF记录时的3个步骤是怎样的?#
A:
-
命令追加
把写命令写道aof_buf缓冲区
-
文件写入和同步
什么时候写入文件,有3种策略
- always:每次写入缓冲区后,都马上写入文件
- everySec:每秒把缓冲区里的数据写一次到文件(可能丢失1秒)
- no:由操作系统控制合何时写文件
Q: 随着写入操作越来越多, AOF文件会越来越大,怎么办?#
A:
当大到一定成都时, redis会创建一个新的AOF文件, 对老文件里的命令进行重写,去除一些冗余命令,只保留每个键的最后状态。
重写操作也是fork了一个子线程进行重写
Q: 重写时,由新的数据写入怎么办?#
A:
同步2份写操作, 老AOF文件写一份, 新AOF文件的缓冲区同步一份。
-
主线程fork出子进程重写aof日志
-
子进程重写日志完成后,主线程追加aof日志缓冲
-
替换日志文件
Q: 如果重写的时候 redis重启了怎么办?#
A:
没写完的AOF文件默认失效, 使用老的AOF文件。
Q: 主进程如何拷贝数据给子进程?#
A:
使用操作系统的copy on write
子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存
一般都采用RDB和AOF混合的方式, 先全量快照, 然后一段时间内增量AOF。
Q: 恢复时, 如果同时有RDB文件和AOF文件,怎么选择?#
A:
优先选择AOF文件,AOF不存在再RDB。
Q: 那么为什么会优先加载AOF呢?#
A:
因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。
Q: 线上高压环境如何进行备份机制的调优,减少阻塞?#
A:
- 关闭非敏感、重要性数据的备份
- 通过一些其他补数据机制进行恢复,例如自己写一些缓存重新载入的补数据功能。
- redis集群实现滚动备份,避免全部同时在备份。
- redis主从复制时, 让从节点做备份,主节点不做部分。
redis网络IO模型和事件触发原理
[toc]
Q: redis的高吞吐率具体指的是什么?#
A:
在网络 IO 操作中能并发处理大量的客户端请求,并建立很多连接,却不会那么那么容易阻塞
Q: redis所说的单线程指的是哪些部分?哪些部分不是单线程?#
A:
单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程
需要额外线程执行的:
- 持久化
- 异步删除
- 集群数据同步
Q: 讲一下一次 Redis 客户端与服务器进行连接并且发送命令的过程?#
A:
-
客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。
-
进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。
-
客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。
-
执行命令获得相应的命令回复。
为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。
当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中。
Q: 上面的过程中, 如果没有IO多路复用,会有什么问题?#
A:
服务端将会有一个线程一直阻塞等待客户端的命令以及回复读取。占用CPU资源。
Q: 那改成IO多路复用后又是怎么优化的?#
A:
调用 epoll 机制,让内核监听这些套接字。
此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。
正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。
Q: redis的多路复用机制只有epoll吗?#
A:
-
Redis 基于的底层 I/O 多路复用库有多套。包括select、epoll、evport和kqueue等。
-
每个IO多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae_select.c,ae_epoll.c, ae_kqueue.c等。
-
Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。事件响应框架一般都采用该架构,比如 netty 和 libevent。
Q: redis有哪些时间事件?#
A:
- 定时事件:让一段程序在指定的时间之后执行一次,执行完就不再执行了。
- 周期性事件:让一段程序每隔指定时间就执行一次。