阅读《深入理解Kafka核心设计与实践原理》第六章 深入服务端
深入服务端
1. 时间轮
Kafka中存在大量的延时操作,比如延时生产、延时拉取和延时删而是基于时间轮的概念自定义实现了一个用于延时功能的定时器除等。
为什么不用java自带的延时器?
因为时间复杂度不能满足Kafka的要求,在Netty,Zookeeper,Akka都有时间轮的存在。
时间轮介绍:
https://zhuanlan.zhihu.com/p/121483218
Kafka时间轮的底层就是一个环形数组,Kafka中一个时间轮TimingWheel是由20个时间格组成,wheelSize = 20;每格的时间跨度是1ms,tickMs = 1ms。参照Kafka,上图中也用了20个灰边小圆表示时间格,为了动画演示可以看得清楚,我们这里每个小圆的时间跨度是1s。
所以现在整个时间轮的时间跨度就是 tickMs * wheelSize ,也就是 20s。从0s到19s,我们都分别有一个灰边小圆来承载。
随着时间推进, 时间轮的指针循环往复地定格在每一个时间格上, 每一次都要判断当前定格的时间格里是不是有任务存在;
其中有很多时间格都是没有任务的, 指针定格在这种空的时间格中, 就是一次"空推进";
比如说, 插入一个延时时间400s的任务, 指针就要执行399次"空推进", 这是一种浪费!
那么Kafka是怎么解决这个问题的呢?
Kafka中的定时器借了JDK中的延迟队列DelayQueue来协助推进时间轮。
kafka会按照超时时间来将任务的任务排序,最短的排在前面,通过这个超时时间来推进时间轮的时间。
Kafka 中的 TimingWheel 专门用来执行插入和删除 TimerTaskEntry的操作,而 DelayQueue 专门负责时间推进的任务。
延时生产:
案例:由于客户端设置了acks为-1,那么需要等到follower1和follower2两个副本都收到消息3和消息4后才能告知客户端正确地接收了所发送的消息。
在将消息写入 leader 副本的本地日志文件之后,Kafka会创建一个延时的生产操作(DelayedProduce),用来处理消息正常写入所有副本或超时的情况,以返回相应的响应结果给客户端。
延时拉取
案例:两个follower副本都已经拉取到了leader副本的最新位置,此时又向leader副本发送拉取请求,而leader副本并没有新的消息写入,那么此时leader副本该如何处理呢?
Kafka选择了延时操作来处理这种情况。Kafka在处理拉取请求时,会先读取一次日志文件,如果收集不到足够多的消息,那么就会创建一个延时拉取操作以等待拉取到足够数量的消息。当延时拉取操作执行时,会再读取一次日志文件,然后将拉取结果返回给 follower 副本。
kafka 还有事务机制?
默认的事务隔离级别为“read_uncommitted”。读未提交
注意:follower副本不可以将事务隔离级别修改为read_committed(读已提交),这样消费者拉取不到生产者已经写入却尚未提交的消息。
2. 控制器
在 Kafka 集群中会有一个或多个 broker,其中有一个 broker会被选举为控制器(Kafka Controller)
Kafka Controller有什么作用?
它负责管理整个集群中所有分区和副本的状态。
当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责分区的重新分配。
Kafka Controller 如何选举出来?
Kafka中的控制器选举工作依赖于ZooKeeper,成功竞选为控制器的broker会在ZooKeeper中创建**/controller**这个临时(EPHEMERAL)节点.
其中version在目前版本中固定为1,brokerid表示成为控制器的broker的id编号,timestamp表示竞选成为控制器时的时间戳。
每个 broker 启动的时候会去尝试读取/controller节点的brokerid的值,如果读取到brokerid的值不为-1,则表示已经有其他 broker 节点成功竞选为控制器,所以当前 broker 就会放弃竞选;
如果ZooKeeper 中不存在/controller节点,或者这个节点中的数据异常,那么就会尝试去创建/controller节点。
当前broker去创建节点的时候,也有可能其他broker同时去尝试创建这个节点,只有创建成功的那个broker才会成为控制器,而创建失败的broker竞选失败。每个broker都会在内存中保存当前控制器的brokerid值,这个值可以标识为activeControllerId。
ZooKeeper 中还有一个与控制器有关的/controller_epoch 节点,这个节点是持久(PERSISTENT)节点,节点中存放的是一个整型的controller_epoch值。controller_epoch用于记录控制器发生变更的次数,即记录当前的控制器是第几代控制器,我们也可以称之为“控制器的纪元”。
由此可见,Kafka 通过controller_epoch 来保证控制器的唯一性,进而保证相关操作的一致性。
具备控制器的身份的broker的职责有?
- 监听分区相关的变化
- 监听主题相关的变化
- 从ZooKeeper中读取获取当前所有与主题、分区及broker有关的信息并进行相应的管理
- 启动并管理分区状态机和副本状态机
- 更新集群的元数据信息
早期的Kafka版本并没有broker Controller 这一个角色,来对分区和副本的状态进行管理, 每个broker都会在zookeeper上注册大量的监听器(Watcher),当分区和副本发生变化时, 会唤醒很多不必要的监听器, 而且会使Zookeeper的负载过大。
新版只有Kafka Controller在ZooKeeper上注册相应的监听器,其他的broker极少需要再监听ZooKeeper中的数据变化。
不过每个broker还是会对/controller节点添加监听器,以此来监听此节点的数据变化(ControllerChangeHandler)。
3. 关闭kafka
配合kill-9的方式来快速关闭Kafka broker的服务进程,显然kill-9这种“强杀”的方式并不够优雅,它并不会等待Kafka 进程合理关闭一些资源及保存一些运行数据之后再实施关闭动作。
使用 kill-s TERM $PIDS 或 kill-15 $PIDS 的方式来关闭进程,注意千万不要使用kill-9的方式。
这样做的好处?
Kafka 服务入口程序中有一个名为“kafka-shutdown-hock”的关闭钩子,待 Kafka 进程捕获终止信号的时候会执行这个关闭钩子中的内容。
一是可以让消息完全同步到磁盘上,在服务下次重新上线时不需要进行日志的恢复操作;二是ControllerShutdown 在关闭服务之前,会对其上的leader副本进行迁移,这样就可以减少分区的不可用时间。
4.分区leader的选举
分区leader副本的选举由控制器负责具体实施。
当新建一个主题,或者新增分区,重分区,都会重新选举leader。
默认的策略的基本思路是按照 AR 集合中副本的顺序查找第一个存活的副本,并且这个副本在ISR集合中。
5. 参数解密
broker.id是broker在启动之前必须设定的参数之一。broker 在启动时会在 ZooKeeper 中的/brokers/ids路径下创建一个以当前brokerId为名称的虚节点,broker的健康状态检查就依赖于此虚节点。