kafka全面理解
什么是消息队列,它的好处是什么?
解藕
将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而上游系统不需要做任何修改
例如有上游系统a,它有3个下游系统b,c,d,为了使b,c,d能拿到a的数据,a需要在代码中去调用这3个系统。如果有一天,b不再使用a的数据了或b的接口发生了变化,a还需要修改代码。而使用消息队列,就只管往队列里发送数据,需要的下游自己去队列里取数据即可。
异步
不用同步等待下游将数据处理完,将消息发到消息队列中即可返回,不阻碍主流程。
例如上游系统a是主业务,b,c,d是非主要业务,没有必要同步等待3个下游都返回主业务才继续。使用消息队列可以实现异步,提高吞吐量。
削峰
上游数据有突发流量,下游可能扛不住,kafka在中间可以起到一个缓冲的作用,把消息暂存在kafka中,下游服务就可以按照自己的节奏慢慢处理。
kafka概念
broker
broker是kafka实例。
replication
每一个partition有多副本,当主节点发生故障时,会选择一个副本作为主节点。kafka是主写主读的。
topic
topic是消息的分类,一个topic可以供任意多个消费组消费。
partition
topic的分区,每个topic的数据可以被分成多个partition,可以不在一个机器上,由此来实现kafka的伸缩性。各个partition的数据是不重复的,相同partition的数据是按照发送顺序有序的。任何partition只有一个leader,只有leader是对外提供服务的。leader接收到数据后,follower会不停给他发送请求尝试去拉取最新的数据,拉取到自己本地后,写入磁盘中。每个partition都有多个副本,相同partition的各个副本分布在不同的broker上。
consumer group/consumer
一个consumer group是一个topic的订阅者,一个topic可以被多个consumer group订阅,各个consumer group是相互独立的。一个consumer group内部可以有多个consumer,多个consumer不会消费相同的partition的消息。最多有效的consumer数与partition数相同,如果consumer数多于partition数,那么多出来的consumer不会消费到任何消息。
rebalance
消费组内某个消费者挂掉后,其他消费者自动重新分配订阅topic的partition的过程。rebalance是消费者端实现高可用的重要手段。
kafka的特性
高吞吐、低延迟
:kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。高伸缩性
: 每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。持久性、可靠性
: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。容错性
: 允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作高并发
: 支持数千个客户端同时读写
kafka为何快
页缓存+顺序写入
kafka 写数据的时候,非常关键的一点,它是以磁盘顺序写的方式来写的。仅仅将数据追加到文件的末尾,不是在文件的随机位置来修改数据。
写入磁盘文件的时候,可以直接写入这个 OS Cache 里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把 OS Cache 里的数据真的刷入磁盘文件中。
零拷贝
如果Kafka从磁盘中读取数据发送给下游的消费者,大概过程是:
- 先看看要读的数据在不在os cache中,如果不在的话就从磁盘文件里读取数据后放入os cache
- 从操作系统的os cache 里拷贝数据到应用程序进程的缓存里
- 从应用程序进程的缓存里拷贝数据到操作系统层面的Socket缓存里
- 从Soket缓存里提取数据后发送到网卡,最后发送出去给下游消费者
整个过程有两次没必要的拷贝
- 从操作系统的cache里拷贝到应用进程的缓存里
- 从应用程序缓存里拷贝回操作系统的Socket缓存里。
为了进行这两次拷贝,中间还发生了好几次上下文切换,一会儿是应用程序在执行,一会儿上下文切换到操作系统来执行。
所以这种方式来读取数据是比较消耗性能的
零拷贝
- 让操作系统的cache中的数据发送到网卡
- 网卡传出给下游的消费者
中间跳过了两次拷贝数据的步骤,Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存
另外:
从磁盘读数据的时候,会先看看os cache内存中是否有,如果有的话,其实读数据都是直接读内存的。
如果kafka集群经过良好的调优,大家会发现大量的数据都是直接写入os cache中,然后读数据的时候也是从os cache中读。
相当于是Kafka完全基于内存提供数据的写和读了,所以这个整体性能会极其的高
消息压缩
批量发送
生产者
我们从创建一个ProducerRecord
对象开始,ProducerRecord 是 Kafka 中的一个核心类,它代表了一组 Kafka 需要发送的 key/value
键值对,它由记录要发送到的主题名称(Topic Name),可选的分区号(Partition Number)以及可选的键值对构成。
在发送 ProducerRecord 时,我们需要将键值对对象由序列化器转换为字节数组,这样它们才能够在网络上传输。然后消息到达了分区器。
如果发送过程中指定了有效的分区号,那么在发送记录时将使用该分区。如果发送过程中未指定分区,则将使用key 的 hash 函数映射指定一个分区。如果发送的过程中既没有分区号也没有,则将以循环的方式分配一个分区。选好分区后,生产者就知道向哪个主题和分区发送数据了。
ProducerRecord 还有关联的时间戳,如果用户没有提供时间戳,那么生产者将会在记录中使用当前的时间作为时间戳。Kafka 最终使用的时间戳取决于 topic 主题配置的时间戳类型。
- 如果将主题配置为使用
CreateTime
,则生产者记录中的时间戳将由 broker 使用。 - 如果将主题配置为使用
LogAppendTime
,则生产者记录中的时间戳在将消息添加到其日志中时,将由 broker 重写。
Kafka 对于数据的读写是以分区
为粒度的,分区可以分布在多个主机(Broker)中,这样每个节点能够实现独立的数据写入和读取,并且能够通过增加新的节点来增加 Kafka 集群的吞吐量,通过分区部署在多个 Broker 来实现负载均衡
的效果
Kafka Broker 在收到消息时会返回一个响应,如果写入成功,会返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量,上面两种的时间戳类型也会返回给用户。如果写入失败,会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败的话,就返回错误消息。
重要配置
acks
acks 参数指定了要有多少个分区副本接收消息,生产者才认为消息是写入成功的。此参数对消息丢失的影响较大
- 如果 acks = 0,就表示生产者也不知道自己产生的消息是否被服务器接收了,它才知道它写成功了。如果发送的途中产生了错误,生产者也不知道,它也比较懵逼,因为没有返回任何消息。这就类似于 UDP 的运输层协议,只管发,服务器接受不接受它也不关心。
- 如果 acks = 1,只要集群的 Leader 接收到消息,就会给生产者返回一条消息,告诉它写入成功。如果发送途中造成了网络异常或者 Leader 还没选举出来等其他情况导致消息写入失败,生产者会受到错误消息。
- 如果 acks = all,这种情况下是只有当所有参与复制的节点都收到消息时,生产者才会接收到一个来自服务器的消息。不过,它的延迟比 acks =1 时更高,因为我们要等待不只一个服务器节点接收消息。
消费者
向群组中增加消费者是横向伸缩消费能力的主要方式。总而言之,我们可以通过增加消费组的消费者来进行水平扩展提升消费能力
。这也是为什么建议创建主题时使用比较多的分区数,这样可以在消费负载高的情况下增加消费者来提升性能
rebalance
我们从上面的消费者演变图
中可以知道这么一个过程:最初是一个消费者订阅一个主题并消费其全部分区的消息,后来有一个消费者加入群组,随后又有更多的消费者加入群组,而新加入的消费者实例分摊
了最初消费者的部分消息,这种把分区的所有权通过一个消费者转到其他消费者的行为称为重平衡
,英文名也叫做 Rebalance
。如下图所示
消费者会向一个叫做 _consumer_offset
的特殊主题中发送消息,这个主题会保存每次所发送消息中的分区偏移量,这个主题的主要作用就是消费者触发重平衡后记录偏移使用的,消费者每次向这个主题发送消息,正常情况下不触发重平衡,这个主题是不起作用的,当触发重平衡后,消费者停止工作,每个消费者可能会分到对应的分区,这个主题就是让消费者能够继续处理消息所设置的。
如果提交的偏移量小于客户端最后一次处理的偏移量,那么位于两个偏移量之间的消息就会被重复处理
offset
Current Offset
Current Offset保存在Consumer客户端中,它表示Consumer希望收到的下一条消息的序号。它仅仅在poll()方法中使用。例如,Consumer第一次调用poll()方法后收到了20条消息,那么Current Offset就被设置为20。这样Consumer下一次调用poll()方法时,Kafka就知道应该从序号为21的消息开始读取。这样就能够保证每次Consumer poll消息时,都能够收到不重复的消息。
Committed Offset
Committed Offset保存在Broker上,它表示Consumer已经确认消费过的消息的序号。主要通过commitSync和commitAsync
API来操作。举个例子,Consumer通过poll() 方法收到20条消息后,此时Current Offset就是20,经过一系列的逻辑处理后,并没有调用consumer.commitAsync()或consumer.commitSync()来提交Committed Offset,那么此时Committed Offset依旧是0。
Committed Offset主要用于Consumer Rebalance。在Consumer Rebalance的过程中,一个partition被分配给了一个Consumer,那么这个Consumer该从什么位置开始消费消息呢?答案就是Committed Offset。另外,如果一个Consumer消费了5条消息(poll并且成功commitSync)之后宕机了,重新启动之后它仍然能够从第6条消息开始消费,因为Committed Offset已经被Kafka记录为5。
log end offset
记录底层日志 (log) 中的下一条消息的 offset, 对 producer 来说,就是即将插入下一条消息的 offset
reblance时机
- 有新的consumer加入
- 旧的consumer挂了
- coordinator挂了,集群选举出新的coordinator
- topic的partition新加
- consumer调用unsubscrible(),取消topic的订阅
ISR
ISR
ISR全称是“In-Sync Replicas”,也就是保持同步的副本,他的含义就是,跟Leader始终保持同步的Follower有哪些
如果说某个Follower所在的Broker因为JVM FullGC之类的问题,导致自己卡顿了,无法及时从Leader拉取同步数据,那么是不是会导致Follower的数据比Leader要落后很多
所以这个时候,就意味着Follower已经跟Leader不再处于同步的关系了。但是只要Follower一直及时从Leader同步数据,就可以保证他们是处于同步的关系的
所以每个Partition都有一个ISR,这个ISR里一定会有Leader自己,因为Leader肯定数据是最新的,然后就是那些跟Leader保持同步的Follower,也会在ISR里
如果leader crash时,ISR为空怎么办
kafka在Broker端提供了一个配置参数:unclean.leader.election,这个参数有两个值:
true(默认):允许不同步副本成为leader,由于不同步副本的消息较为滞后,此时成为leader,可能会出现消息不一致的情况。
false:不允许不同步副本成为leader,此时如果发生ISR列表为空,会一直等待旧leader恢复,降低了可用性
异常消费情况
消费丢失的情况:
auto.commit.enable=true,消费端自动提交offersets设置为true,当消费者拉到消息之后,还没有处理完 commit interval 提交间隔就到了,提交了offersets。这时consummer又挂了,重启后,从下一个offersets开始消费,之前的消息丢失了。
消费重复的情况
消费者A读取了数据,还没来得及消费,就挂掉了,还未commit。Zookeeper发现消费者A挂了,让消费者B去消费原本消费者A的分区
参考文章
https://juejin.im/post/684490...
https://juejin.im/post/684490...
https://zhuanlan.zhihu.com/p/...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。