Kafka为了保证数据不丢失, 对topic进行了分区备份, 然后通过在ISR中选举Leader来保证Fail-Over的实现, 本节会稍微长一点.
- 官网文档的说明
- PacificA论文的说明
1. Kafka的Topic是如何做备份的
Kafka中一个逻辑上的Topic
实际上是分布在多个服务器上的Partition, 这些Parition中有一个是Leader Partition, 以及众多跟随Leader
的备份, 可以称之为Follower来实现Fail-Over
其它的消息队列系统一般都提供默认的备份功能, 让一个不活跃的slaver去跟随主队列的数据, 整个设计非常的重. 在Kafka中没有这样的复制备份的概念, 所有的Topic都是有replication的, 只不过有一些topic的replication数是0
在Kafka的设计中Leader Partition
会直接接收来自Producer
端写入的数据, 而Follower Partition
会作为一个Consumer
去Leader这里读取数据, 并写入到自己的本地的文件中. 这样的设计下, 部分Follower
可能会落后Leader
, 有一个坏处是的时间窗口让Leader
和Follower
们的数据不一致. 这种设计的一个好处是Follower
的拉取逻辑比较简单, 而且是批量拉取的实现让吞吐比较高.
一个ISR
是active的, 也就是可用, 需要满足两个条件:
- 它所在的node(broker)需要能在zookeeper里维护心跳
- 作为一个备份, 它和leader之间的数据差异不能太大(可配置的一个threhold)
满足以上条件的Follower被称之为 "in sync", 而不是"alive" or "failed". Leader会持续跟踪这些"in sync""的Follower. 如果一个Follower不满足以上两点条件了, Leader就会把它从Follower的列表中剔除.
NOTE: replica.lag.time.max.ms 控制落后threahold的配置项
在传统的分布式系统中, 会使用各种方法来解决Fail-Over问题, 俗称拜占庭问题. Kafka在设计中并不能解决这个问题, Kafka不会假设一个Node会恶意发布虚假信息
拜占庭问题: https://en.wikipedia.org/wiki/Byzantine_fault_tolerance
在Kafka中, 一条信息被commit意味着它被存放在了这个Topic关联的所有的"in sync replicas"中. 只有commit的消息可以被Consumer端拉取到. 这样Consumer可以不用担心它读取的信息会丢失, 它和其它的Consumer读取的内容可能不一致.
Producers, 可以通过一个配置项来标记自己是否等待commit状态完成.
在Procuer的架构中可以看到Response有三种不同的触发条件
通过设置最小的ISR接受次数, 可以控制Kafka返回Response到Producer的行为, 这个最小次数可以设置成0, 意味着数据仅仅被保存在Leader上就认为结束了. 一般来说只要有一个ISR或者, 就认为这个数据没有丢失, 可以执行Kafka自己的Fail-Over策略.
Kafka可以在一个Node失效后成功的fail-over, 但无法在网络错误(分区错误)后保持可用性.
2. 基于日志的Replication实现的抉择
基于Replicate Log(日志复制策略)的分布式系统备份的实现, 是非常传统的策略. 只要整个系统是基于state machine(状态机) 实现的, 就可以使用这种策略来做HA, 大家都知道的就是MySQL的HA策略, 从Master上不断的把Bin-Log复制到Standby上去, Standby基于Log执行每一个在Master上已经执行过的操作来保证Standby的机器和Master的一致性
Note: State Machine: https://en.wikipedia.org/wiki/State_machine_replication 维基百科里还介绍了多个Standby时的选择Paxos
Replicate Log模型要求所有的日志必须按照顺序被发送到standby的节点上去, 最简单的策略就是在Leader节点不死的情况下, 由Leader节点来整理需要发送的Log的顺序.
在Kafka里实际上就是当ISR去Leader拉数据的时候, Leader保证发送给所有的ISR的message都是同样的顺序即可.
当Leader节点不行挂掉了, 我们就需要去从所有的Follower里面选举一个出来做新的Leader. 在选举过程中, Follower可能也会挂掉, 甚至Follower上的数据可能不全. 参考上一章介绍的ISR的两个约束, 在Commit前需要的最小ISR数越多, 当Leader挂掉时可以参与选举的节点就越多, 但是吞吐率就越低. 用户需要在可用性和吞吐之间做平衡
经典策略是多数选择. 当我们维护2f+1个备份时, 每次commit前强制必须至少有f+1个备份机接收到消息, 然后当这此commit完成后leader挂掉的时候, 我们从所有的follower中选一个拥有消息数最多的点做leader. 这个策略保证了, 只要挂掉的follower不超过f个, 那么至少应该有一个点, 拥有所有的消息. 这个就是个简单的抽屉原理的实现.
选举算法的好处是: 延迟Latency被比较快的节点决定, 这样没有异构集群下慢机器的问题.
坏处也很明显, 为了实现3个稳定备份需要7个Follower和7次网络传输, 代价非常的高. 所以一般这种策略用来实现元数据的存储, 像Zookpeer, HDFS NameNode的元数据备份等. 而数据, 分布在DataNode上的HFile是无法使用这种策略的.
HDFS NameNode的备份策略: http://blog.cloudera.com/blog/2012/10/quorum-based-journaling-in-cdh4-1
Kafka 使用一个工程化版的Quorum. 像前文说的Kafka会维护一个in-sync replicas (ISR) 来对Leader进行备份, 这些ISR的信息是被写到Zookeeper中的. 只有足够数量的ISR接收到message才算这条message被commit, 所以也就只有ISR可以参与选举过程. Kafka相当于使用一定的端到端延迟为代价换取只需要维护f+1个备份来保证数据的可用性. end-to-end latency 的trade off体现在可能某个follower就是慢, 导致commit比较慢, 进而导致consumer就是读不到这条信息
如果用户觉得特别慢, 用户可以直接通过配置项来动态决定commit的策略. Keep It Simple and Stupid. 认为系统的使用者是充分理解自己业务状态的, 这是Apache的几个组件的共同特色
3. ISR全部挂掉时的抉择
起码有以下两个选择
- 等待某个ISR恢复, 它可能只是掉线了, 过一会IT部的哥们说不定把它连上呢?
- 选举一个没有那么全消息的follower成为leader
unclean.leader.election.enable
为True时选择第二个策略, 恢复回来的数据是unclean的, 为False时使用第一个策略, 等待开发人员恢复一个备份. 默认是False
4. 可用性与持久性的抉择
Procuder端在写的时候可以选择: 生产者被通知写入成功, 是否这条message被拉取到 0个, 至少1个, 或者所有(all) 的 Follower之后才发生.
这里的通知"acknowledged"不代表这个数据就被持久化了. 当Producer端设置acks=all, 当所有的Follower拉取message后就会返回成功, 但它们拉取消息不代表它们成功写入本地, 甚至拉取过程本身都因为网络波动失败掉.
这里生产者得知写入成功, 和这条消息达到commit状态之间也没有必然关系. 比如一个生产者写入一个Replication = 2
, ISR mini size = 1
的topic, 两个follower尝试从leader拉取数据时, producer这里会异步得到一个成功的回执. 然而很可能其中一个在拉取过程中失败了, 这个message的真实备份数只有1份!
-
unclean leader election
, 是等待一个全信息的ISR恢复, 还是直接使用脏数据让业务接续, 并承受可能的数据损失. -
minimum ISR size
. 当Producer端设置 ack=all时需要的最小ISR完成数, 这个设置的越大, 延迟就越高, 吞吐也可能会降低. 设置的越小, 丢失数据的风险就越大
5. Replication的管理
决定什么时候进恢复流程, 以及选举算法在哪里跑也很重要. Kafka会从所有的Broker里面选一个Controller, 这个Controller会周期性检测Broker级别的错误, 通过zookeeper心跳. 当发现某个Broker不可用时, 就需要对它上面的Leader Partition执行选举算法过程来决定新的partition.
同样的它也需要维护失败节点上的ISR等配置信息, 这样单点在大规模集群里运行速度比较快, 过程比较容易理解. 而复杂的配置同步过程, 状态和心跳维持过程剥离到Zookeeper去做, 整个套件里只需要维护Zab
这样一个复杂的同步策略就OK了, Kafka自身不用去关注完全拜占庭问题.
6. PacificA - Kafka官方文档的"灵感来源"
本节很多内容和图片来自知乎专栏 @分布式和存储的那些事, 在原文基础上说明了PacificA 的设计映射到Kafka上是什么
6.1 设计策略
这是微软的分布式存储框架, 是Kafka整个备份设计的灵感来源
- 设计了一个通用和抽象的复制框架,模型容易验证正确性,实现不同的策略实例. 对应Kafka中的ISR复制策略
- 配置管理和数据复制分离,用著名的Paxos负责管理配置和副本信息. 实际上在Kafka里是用Zookeeper来保证的, Zookeeper用的算法是Zab
- 将错误检测和配置更新容在数据复制的交互里面,去中心化,降低瓶颈
6.2 系统框架
存储集群:对应Kafka中的Broker, 实现数据的存储, 同时数据会再存储节点之间复制 . 大数据实际上是被切分成partition, 分布式的存在多个broker上的.
配置管理集群:对应Kafka在Zookeeper里维护的配置信息, 包括ISR的状态, Leader的状态, Broker的状态等.
6.3 数据复制策略
pacificA也是用Leader和Follower来实现一份数据存放在多地的, 如果Leader挂掉了, 就从Follower里选一个出来做新的Leader
- 更新记录进入主副本节点处理,为该记录分配Sn(Serial Number),然后将该记录插入prepare list,该list上的记录按照sn有序排列;
这里对应前面提到的当Follower去拉取Leader信息时, Leader有责任保证写过去的消息对所有的Follower的顺序是一致的. - 主副本节点将携带sn的记录发往从节点,从节点同样将该记录插入到prepare list;
这里对应Follower们把数据刷到本地的segement里 - 一旦主节点收到所有从节点的响应,确定该记录已经被正确写入所有的从节点,那就将commit list向前移动,并将这些消息应用到主节点的状态机;
主节点提交后即可给客户端返回响应,同时向所有从节点发送消息,告诉从节点可以提交刚刚写入的记录了。
这里对应全面提到的commit策略
这个设计和Kafka一样, Consumer读数据时, 总是从Leader Partition读, 不会从Follower上读. 这是Kafka和HDFS的一个显著差异, HDFS是允许从Replication的DataNode上读数据的, Kafka不行
6.4 配置管理服务
前面的pacificA架构中我们提到该框架一个很重要的组成部分是配置管理服务。该服务维护所有复制组信息. 也就是ISR的信息
在系统运行过程中可能有以下几种情形导致复制组信息发生变更:
- 复制组从节点离线;
- 复制组主节点离线;
- 复制组增加新节点
对每种情况处理方法:
- 从节点离线,主节点能检测到该事件(检测方法下面部分说明),此时主节点向配置管理服务汇报最新的拓扑,注意此时的拓扑中已经不包含离线的从节点;
- 主节点离线,从节点能检测到该事件(检测方法下面部分说明),此时从节点向配置管理服务汇报最新的拓扑,注意此时拓扑中已经不包含离线的主节点,同时从节点会将自己设置为主节点
- 复制组增加新节点,可能是因为原来离线的节点又重新上线了,此时主节点向配置管理服务汇报最新的拓扑,注意此时的拓扑中加上该新增节点
6.5 节点异常检测
在Kafka里这是由Controller和Zookeeper一起来完成的, 在微软的设计里不是,这是整篇论文看下来最大的差异
系统运行过程中难免出现节点宕机离线等诸多异常。如何保证在节点异常情况下协议的正常运行是分布式系统设计中的关键问题。pacificA是一种强一致协议,通过主节点来维护多副本数据一致性。
pacificA使用了lease机制来保证不会产生脑裂问题。主节点与从节点维护心跳信息:Leader定期发送心跳,只要Follower响应该心跳,Leader就确定自己还是Leader。对于以下两种可能的异常:
- 如果Leader在一定时间内(lease period)未收到Follower对心跳的回应,它就需要向配置维护集群汇报这个信息, 并发起一轮Leader选举. 这个过程在Kafka里是不存在的.
- 如果Follower在一定时间内(grace period)未收到Leader的心跳信息,那么其认为Leader异常,于是向配置管理服务汇报回信复制集拓扑,将Leader从复制集中移除,同时将自己提升为新的Leader。
只要lease period
<= grace peroid
,就不会产生出现两个Leader问题,因为:
- 假如主从之间出现网络分区。只要lease period <= grace peroid,主必然先检测到从没有回复的事件,于是他开始停止作为主服务,并且通知配置管理服务;
- 由于lease period <= grace peroid,那么从节点检测到主节点异常则是在主节点停止作为主以后,此时该副本集是不存在主的,因此,必然不会产生二主现象。
6.6 节点异常恢复
上面是Primary和Secondary-2两个节点之间出现了网络异常。接下来,配置管理服务器可能会收到三个节点发起的拓扑重配置请求M1,M2和M3。
M1:主节点DATA NODE 2将自己降级为从,同时向配置管理服务发起移除dn3的请求,我的理解是,如果配置管理服务批准了该请求,那么该节点重新成为主;
M2:从节点DATA NODE 3可能只是与主节点出现网络分区,进程还在,此时从节点DATA NODE 3向配置管理服务发起Change Primary请求并将主节点DATA NODE 2从复制集中移除;
M3:因为主节点DATA NODE 2放弃了主节点的身份,因此,可能导致DATA NODE 1在心跳周期内也无法获得心跳,于是它也有可能发起一次Change Primary请求,将主节点dn2移除,将自己提升为新的主。
而配置管理服务器会根据请求的先后顺序来处理,对于上述M1、M2、M3请求,只有一个会得到批准,假如M1得到批准,那么新的结构就变为:
此时DATA NODE 3便被从复制集中剔除了。当然,如果DATA NODE 3进程依然存活的话,它接下来还是可以向配置管理服务申请将自己加入复制组的。
在确定新的复制组拓扑后,还要经历一个reconciliation过程。该过程的主要目的是保持从节点和主节点的数据一致性,主要过程是主节点将自己的prepare list上的操作记录全部提交至状态机并将自己的prepare list记录同步至从。对于从节点来说,有可能其prepare list落后于主节点,这时候需要补齐,有可能其prepare list领先于从节点,这时候需要截断。无论如何,由于满足prepare list和commit list的定式关系,因此不会出现已经提交的记录被回滚的情况。