赞
踩
前面我们学习了SpringBoot整合Kafka进行编码实战,本篇文章我们来研究一下Producer 发送消息 和 Consumer 消费消息的底层执行原理,让大家对Kafka理解得更加深入。
kafka依赖ZooKeeper负责维护整个Kafka集群的状态,存储Kafka各个节点的信息及状态,实现Kafka集群的高可用,协调Kafka的工作内容。工作流程如下:
这里对图中的概念再做一下解释,虽然前面章节已经有解释过了
整体工作流程如下:
注意:这里的Producer并不需要根据ZooKeeper来获取集群状态,而是在配置中指定多个Broker节点进行发送消息,同时跟指定的Broker建立连接,来从该Broker中获取集群的状态信息,这时Producer可以知道集群中有多少个Broker是否在存活状态,每个Broker上的Topic有多少个Partition。然后把消息发送给分配的partition中
我们要探讨的是消息生产者也就是KafkaProducer是如何把消息发送到Kafka Brocker中的,要知道KafkaProducer就是把用户待发送的消息封装成ProducerRecord
对象,它由五个字段组成(topic,partition分区, key消息的key,value消息体,timestamp 消息时间戳)
,然后使用 KafkaProducer#send 方法进行发送,详细流程如下
消息的发送过程被分为两个不同的线程:主线程和Sender I/0线程
KafkaProducer拿到消息首先会进入拦截器,通过拦截器可以实现对消 息的扩展
随后对消息进行序列化,Kafka有自己的虚拟化器,不使用Java自带的序列化是因为Java的序列化后数据太过臃肿,性能较差。
接着通过Partitioner分区器会根据一定的路由策略确定Topic下的某个分区。
然后把消息写入消息缓冲区(RecordAccumuator)。producer 创建时会创建 个默认 32MB (由 buffer.memory 参数指定〉的 accumulator 缓冲区,专门保存待发送的消息。消息是分批次发送的,发往不同分区的消息保存在对应分区下的 batch 队列中
KafkaProducer中还有 个专门的 Sender 线程负责将缓冲区中的消息分批次(batch)发送给 Kafka broker,消息是分批(batch)发送的性能更好。Sender发送数据有几个条件
batch.size : 批次大小,消息累计到batch.size(默认16k)大小之后sender才会发送
linger.ms :延迟发送时间,如果消息并没有达到batch.size 但是等待时间达到了linger.ms,数据也会被发送。linger.ms默认 0 毫秒。
需要特别说明的是:kafka 在设计底层网络库时采用了 Java Selector 机制(NIO),实现高效的网络传输。
Brocker收到消息之后做出应答(acks),应答模式有: 0 ,1 ,-1/all
0 :不用等到KafkaBrocker的leader和follower副本是否成功存储消息到磁盘
1 :Kafka Brocker 的Leader 副本收到producer 发送的消息写入本地日志,然后便发送响应结果给producer ,而无须等待其他lollowrer副本写入该消息
-1(或者all) : Leader broker 不仅会将消息写入本地日志,同时还会等待所有其他followrer副本都成功写入它们各自的本地日志后,才发送响应结果给,消息安全但是吞吐量会比较低
如果发送成功,这正常返回结果,否则会进行重试,重试次数是Interger的最大值,可以修改 spring.kafka.producer.retries
。
Kafka服务器会返回一个RecordMetadata(元数据)给客户端,内容包含:offset 消息的位置 ;timestamp消息的时间戳 ;topic/partition 消息分区 ;serializedKeySize :序列化后的消息 key字节数 ;serializedValueSize :序列化后的消息 value 字节数
producer 拦截器 interceptor是 个相当新的功能,它和 consumer interceptor 是在 Kafka 0.10.0.0 版本中被引入的,主要用于实现 clients 端的定制化控制逻辑,对于 producer 而言,interceptor 使得用户在消息发送前以及 producer 回调逻辑前有机会对消息做些定制化需求,比如修改消息等,同时 producer 允许用户指定多个 interceptor 按序作用于同条消息从而形成 个拦截链( interceptor chain ),拦截器的接口为.Producerlnterceptor,实现该接口即可定义自己的拦截器。
在网络中发送数据都是以字节的方式, Kafka 也不例外。 Kafka支持用户给 broker发送各种类型的消息。它可以是一个字符串、一个整数、一个数组或是其他任意的对象类型。序列化器( serializer )负责在 producer 发送前将消息转换成字节数组:而与之相反,解序列化器( deserializer )则用于将 consumer 接收到的字节数组转换成相应的对象。
Kafka 1.0.0 默认提供了十几种序列化器,其中常用的 serializer 如下
当然用户也可以自定义序列化器。
Kafka的一个topic由多个partition组成, partition就是分区,这些partition分散在不同brocker中,以实现请求负载的目的提高吞吐量。producer 提供了分区策略以及对应的分区器 partitioner )供用户使用 Kafka 发布的默认 partitioner 会尽力确保具有相同 key 的所有消息都会被发送到相同的分区上
第一篇文章就介绍过, Kafka 是分布式的消息引擎集群环境,它支持自动化的服务发现与成员管理。那么它是如何做到的呢? 学过SpringCloud的同学应该很容易理解 ,Kafka 是依赖 Apache ZooKeeper 实现的 。每当一个broker 启动时(一个brocker就是一个kafka服务器),它会将自己注册到 ZooKeeper(注册:主机,端口,启动时间等,以JSON格式保存),Zookeeper注册中心会形成一个Brocker的注册信息表:
我这里用来一个ZooView工具查看了一下zookeeper中的注册信息
Kafka 的paritition 分区就是为了数据备份,利用多份相同的数据备份(冗余机制)来保持系统高可用性,这些备份在 Kafka 中被称为副本( replica) Kafka 把分区的所有副本均匀地分配到所有 broker 上,并从这些副本中挑选一个作为 leader副本对外提供服,而 他副本被称为 follower 副本,只能被动地向 leader 副本请求数据,从而保持与 leader副本的同步。
如果Leader不宕机follower就没有任何用处,当leader宕机,那么Kafka会从当前leader的多个follower副本中选举一个新的leader副本。
所谓 ISR ,就是 Kafka 集群动态维护的一组同步副本集合( in-sync replicas) 每个 topic分区都有自己的 ISR 列表, ISR 中的所有副本都与 leader 保持同步状态。值得注意的是, leader副本总是包含在 ISR 中的,只有 ISR 中的副本才有资格被选举为 leader 。 producer 写入的Kafka 消息只有被 ISR 中的所有副本都接收到,才被视为“己提交”状态 由此可见,若ISR 中有n 个副本,那么该分区最多可以忍受 n-1 个副本崩溃而不丢失己提交消息。
需要注意的是:follower副本只是做一个事情,就是向leader副本请求数据,然后进行备份。
这里有三个概念
Leader 负责维护和跟踪 ISR 集合中所有 Follower 副本的滞后状态,当 Follower 出现滞后太多或者失效时,Leader 将会把它从 ISR 集合中剔除。
当然,如果 OSR 集合中有 Follower 同步范围追上了 Leader,那么 Leader 也会把它从 OSR 集合中转移至 ISR 集合。
一般情况下,当 Leader 发送故障或失效时,只有 ISR 集合中的 Follower 才有资格被选举为新的 Leader,而 OSR 集合中的 Follower 则没有这个机会(不过可以修改参数配置来改变)。
在一个 Kafka 集群中, broker 服务器着机或崩溃是不可避免的。 旦发生这种情况,该 broker 上的那些 leader 副本将变为不可用,因此就必然要求 Kafka 把这些分区的 leader 移到其他的 broker 上。即使崩溃 broker 重启回来,其上的副本也只能作为 follower 副本加入 ISR中,不能再对外提供服务。
Kafka的Leader选举采用首选副本(preferred replica) ,假设我们为一个分区分配了3个副本,它们分别是 0、 1 、2 。那么节点0就是该分区的preferred replica ,并且通常情况下是不会发生变更的。选择节点0的原因仅仅是它是副本列表中的第一个副本。
Kafka brocker收到Producer发送过来的消息后需要把消息存储到日志文件中。注意这里的日志和传统的 error,debug 日志有明显的区别,这里的日志指的是是kafka发送的消息内容。从某种意义上来说Kafka 日志的设计更像是关系型数据库中的记录。kafka会不这些消息的内容按顺序在日志尾部追加。顺序读写是Kafka高性能的很重要一个原因。
创建 topic 时, Kafka 为该 topic 的每个分区在文件系统中创建了一个对应的子目录,名字
就是<topic>-<分区号>。所以,倘若有一个 topic 名为 test ,有两个分区,那么在文件系统中Kafka 会创建两个子目录: test-0 test-1 。
Kafka broker 处理请求的模式就是 Reactor 设计模式,Reactor 设计模式是一种事件处理模式,旨在处理多个输入源同时发送过来的请求, Reactor 模式中的服务处理器( service handler )或分发器( dispatcher )将入站请求( inbound request )按照多路复用的方式分发到对应的请求处理器(request handler )中进行处理。
Reactor模型更多的内容大家可以看我这篇文章《Netty和Reactor线程模型》
生产者把消息发送到Kafka,消息存储在指定Topic的指定Partition中,消费者从ZK中拿到注册信息找到对应的Brocker,从对应partition的leader 读取消息。
需要注意的是:kafka中一个消费者可以从多个partition中消费消息,但是一个partition中的消息只能由一个Consumer Group(groupId相同的会形成一个组)中的一个消费者消费。因为一个Consumer Group 中的多个消费者消费一个分区中的数据可能会造成消息重复消费问题。
那么消费者从Kafka中消费消息,它怎么知道消费到哪个位置,下一次应该从哪儿开始消费?在Topic中维护了一个offset 位移来控制当前消息的消费位置,在老版本的Kafka中offset是交由Zookeeper来维护,新版本交由brocker自己来维护,这样的好处是减少了和Zookeeper的交互次数。
消息的消费有pull主动拉取和push推送两种方式消费消息。kafka采用pull主动拉取方式。因为每个消费者的消费进度不一样,消费者主动向Kafka拉取消息的方式更加合理。
拥有相同groupId的消费者会被加入到同一个消费者组。Kafka的每个Brocker都有一个Coordinator协调器来辅助消费。
消费者通过算法hashcode(groupId) % consumer_offsets分区数量
得到一个分区下标值来选择一个分区,然后根据分区所在的brocker选择所在的coordinator。
这个时候所有的consumer都会向该coordinator发请求申请加入该Group。
接着Coordinator会从Consumer中选出Leader(老大)然后把topic信息发给Leader Consumer
Leader Consumer会制定消费方案(比如轮询),然后把方案发送给Coordinator,再由Coordinator把消费方法发给所有的consumer
另外:每个Consumer和Coordinator维持心跳(3s),超时会移除Consumer(45s),触发Rebalance,如果消费者超时(5m),也会触发Rebalance
消费者通过 ConsumerNetworkClient和Kafka做交互,每批次次抓取1字节,可以通过fetch.mini.bytes设置。当fetch.max.wait.ms时间超时不管数据有没有1字节都会拉取。可以通过fetch.max.bytes设置一批次最大拉取字节数。
拉取到的数据通过onSuccess回调把数据存储到队列中(CompletedFetchs),消费者从队列中获取消息,每次获取500条。可以通过Max.poll.records配置。拉取到的消息内容会进行 反序列化,然后经过消费者的拦截器,最后交由处理程序对数据进行处理。
文章到这里就结束了,本文讲了从生产者到Kafka到消费者的整个执行原理和相关概念。认证看完之后相信你对Kafka已经有了深刻的认识。最近也是比较忙,一篇文章写了好几周,写到一半就有其他事情耽误了,今天才写完。最后~~~~ 喜欢的话给个好评。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。