Kafka是由Apache软件基金会开发的一个开源流平台,由Scala和Java编写。Kafka的Apache官网是这样介绍Kakfa的。
Apache Kafka是一个分布式流平台。一个分布式的流平台应该包含3点关键的能力:
1.发布和订阅流数据流,类似于消息队列或者是企业消息传递系统。
2.以容错的持久化方式存储数据流。
3.处理数据流。
前面我们了解到,消息队列中间件有很多,为什么我们要选择Kafka?
高性能: 单一的Kafka代理可以处理成千上万的客户端,每秒处理数兆字节的读写操作,Kafka性能远超过传统的ActiveMQ、RabbitMQ等,而且Kafka支持Batch操作;
可扩展:kafka集群可以透明的扩展,增加新的服务器进集群
容错性: kafka每个partition数据会复制到几台服务器,当某个broker失效时,zookeeper将通知生产者和消费者从而使用其他的broker;
push:优势在于消息实时性高。劣势在于没有考虑consumer消费能力和饱和情况,容易导致producer压垮consumer。
pull:优势在可以控制消费速度和消费数量,保证consumer不会出现饱和。劣势在于当没有数据,会出现空轮询,消耗cpu。
在Kafka中,每个topic都可以配置多个分区以及多个副本。在创建topic时,Kafka会将每个分区的leader均匀地分配在每个broker上。如果leadder出现故障,follower就会被选举为leader。
AR: 分区的所有副本。
ISR:所有与leader副本保持一定程度同步的副本(包括 leader 副本在内)。
OSR:follower副本同步滞后过多的副本(不包括 leader 副本)组成 「OSR」(Out-of-Sync Replias)
AR = ISR + OSR。正常情况下,所有的follower副本都应该与leader副本保持同步,即AR=ISR,OSR集合为空。
为什么不能通过ZK的方式来选举partition的leader?
1、Kafka集群如果业务很多的情况下,会有很多的partition。
2、假设某个broker宕机,就会出现很多的partition都需要重新选举
3、如果使用zookeeper选举leader,会给zookeeper带来巨大压力,所以,kafka中leader的选举不能使用ZK来实现。
kafka采用拉取模型,由消费者自己记录消费状态,每个消费者互相独立地顺序拉取每个分区的消息。消费者可以按照任意的顺序消费消息。比如,消费者可以重置到旧的偏移量,重新处理之前已经消费过的消息;或者直接跳到最近的位置,从当前的时刻开始消费。
1、每个consumer都可以根据分配策略(默认RangeAssignor),获得要消费的分区。
2、获取到consumer对应的offset(默认从ZK中获取上一次消费的offset)
3、找到该分区的leader,拉取数据
4、消费者提交offset
在kafka创建主题,主题名为“test_10m”,副本数为2,分区数为3,并配置日志文件最大为10M。
bin/kafka-topics.sh --create --zookeeper node1 --topic test_10m --replication-factor 2 --partitions 3 --config segment.bytes=10485760
node1:
node2:
node3:
如上图所示,当我们创建一个主题后,kafka_2.12-2.4.1/data/目录下会生成以主题+分区号命名的文件夹。
对于一个partition(在Broker中以文件夹的形式存在),里面又有很多大小相等的segment数据文件(这个文件的具体大小可以在config/server.properties中进行设置),这种特性可以方便old segment file的快速删除。
下面先介绍一下partition中的segment file的组成:
segment file 组成:由2部分组成,分别为index file和data file,这两个文件是一一对应的,后缀".index"和".log"分别表示索引文件和数据文件;
segment file 命名规则:partition的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset,ofsset的数值最大为64位(long类型),20位数字字符长度,没有数字用0填充。如下图所示:
生产者生产消息大于10M后,日志保存到新的文件中。下图中143999=143998+1为新的log文件的起始偏移量,也是上一个log中的消息数量。
继续往kafka写数据,数据总是写入到最后的日志文件中。关于segment file中index与data file对应关系图,如下图所示:
通过offset查找message,如图所示,加入我们想要读取offset=368776的message,通过一下两步:
1、查找segment file
00000000000000000000.index表示最开始的文件,起始偏移量(offset)为0.第二个文件00000000000000368769.index的消息量起始偏移量为368770 = 368769 + 1.同样,第三个文件00000000000000737337.index的起始偏移量为737338=737337 + 1,其他后续文件依次类推,以起始偏移量命名并排序这些文件,只要根据offset 二分查找文件列表,就可以快速定位到具体文件。当offset=368776时定位到00000000000000368769.index|log
2、通过segment file查找message
通过第一步定位到segment file,当offset=368776时,用该offset减去368769 = 7,折半查找索引文件,发现索引文件中没有编号为7的,那就获取它前面存在的编号,编号为6,对应的物理地址为1407,然后从此位置顺序遍历后面的消息,顺序查找直到offset=368776为止。
segment index file并没有为数据文件中的每条message建立索引,而是采取稀疏索引存储方式,每隔一定字节的数据建立一条索引,它减少了索引文件大小,通过map可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
在Kafka中,消息是会被定期清理的。一次删除一个segment段的日志文件
Kafka的日志管理器,会根据Kafka的配置,来决定哪些文件可以被删除。
(1)如果是kafka消费能力不足,则可以考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者=分区数(二者缺一不可)
(2)如果下游的数据处理不及时,提高每批次拉取的数量。批次拉取数量过少(拉取数据/处理时间<生产速度),是处理的数据小于生产的数据,也会造成数据积压。
注:先查清楚导致数据积压的原因再解决问题。
在生产者生产消息时,如果出现retry时,有可能会一条消息被发送了多次,如果Kafka不具备幂等性的,就有可能会在partition中保存多条一模一样的消息。
为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)和 Sequence Number的概念。
PID: 每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的。
Sequence Number: 针对每个生产者(对应PID)发送到指定主题分区的消息都对应一个从0开始递增的Sequence Number。
当生产者没有收到确认相应ACK时,会重新发送消息,在保存消息时会判断当前的seq是否小于pid对用的seq,如果小于或等于,则不保存。
Kafka事务指的是生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。
默认的策略,也是使用最多的策略,可以最大限度保证所有消息平均分配到一个分区
如果在生产消息时,key为null,则使用轮询算法均衡地分配分区。
按key分配策略,有可能会出现「数据倾斜」,例如:某个key包含了大量的数据,因为key值一样,所有所有的数据将都分配到一个分区中,造成该分区的消息数量远大于其他的分区。
Kafka中的Rebalance称之为再均衡,是Kafka中确保Consumer group下所有的consumer如何达成一致,分配订阅的topic的每个分区的机制。
Rebalance触发的时机有:
1、消费者组中consumer的个数发生变化。例如:有新的consumer加入到消费者组,或者是某个consumer停止了。
2、订阅的topic个数发生变化。消费者可以订阅多个主题,假设当前的消费者组订阅了三个主题,但有一个主题突然被删除了,此时也需要发生再均衡。
Range范围分配策略是Kafka默认的分配策略,它可以确保每个消费者消费的分区数量是均衡的。
注意:Rangle范围分配策略是针对每个Topic的。
RoundRobinAssignor轮询策略是将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序(topic和分区的hashcode进行排序),然后通过轮询方式逐个将分区以此分配给每个消费者。
从Kafka 0.11.x开始,引入此类分配策略。主要目的:
1.分区分配尽可能均匀
2.在发生rebalance的时候,分区的分配尽可能与上一次分配保持相同。
没有发生rebalance时,Striky粘性分配策略和RoundRobin分配策略类似。
上面如果consumer2崩溃了,此时需要进行rebalance。
Striky粘性分配策略,保留rebalance之前的分配结果。这样,只是将原先consumer2负责的两个分区再均匀分配给consumer0、consumer1。这样可以明显减少系统资源的浪费,例如:之前consumer0、consumer1之前正在消费某几个分区,但由于rebalance发生,导致consumer0、consumer1需要重新消费之前正在处理的分区,导致不必要的系统开销。
三种方式通俗概括,分区分配就像打扑克牌,每张扑克牌都是一个partition,每个人是一个消费者,发牌(不考虑最后3张底牌)。
range范围分配策略:先连续发17张给A,然后发17张给B,最后发17张给C。如果C不玩了,需要重新分配,需先发26张给A,再发25张给B。
RoundRobin轮序策略:就是最传统的发牌策略,A,B,C,A,B,C…直到发完。如果C不玩了,需要重新分配,则发牌顺序为A,B,A,B…A,B,直到发完
Stricky粘性分配策略:先是最传统的发牌策略,A,B,C,A,B,C…直到发完。如果C不玩了,则A,B的牌不动,将C的牌依次发给A,B,直到发完。
对副本关系较大的就是,producer配置的acks参数了,acks参数表示当生产者生产消息的时候,写入到副本的要求严格程度。它决定了生产者如何在性能和可靠性之间做取舍。
● 高级API写起来简单
● 不需要去自行去管理offset,系统通过zookeeper自行管理
● 不需要管理分区,副本等情况,系统自动管理
● 消费者断线会自动根据上一次记录在 zookeeper中的offset去接着获取数据(默认设置5s更新一下 zookeeper 中存的的offset),版本为0.10.2
● 可以使用group来区分对访问同一个topic的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)
● 不能自行控制 offset(对于某些特殊需求来说)
● 不能细化控制如分区、副本、zk 等
● 能够开发者自己控制offset,想从哪里读取就从哪里读取。
● 自行控制连接分区,对分区自定义进行负载均衡
● 对 zookeeper 的依赖性降低(如:offset 不一定非要靠 zk 存储,自行存储offset 即可,比如存在文件或者内存中)
● 太过复杂,需要自行控制 offset,连接哪个分区,找到分区 leader 等
如果Kafka中的消息超过指定的阈值,就会将日志进行自动清理
日志删除任务会检查当前日志的大小是否超过设定的阈值来寻找可删除的日志分段的文件集合。可以通过broker端参数 log.retention.bytes 来配置,默认值为-1,表示无穷大。如果超过该大小,会自动将超出部分删除。
每个segment日志都有它的起始偏移量,如果起始偏移量小于 logStartOffset,那么这些日志文件将会标记为删除。
Log Compaction是默认的日志删除之外的清理过时数据的方式。它会将相同的key对应的数据只保留一个版本。
序列化器: 生产者需要用序列化把对象转换成字节数组才能通过网络发送给kafka。而在对侧,消费者需要用反序列化器(Deserializer)把从 Kafka 中收到的字节数组转换成相应的对象。
分区器: 分区器的作用就是为消息分配分区。如果消息 ProducerRecord 中没有指定 partition 字段,那么就需要依赖分区器,根据 key 这个字段来计算 partition 的值。
kafka一共有两种拦截器: 生产者拦截器和消费者拦截器。
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
消费者拦截器主要在消费到消息或在提交消费位移时进行一些定制化的操作。
处理顺序: 拦截器->序列化器->分区器
整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线程)。
在主线程中由 KafkaProducer 创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。
Sender 线程负责从 RecordAccumulator 中获取消息并将其发送到 Kafka 中。
RecordAccumulator 主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输的资源消耗以提升性能。
不可以。不可能越过zookeeper直接联系kafka broker,一旦zookeeper停止工作,它就不能响应客户端请求。
zookeeper主要用于集群中不同节点之间进行通信,在kafka中,它被用于提交偏移量,因此如果节点在任何情况下都失败了。它都可以从之前提交的偏移量中获取,除此之外,它还执行其他活动,如:leader检测、分布式同步、配置管理、识别新节点何时离开、连接、实时状态等。
两个消费者负责同一个分区,那么就意味着两个消费者同时读取分区的消息,由于消费者自己可以控制读取消息的offset,就有可能C1才读到2,而C1读到1,C1还没处理完,C2已经读到3了,则会造成很多浪费,因为这就相当于多线程读取同一个消息,会造成消息处理的重复,且不能保证消息的顺序。
重复消费:
1、当ack=-1时,如果在follower同步完成后,broker发送ack之前,leader发生故障,导致没有返回ack给Producer,由于失败重试机制,又会给新选举出来的leader发送数据,造成数据重复。
2、(手动管理offset时,先消费后提交offset)消费者消费后没有commit offset(程序崩溃/强行kill/消费耗时/自动提交偏移情况下unscrible)
漏消费:
1、手动管理offset时,先提交offset后消费)如果先提交offset,后消费,可能会出现数据漏消费问题。比如,要消费0,1,2,我先提交offset ,此时__consumer_offsets的值为4,但等我提交完offset之后,还没有消费之前,消费者挂掉了,这时等消费者重新活过来后,读取的__consumer_offsets值为4,就会从4开始消费,导致消息0,1,2出现漏消费问题。
2、当ack=0时,producer不等待broker的ack,这一操作提供了一个最低的延迟,broker还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。
3、当ack=1时,producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,而由于已经返回了ack,系统默认新选举的leader已经有了数据,从而不会进行失败重试,那么将会丢失数据。
kafka特点:高性能、可扩展、容错性。分别从kafka的特点进行阐述,其中可拓展性无需赘述,分布式集群特点。
页缓存技术:为了保证数据写入性能, Kafka 基于操作系统的页缓存来实现文件写入的。仅这一个步骤,就可以将磁盘文件写性能提升很多了,因为其实这里相当于是在写内存,不是在写磁盘。
磁盘顺序写:仅仅将数据追加到文件的末尾,不是在文件的随机位置来修改数据。
通过零拷贝技术,就不需要把 OS Cache 里的数据拷贝到应用缓存,再从应用缓存拷贝到 Socket 缓存了,两次拷贝都省略了,所以叫做零拷贝。对 Socket 缓存仅仅就是拷贝数据的描述符过去,然后数据就直接从 OS Cache 中发送到网卡上去了,这个过程大大的提升了数据消费时读取文件数据的性能。
在从磁盘读数据的时候,会先看看 OS Cache 内存中是否有,如果有的话,其实读数据都是直接读内存的。如果 Kafka 集群经过良好的调优,大家会发现大量的数据都是直接写入 OS Cache 中,然后读数据的时候也是从 OS Cache 中读。相当于是 Kafka 完全基于内存提供数据的写和读了,所以这个整体性能会极其的高。
(1)broker数据不丢失
生产者通过分区的leader写入数据后,所有在ISR中follower都会从leader中复制数据,这样,可以确保即使leader崩溃了,其他的follower的数据仍然是可用的。
(2)生产者数据不丢失
生产者连接leader写入数据时,可以通过ACK机制来确保数据已经成功写入。ACK机制有三个可选配置:
1.配置ACK响应要求为 -1 时 —— 表示所有的节点都收到数据(leader和follower都接收到数据)。
2.配置ACK响应要求为 1 时 —— 表示leader收到数据(可能产生数据丢失)
3.配置ACK影响要求为 0 时 —— 生产者只负责发送数据,不关心数据是否丢失(这种情况可能会产生数据丢失,但性能是最好的)。
生产者可以采用同步和异步两种方式发送数据
同步:发送一批数据给kafka后,等待kafka返回结果
异步:发送一批数据给kafka,只是提供一个回调函数。
(3)消费者数据不丢失
在消费者消费数据的时候,只要每个消费者记录好offset值即可,就能保证数据不丢失。
更多kafka面试问题详见:Kafka相关面试题详解