1、kafka0.11.0.0版本之前,Kafka只能支持两种语义:At most once和At least once。Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证生产者发送的消息,不会丢失,而且不会重复。
要实现 exactly-once 在 Kafka 0.11.0 中有两个官方策略:
2、kafka key为空的时候消息发到哪个分区?
kafka定义了一个全局变量,这个变量值是配置参数中的topic.metadata.refresh.interval.ms设置的值,也就是说在这个时间内,key=null的消息都会往缓存起来的这个分区存储,当缓存过时之后,就会重新计算分区号,将计算结果缓存起来。也就是说在key为null的情况下,Kafka并不是每条消息都随机选择一个Partition;而是每隔topic.metadata.refresh.interval.ms才会随机选择一次!
3、kafka底层存储
Partition是以文件的形式存储在文件系统中,比如,创建了一个名为test的topic,其有5个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就会有这样5个目录: test-0,test-1…
partition还可以细分为segment,一个partition物理上由多个segment组成。
segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件
4、只要不更改group.id,每次重新消费kafka,都是从上次消费结束的地方继续开始,不论"auto.offset.reset”属性设置的是什么。因为kafka会维护每个group.id消费的偏移量。每次都会从上次消费结束的地方继续开始
5、Kafka保证exactly-once(一般只说producer到broker的exactly-once)
设置Kafka的ack级别为-1,可以保证producer到broker之间不会丢失数据,即at least once。
设置Kafka的ack级别为0,可以保证at most once
Kafka 0.11版本之前,只能通过at least once + 下游消费者全局去重保证exactly-onec。0.11版本后,引入了幂等性:无论producer向broker发送多少重复数据,broker只会持久化一条。所以幂等性利用了at least once,并且在broker端去重,完成exactly-once。
开启幂等性:设置produce的参数enabled.idempotence=true即可
幂等性原理:
开启后,ack就会自动设置为-1。然后每个 Producer 在初始化的时候都会被分配一个唯一的 PID,对于每个唯一的 PID,Producer 向指定的Topic 中某个特定的 Partition 发送的消息都会携带一个从 0 单调递增的 Sequence Number。broker会对
但是producer挂掉重启后,pid会发生变化。不同分区partition也不一样。所以幂等只能保证生产者单会话单分区的exactly-once。
为了实现跨分区跨会话的exactly-once,0.11版本后引入事务。一般我们只说produce端的事务,即我们的客户端代码会给一个全局唯一的transactionid,然后将pid和transactionID绑定。这样当produce重启后,transactionID不变,就可以根据transactionID去找挂掉之前的pid,那么当前启动的produce的pid就不会重新生成,还是原来的pid,那么有重复数据到来时,
上面只是事务的一个功能,即解决幂等性无法跨生产者会话exactlyonce的问题。除此之外,事务还有很多功能,如
Kafka事务代码示例
Properties props = new Properties();
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "ProducerTranscationnalExample");
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
//事务ID
props.put("transactional.id", "test-transactional");
KafkaProducer producer = new KafkaProducer(props);
//初始化一个事务
producer.initTransactions();
try {
String msg = "matt test";
//开启一个事务
producer.beginTransaction();
producer.send(new ProducerRecord(topic, "0", msg.toString()));
producer.send(new ProducerRecord(topic, "1", msg.toString()));
producer.send(new ProducerRecord(topic, "2", msg.toString()));
//提交事务
producer.commitTransaction();
} catch (ProducerFencedException e1) {
e1.printStackTrace();
producer.close();
} catch (KafkaException e2) {
e2.printStackTrace();
//中止事务
producer.abortTransaction();
}
producer.close();
当事务中仅仅存在Consumer消费消息的操作时,它和Consumer手动提交Offset并没有区别。因此单纯的消费消息并不是Kafka引入事务机制的原因,单纯的消费消息也没有必要存在于一个事务中。
7、kakfa有3个broker。创建一个topic,其中有3个partition,2个replication。那么分区和副本分配情况如下:
broker1{partition-0,partition-2} 》broker2 {partition-1,partition-0 }==》broker3{partition-2,partition-1}
创建副本的单位是topic的分区。
8、如何保证kafka全局消息有序?
Kafka只能保证一个分区之内消息的有序性,在不同的分区之间是不可以的,这已经可以满足大部分应用的需求。如果需要topic中所有消息的有序性,那就只能让这个topic只有一个分区,当然也就只有一个consumer组消费它。
9、kafka从0.9版本开始,就有两套消费者API,一个是老的API:需要提供ZK集群信息。一个是新的API:提供的是broker集群信息
10、消费者消费分区的策略
一个消费者组下,每个消费者消费kafka分区的策略:http://blog.csdn.net/wangqyoho/article/details/76169514
每个分区只能由同一个消费组内的一个consumer来消费。每个group中的consumer应该消费哪些分区,这就需要用特定的策略来进行分配。在 Kafka 内部存在两种默认的分区分配策略:Range 和 RoundRobin。两者的目的都是让每个消费者得到的分区数均衡
11、生产者写分区的策略
如果定义了key,那么会按照key进行分区:相同key的记录写到一个分区;
如果没有定义key,则按照轮询round-robin挨个均匀地写分区,即先写分区1一条记录,然后写分区2一条记录,然后写分区3一条记录。。。。
12、Kafka和rabbitmq的区别
RabbitMQ 内置重试逻辑和死信(dead-letter)交换器,但是 Kafka 只是把这些实现逻辑交给用户来处理。所以rabbitmq在可靠性方面做的更多更好,同时效率也会更低。
当消费者成功消费消息之后,RabbitMQ 就会把对应的消息从存储中删除。这种行为没法修改。相反,Kafka 会给每个主题配置超时时间,只要没有达到超时时间的消息都会保留下来。
在消息留存方面,Kafka 仅仅把它当做消息日志来看待,并不关心消费者的消费状态。消费者可以不限次数的消费每条消息,并且他们可以操作分区偏移来“及时”往返的处理这些消息。kafka 会周期的检查分区中消息的留存时间,一旦消息超过设定保留的时长,就会被删除。
业务一般用rmq,大数据用Kafka。
kafka和rabbitmq最大的区别就是在吞吐量上,kafka吞吐量几十万每秒,rabbitmq 几万每秒。kafka要做到全局有序,只能设置成单分区。rabbitmaq要做到全局有序,只能做成单线程生产,单线程消费,顺序性条件比较苛刻
13、Memory Mapped Files(MMAP)
kafka数据不是实时写入磁盘,而是利用操作系统分页存储,利用内存提高IO效率。通过mmap,进程像读写磁盘一样读写内存(虚拟机内存),也不必关心内存的大小(因为有虚拟内存兜底)。使用这种方式,省去了用户空间到内核空间复制的开销。
但这种方式有一个致命缺陷——不可靠,写入mmap的数据并没有真正写入磁盘。所以kafka提供了参数来控制producer是否主动flush到磁盘,写入mmap后立即flush,就是同步,写入mmap后立即返回producer而不flush,就是异步。
zero-copy
传统读取方式为,先复制到内核空间(pagecache),再复制到用户空间,从用户空间重新复制到内核空间(套接字缓冲区),然后发给网卡。
zero-copy直接从内核空间(DMA)到内核空间(socket),然后发给网卡。
14、kafka多线程消费
public class ConsumerRunnable implements Runnable {
// 每个线程维护私有的KafkaConsumer实例
private final KafkaConsumer<String, String> consumer;
public ConsumerRunnable(String brokerList, String groupId, String topic) {
Properties props = new Properties();
props.put("bootstrap.servers", brokerList);
props.put("group.id", groupId);
this.consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic)); // 本例使用分区副本自动分配策略
}
@Override
public void run() {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(200);
for (ConsumerRecord<String, String> record : records) {
// 这里面写处理消息的逻辑
}
}
}
}
public class ConsumerGroup {
private List<ConsumerRunnable> consumers;
public ConsumerGroup(int consumerNum, String groupId, String topic, String brokerList) {
consumers = new ArrayList<>(consumerNum);
//多个线程执行多个consumer,每个consumer负责一个或者多个分区
//如果这里consumerNum=1,那么只有一个线程,一个consumer,那么这个线程先去把分区1的数据消费完,再去消费分区2,再去分区3....
for (int i = 0; i < consumerNum; ++i) {
ConsumerRunnable consumerThread = new ConsumerRunnable(brokerList, groupId, topic);
consumers.add(consumerThread);
}
}
public void execute() {
for (ConsumerRunnable task : consumers) {
new Thread(task).start();
}
}
}
15、kafka单分区有序是指按照写入的顺序有序而不是是按照大小排序。比如依次往一个分区写10,5,11那存储的时候是存储10,5,11而不是是5,10,11
16、Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。kafka就是利用分段+索引的方式来解决查找效率的问题。
18、Kafka底层存储
数据文件的分段
Kafka解决查询效率的手段之一是将数据文件分段,比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。
为数据文件建索引
数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。
索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。
相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20 = 5。存储相对offset可以减小索引文件占用的空间。
position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。
index文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
20、kafka为什么快:
写入快的原因: 顺序写入 和 MMFile
读取快的原因:1、基于sendfile实现Zero copy 2、分区分段,以及分段文件建索引
22、kafka 0.10之后,虽然offset不存储在zk上了,但是kafka的安装,启动还是依赖zk。使用/kafka-topics.sh创建topic,查看topic列表,还是要指定zk。消费topic数据不需要指定zk
23、ack设置为1,写到页缓存就会通知客户端写入成功,而不是写到磁盘才通知
24、生产者数据丢失
25、Kafka 的核心源码分为两部分:客户端源码和服务端源码,客户端又分为生产者和消费者。
kafka生产者流程
1:一条消息过来首先会被封装成为一个ProducerRecord对象。
2:接下来要对这个对象进行序列化,因为Kafka的消息需要从客户端传到服务端,涉及到网络传输,所以需要实现序列。Kafka 提供了默认的序列化机制,也支持自定义序列化。
3:消息序列化完了以后,对消息要进行分区,分区的时候需要获取集群的元数据。
4:分好区的消息不是直接被发送到服务端,而是放入了生产者的一个缓存里面。在这个缓存里面,多条消息会被封装成为一个批次(batch),默认一个批次的大小是 16K。
5:Sender 线程启动以后会从缓存里面去获取可以发送的批次。
6:Sender 线程把一个一个批次发送到服务端
生产者高级设计之内存池设计
刚刚我们看到 batches 里面存储的是批次,批次默认的大小是 16K,整个缓存的大小是 32M,生产者每封装一个批次都需要去申请内存,正常情况下如果一个批次发送出去了以后,那么这 16K 的内存就等着 GC 来回收了。但是如果是这样的话,就可能会频繁的引发 FullGC,故而影响生产者的性能,所以在缓存里面设计了一个内存池(类似于我们平时用的数据库的连接池),一个 16K 的内存用完了以后,把数据清空,放入到内存池里,下个批次用的时候直接从里面获取就可以。这样大大的减少了 GC 的频率,保证了生产者的稳定和高效。
首先这个acks参数,是在KafkaProducer,也就是生产者客户端里设置的。然后这个参数实际上有三种常见的值可以设置,分别是:0、1 和 all。
第一种选择是把acks参数设置为0,意思就是我的KafkaProducer在客户端,只要把消息发送出去,不管那条数据有没有在哪怕Partition Leader上落到磁盘,我就不管他了,直接就认为这个消息发送成功了。
第二种选择是设置 acks = 1,意思就是说只要Partition Leader接收到消息而且写入本地磁盘了,就认为成功了,不管他其他的Follower有没有同步过去这条消息了。这种设置其实是kafka默认的设置
26、kafka多消费者
消费者组ConsumerGroup
27、Coordinator(协调者)介绍
Consumer端应用程序在提交offset时,其实是向Coordinator所在的Broker提交。同样地,当Consumer应用启动时,也是向Coordinator所在的Broker发送各种请求,然后由Coordinator负责执行消费者组的注册、成员管理记录等元数据管理操作。
每个Broker都有各自的Coordinator组件。Group Coordinator 是一个服务,每个Broker在启动的时候都会启动一个该服务。每个consumer group都会被分配一个这样的coordinator用于组管理和位移管理。
当新版本consumer group的第一个consumer启动的时候,它会去和kafka server确定谁是它们组的coordinator。之后该group内的所有成员都会和该coordinator进行协调通信。
Group Coordinator 的作用是用来存储 Group 的相关 Meta 信息,并将对应 Partition 的 Offset 信息记录到 Kafka 内置Topic(__consumer_offsets) 中。
Kafka在0.9之前是基于zk来存储 Partition的Offset信息,因为 Zookeeper 并不适用于频繁的写操作,所以在0.9之后通过内置Topic的方式来记录对应Partition的Offset。
如何进行分区和消费者的Rebalance?
Kafka提供的再平衡策略主要有三种:Range,Round Robin和Sticky,默认使用的是Range。
Range范围分区
Range 范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假如现在有 10 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6,7,8,9;消费者排序完之后将会是C1-0,C2-0,C3-0。通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
例如,10/3 = 3 余 1 ,除不尽,那么 消费者 C1-0 便会多消费 1 个分区,最终分区分配结果如下:
C1-0 消费 0,1,2,3 分区
C2-0 消费 4,5,6 分区
C3-0 消费 7,8,9 分区(如果有11 个分区的话,C1-0 将消费0,1,2,3 分区,C2-0 将消费4,5,6,7分区 C3-0 将消费 8,9,10 分区)
Range 范围分区的弊端:
如上,只是针对 1 个 topic 而言,C1-0消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,topic越多,C1-0 消费的分区会比其他消费者明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了
由于 Range 范围分区存在的弊端,于是有了RoundRobin 轮询分区策略
RoundRobinAssignor 轮询分区
RoundRobin 轮询分区策略,是把所有的 partition 和所有的 consumer 都列出来,然后按照 hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。
使用RoundRobin策略时,如果要达到分配均匀。必须满足条件:每个消费者订阅的主题必须相同(比如都订阅了topc1和topic2,而不是一个订了1一个订了2)。
如果不满足这个条件,RoundRobin也无法满足分配均匀
28、数据一致性
kafka的副本分为leader副本和follower副本;每个副本其实就是一个分区,其实也就是leader分区和follower分区。 kafka是leader副本负责读写,follower副本只负责备份。
而 zookeeper是 leader机器(进程)节点读写,follower机器(进程)节点负责读。