1、生产者
生产者发送消息流程
在消息发送的过程中,涉及到了两个线程 : main线程和Sender线程。在main线程中创建了一个双端队列RecordAccumulator。main线程将消息发送给 RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka Broker
Main线程
Producer(send ProducerRecord) -> Interceptors(拦截器) -> Serializer(序列化器) -> Paritioner(分区器),最终放入到 RecordAccumulator(默认32M),双端队列,最小单位是ProducerBatch 默认16K
Sender线程
NetworkClient 网络通信组件,Selector一旦有写事件,将通过 batch.size或linger.ms 来判断是否要发送。同时会在使用inFlightRequests,默认每个broker节点最多缓存5个请求
注意 : linger.ms生产环境是必须要设置的,默认是0,就是说会里面发送数据,肯定是不行的(吞吐量太小)。那linger.ms设置多大比较好呢?没有绝对值,只有相对值,相对谁?相对batch.size,batch.size默认是16kb,linger.ms指定时间段内,一定要大于16kb的数据
才有意义,不然每次触发的条件都是linger.ms了,batch.size默认是16kb就失去了意义
生产者参数解说
buffer.memory : RecordAccumulator缓冲区总大小,默认32m
batch.size : 16kb
linger.ms : 默认值0,表示没有延迟,立即发送。生产环境建议该值大小为 5-100ms 之间
acks :
0 : 生产者发送过来的数据,不需要等数据落盘应答
1 : 生产者发送过来的数据,Leader 收到数据后应答
-1(all) : 生产者发送过来的数据,Leader+和 isr 队列 里面的所有节点收齐数据后应答
默认值是-1,-1 和 all 是等价的
max.in.flight.requests.per.connection : 允许最多没有返回 ack 的次数,默认为 5,开启幂等性 要保证该值是 1-5 的数字
retries :
当消息发送出现错误的时候,系统会重发消息。retries 表示重试次数。默认是 int 最大值,2147483647。 如果设置了重试,还想保证消息的有序性,需要设置 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 否则在重试此失败消息的时候,其他的消息可能发送 成功了
retry.backoff.ms : 两次重试之间的时间间隔,默认是 100ms
enable.idempotence : 是否开启幂等性,默认 true,开启幂等性
compression.type :
生产者发送的所有数据的压缩方式。默认是none,也就是不压缩。支持压缩类型:none、gzip、snappy、lz4和zstd
数据幂等
Kafka 0.11版本以后,引入了一项重大特性 : 幂等性和事务
数据传递语义
至少一次(At Least Once) : ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
最多一次(At Most Once) : ACK级别设置为0
精确一次(Exactly Once) : 对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失
幂等
幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复
实现原理
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。那这两个概念的用途是什么呢?
- ProducerID : 在每个新的Producer初始化时,会被分配一个唯一的ProducerID,这个ProducerID对客户端使用者是不可见的
- SequenceNumber : 对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应一个从0开始单调递增的SequenceNumber值
数据乱序
- kafak在1.x版本之前保证数据单分区有序,条件如下 :
max.in.flight.requests.per.connection=1 (不需要考虑是否开启幂等性) - kafka在1.x及以后版本保证数据分区有序,条件如下 :
(1) 未开启幂等性
max.in.flight.requests.per.connection需要设置为1
(2) 开启幂等性
max.in.flight.requests.per.connection需要设置小于等于5
因为kafka1.x以后,启用幂等性后,kafka服务端会缓存producer发送来的最近5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的
幂等性的注意事项
- 幂等性Producer只能保证单分区上的幂等性 : 即只能保证某个主题上的一个分区上不出现重复消息,无法实现多个分区的幂等性(简单理解一下,就是比如说第一次你发送的是0号分区,但是ack失败了,重试发送,你往1号分区发送了,如果不指定key的情况下,是可以这么干的,这样就保证了不了不同分区的幂等性了)
- 幂等性Producer只能实现单会话上的幂等性,不能实现跨会话的幂等性(因为重启,producerId就变了)
配置幂等性
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
事务
幂等性并不能跨多个分区操作,事务是可以保证不同分区的事务的。Kafka事务指的是在 Exactly Once 语义的基础上,生产和消费可以跨分区和会话,生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。(consumer-transform-producer模式)
事务的应用场景
原子操作中,根据包含的操作类型,可以分为三种情况 :
- 只有Producer生产消息
- 消费消息和生产消息并存,这个是事务场景中最常用的情况,就是我们常说的“consume-transform-produce”模式
- 只有consumer消费消息
相关配置
- 消费者的自动模式设置为false,不在使用手动同步或者异步提交
- 生产者配置transaction.id属性,则此时enable.idempotence会被设置为true
- 消费者需要配置Isolation.level。在consume-trnasform-produce模式下使用事务时,必须设置为READ_COMMITTED
Producer事务原理
为了实现跨分区跨会话的事务,需要引入一个全局唯一的Transaction ID,并将Producer 获得的PID和Transaction ID绑定。这样当Producer重启后就可以通过正在进行的Transaction ID获得原来的PID
为了管理 Transaction,Kafka 引入了一个新的组件Transaction Coordinator(默认有50个分区,每个分区负责一部分事务,事务划分是根据transaction.id的hashcode%50,计算出该事务属于哪个分区该分区leader副本所在的borker节点即为transaction.id对应的Transactin Coordinator)。Producer就是通过和Transaction Coordinator交互获得Transaction ID对应的任务状态。Transaction Coordinator还负责将事务所有写入Kafka 的一个内部Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行
要使用事务生产者和attendant API,必须设置transactional.id。如果设置了transactional.id,幂等性会和幂等所依赖的生产者配置一起自动启用。此外,应该对包含在事务中的topic进行持久化性配置。特别是,replication.factor应该至少是3,而且这些topic的min.insync.replicas应该设置为2。最后,为了实现从端到端的事务性保证,消费者也必须配置为只读取已提交的消息
API
// 1 初始化事务
void initTransactions();
// 2 开启事务
void beginTransaction() throws ProducerFencedException;
// 3 在事务内提交已经消费的偏移量(主要用于消费者)
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
String consumerGroupId) throws
ProducerFencedException;
// 4 提交事务
void commitTransaction() throws ProducerFencedException;
// 5 放弃事务(类似于回滚事务的操作)
void abortTransaction() throws ProducerFencedException;
工具类
package com.journey.kafka;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import java.util.Properties;
public class KafkUtils {
/**
* 创建生产者
* @return
*/
public static Producer<String, String> createProducer() {
Properties properties = new Properties();
// 配置kafka broker节点列表
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"node:9092");
// 每个batch的大小,默认是16kb
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
// linger.ms ,其实就是client发送服务端发送时机就是batch到达了,或者linger.ms在accumuator中到时间了
properties.put(ProducerConfig.LINGER_MS_CONFIG,"1");
// RecordAccumulator,也就是client的一个缓冲池,默认32M
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
/**
* 该配置控制 KafkaProducer's send(),partitionsFor(),inittransaction (),sendOffsetsToTransaction(),commitTransaction() "
* 和abortTransaction()方法将阻塞。对于send(),此超时限制了获取元数据和分配缓冲区的总等待时间"
**/
properties.put(ProducerConfig.MAX_BLOCK_MS_CONFIG,"5000");
// 将消息发送到kafka server, 所以肯定需要用到序列化的操作 我们这里发送的消息是string类型的,所以使用string的序列化类
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
// 设置事务ID 如果配置了transactional.id属性,则enable.idempotence 会被设置为true.
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"my-transactional-id");
return new KafkaProducer<>(properties);
}
/**
* 创建消费者
* @return
*/
public static Consumer createConsumer() {
Properties properties = new Properties();
// 配置kafka broker节点列表
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"node:9092");
// 指定消费者组
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "trnasaction-consumer");
// 设置消费者事务隔离级别
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
// 设置offset不自动提交
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 设置session timeout
properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "30000");
// 设置序列化和反序列化器
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
return new KafkaConsumer<String, String>(properties);
}
}
Producer生产消息
package com.journey.kafka;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.OutOfOrderSequenceException;
import org.apache.kafka.common.errors.ProducerFencedException;
public class OnlyProduceInTransactionDemo {
public static void main(String[] args) throws Exception {
// 1.创建生产者
Producer<String, String> producer = KafkUtils.createProducer();
// 2.初始化事务
producer.initTransactions();
try {
// 3.开启事务
producer.beginTransaction();
// 4.业务逻辑
for (int i = 0 ; i < 10 ; i++) {
ProducerRecord<String, String> record = new ProducerRecord<>("transaction-topic", "key-" + i, "value-" + i);
/**
* 用户可以实现回调接口,允许代码再请求完成时执行。这个回调通常会在后台I/O线程中执行,所以它会很快
*/
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
System.out.println("partition : " + recordMetadata.partition() + " offset : " + recordMetadata.offset());
}
});
}
// 5.事务提交
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// 6.放弃事务
producer.abortTransaction();
} catch (KafkaException e) {
// 6.放弃事务
producer.abortTransaction();
} finally{
// 7.关闭连接
// 所有的通道打开都需要关闭 close方法会会将缓存队列状态置为关闭,唤醒io线程将内存中的数据发往broker,避免这个程序的进程突然挂掉,
// 然后内存里面的消息丢失,所以这个方法结束的时候,将消息数据都发送出去
producer.close();
}
}
}
只有consumer消费消息
package com.journey.kafka;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerGroupMetadata;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.OutOfOrderSequenceException;
import org.apache.kafka.common.errors.ProducerFencedException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class OnlyConsumeInTransactionDemo {
public static void main(String[] args) throws Exception {
// 1.构建生产者
Producer<String, String> producer = KafkUtils.createProducer();
// 2.初始化事务(生成produerId),对于一个生产者,只能执行一次初始化事务操作
producer.initTransactions();
// 3.构造消费者和订阅主题
Consumer consumer = KafkUtils.createConsumer();
consumer.subscribe(Arrays.asList("transaction-topic"));
while (true) {
// 4.开启事务
producer.beginTransaction();
// 5.接受消息
Duration duration = Duration.ofMillis(500);
ConsumerRecords<String, String> records = consumer.poll(duration);
try {
System.out.println("customer Message---");
Map<TopicPartition, OffsetAndMetadata> commits = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
// 5.2.1 处理消息 print the offset,key and value for the consumer records.
System.out.printf("offset = %d, key = %s, value = %s\n",
record.offset(), record.key(), record.value());
// 5.2.2 记录提交偏移量
commits.put(new TopicPartition(
record.topic(),
record.partition()),
new OffsetAndMetadata(record.offset()));
}
// 6.提交偏移量
producer.sendOffsetsToTransaction(commits, new ConsumerGroupMetadata("groupTxn"));
// 7.事务提交
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
producer.close();
} catch (KafkaException e) {
// 8.放弃事务
producer.abortTransaction();
}finally{
producer.flush();
}
}
}
}
消费消息和生产消息并存(consume-transform-produce)
package com.journey.kafka;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerGroupMetadata;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.AuthorizationException;
import org.apache.kafka.common.errors.OutOfOrderSequenceException;
import org.apache.kafka.common.errors.ProducerFencedException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class ConsumeTransferProduceDemo {
public static void main(String[] args) throws Exception {
// 1.构建上产者
Producer<String,String> producer = KafkUtils.createProducer();
// 2.初始化事务(生成productId),对于一个生产者,只能执行一次初始化事务操作
producer.initTransactions();
// 3.构建消费者和订阅主题
Consumer consumer = KafkUtils.createConsumer();
consumer.subscribe(Arrays.asList("trnasaction-topic"));
while (true) {
// 4.开启事务
producer.beginTransaction();
// 5.处理业务
Duration duration = Duration.ofMillis(5000);
ConsumerRecords<String, String> records = consumer.poll(duration);
System.out.println(records.count());
try {
Map<TopicPartition, OffsetAndMetadata> commits = new HashMap<>();
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s\n",
record.offset(), record.key(), record.value());
// 6. 记录提交的偏移量
commits.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset()));
// 7.生产新的消息,状态的变更
producer.send(new ProducerRecord<>("xt", "data"));
}
// 8.提交偏移量
producer.sendOffsetsToTransaction(commits, new ConsumerGroupMetadata("groupxt"));
// 9.事务提交
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
producer.abortTransaction();
} catch (KafkaException e) {
// 10.放弃事务
producer.abortTransaction();
}finally{
producer.flush();
}
}
}
}
Consumer事务
上述事务机制主要是从 Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其是无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被删除的情况
生产者常见的异常
retries,他会自动重试的
LeaderNotAvailableException
这个就是如果某台机器挂了,此时leader副本不可用,会导致你写入失败,要等到其他的follower副本切换为leader副本之后,才能继续写入,此时可以重试发送如果你平台重启kafka的broker进程,肯定会导致leader切换,一定会导致你写入报错,是LeaderNotAvaiableException
- NotControllerException
如果说Controller所在的Broker挂了,那么此时会有问题,需要等待Controller重新选举,此时也是一样的重试即可 - NetworkException
网络异常,重试即可
Producer读粘、拆包问题
粘包问题
简单说就是我只读取我想要的长度的数据,多了不读取,好吧
拆包问题
其实就是我要读取指定长度,如果不够,不好意思,缓存当前数据,网络继续传递吧
batch超时
accumulator内超时
List<RecordBatch> expiredBatches = this.accumulator.abortExpiredBatches(this.requestTimeout, now);
inFlightRequests
生产者消息回调
简化元数据拉取
RecordAccumulator(缓冲池)
Kafka Producer发送数据流程调
Kafka Producer发送数据流程调
2、broker
ISR & LEO & HW
ISR : in-sync replica,就是跟leader partition保持同步的follower partition的数量,只有处于ISR列表中的follower才可以在leader宕机之后被选举为新的leader,因为在这个ISR列表里代表他的数据跟leader是同步的
如果要保证写入kafka的数据不丢失,首先需要保证ISR中至少有一个follower,其实就是在一条数据写入了leader partition之后,要求必须复制给ISR所有的follower partition,才能说代表这条数据已提交,绝对不会丢失,这是Kafka给出的承诺
LEO(log end offset):offset = 0 ~ offset = 4 ,LEO = 5,代表了最后一条数据后面的offset,下一次要写入的offset
HW(High Water Mark) 高水位:follow同步到数据之后,就会更新自己的LEO,并不是leader主动推送数据给follower,他实际上是follower主动向leader尝试获取数据,不断的发送请求到leader来fetch最新的数据
然后对于接受到的某一条数据,所有follower的LEO都更新之后,leader才会把自己的HW高水位offset + 1,这个高水位offset表示的就是最新的一条所有follower都同步完成的消息
partition中最开始的一条数据的offset是base offset
LEO很重要的一个功能,是负责用来更新HW的,就是如果leader和follower的LEO同步了,此时HW就可以更新
所以对于消费者来说,只能看到base offset和HW offset之间的数据因为只有这之间的数据才表明所有follower都同步完成的,这些数据叫做”已提交”的,也就是commited,是可以被消费的
HW offset到LEO之间的数据,是”未提交的”,这时消费者是看不到的
整体流程过程 :
假设leader收到第一条数据,此时leader LEO = 1,HW = 0,因为他发现其他follower的LEO也是0,所有HW必须是0
第一请求过程 :
接着follower来发送fetch请求给leader同步数据,带过去follower的LEO = 0,所以leader上维护的follower LEO = 0,更新了一下,此时发现follower的LEO还是0,所以leader的HW继续是0
接着leader发送一条数据给follower,这里带上了leader的HW=0,因为发现leader的HW=0,此时follower LEO更新为1,但是follower HW = 0,取leader HW
第二请求过程 :
接着下次follower再次发送fetch请求给leader的时候,就会带上自己的LEO = 1,leader更细自己维护的follower LEO = 1,此时发现follower跟自己的LEO 同步了,那么leader的 HW更新为1
接着leader发送给follower的数据包含了HW = 1,此时follower发现leader HW = 1,自己的LEO = 1,此时follower的HW更新为1
重点:全部都要往前推进,需要2次请求,第一次请求是仅仅更新两边的LEO,第二次请求是更新leader管理的follower LEO,以及两个HW
剔除ISR列表情况
**Leader LEO和Follower LEO差距replica.lag.max.messages=4000了或者在replica.lag.time.max.ms=10s规定的时间内没有和Leader同步数据,就会剔除ISR列表
**
Kafka 0.8.x系列的ISR机制,其实是有权限的,参数默认的值是4000,也就是follower落后4000条数据就认为是out-of-sync,但是这里有一个问题,就是这个是固定死的。一般来说在Kafka 0.8.2.x系列版本上生产的时候,都会把这个ISR落后判定阈值设置的大一些,避免频繁的剔除ISR列表再回到ISR列表,你可以设置个几万,10万
Kafka 0.9.x之后去掉了replica.lag.max.messages=4000参数,只保留了replica.lag.time.max.ms参数,默认值是10秒,这个就不按照落后的条数来判定了,而是说如果某个follower的LEO一直落后leader超过了10秒,那么才判定这个follower是out-of-sync的
一般导致follower跟不上的情况主要是以下三种 :
1、follower所在机器的性能变差,比如说网络负载过高,IO负载过高,CPU负载过高,机器负载过高,都可能导致机器性能变差,同步过慢,这个时候就可能导致某个follower的LEO一直跟不上leader,就从ISR列表里剔除了
2、follower所在的broker进程卡顿,常见的就是full gc问题
3、kafka是支持动态调节副本数量的,如果动态增加了 partition 副本,就会增加新的follower,此时新的follower会拼命从leader上同步数据,但是这个是需要过程的,所以此时需要等待一段时间才能跟leader同步
Kafka Reactor网络线程模型
写磁盘
官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s
零拷贝 : Kafka的数据加工处理操作交由Kafka生产者和Kafka消费者处理。Kafka Broker应用层不关心存储的数据,所以就不用走应用层,传输效率高,说白了其实就是Linux内核数据传输
OS Cache + 顺序写 + 稀疏索引 + Segment
日志段文件,.log文件会对应一个.index和.timeindex两个索引文件
Kafka在写入日志文件的时候,同时会写入索引文件,就是.index和.timeindex,一个是位移索引,一个是时间戳索引,是两种索引
默认情况下,有个参数log.index.interval.bytes限定了在日志文件写入多少数据,就要在索引文件写入一条索引,默认是4KB,写4KB的数据然后在索引里写一条索引,所以索引本身是稀疏的索引,不是每条数据对应的一条索引
而且索引文件里的数据是按照位移和时间戳升序排序的,所以kafka在查找索引的时候,会用二分查找,时间复杂度是0(logN),找到索引,就可以在.log文件里定位到数据了
.index
44576 物理文件(.log位置)
57976 物理文件(.log位置)
64352 物理文件(.log位置)
offset = 58892 => 57976这条数据对应的.log文件的位置
接着就可以从.log文件里的57976这条数对应的位置开始查找,去找offset = 58892这条数据在.log里的完整数据
.timeindex是时间戳索引文件,如果要查找某段时间内的数据,先在这个文件里二分查找到offset,然后再去.index里根据offset二分找到对应的.log文件里的位置,最后就去.log文件里找到对应的数据
零拷贝
定时清理
本质Kafka是一个流式数据的中间件,不需要跟离线存储系统一样保存全量的大数据,所以Kafka是会定期清理掉数据的
Kafka默认是保留最近7天的数据,会把7天以前的数据给清理掉,包括.log、.index和.timeindex几个文件,log.retention.hours参数,可以自己设置数据要保留多少天,你可以根据自己线上的场景来判定一下
只要你的数据保留在Kafka里,你随时可以通过offset的指定,随时可以从Kafka搂出来几天前的数据,数据回放一遍
Kafka Controller
Kafka在Zookeeper中的元数据
在zookeeper的服务端存储的Kafka相关信息
1) /kafka/brokers/ids [0,1,2] 记录有哪些服务器
2) /kafka/brokers/topics/first/partitions/0/state
{"leader":1 ,"isr":[1,0,2] } 记录谁是Leader,有哪些服务器可用
ISR/ASR/OSR
ISR : 是leader和follower同步中的borker节点
AR : 是leader和follower所有的副本集,是一个整体
OSR : 是follower没有跟上leader步伐的broker
3) /kafka/controller
{"brokerid":0}
leader partition均匀分散
有一个参数,auto.leader.rebalance.enable,默认是true,每隔300秒(leader.imbalance.check.interval.seconds)会执行一次preferred leader选举,如果一台broker上的不均衡的leader超过了10%,leader.imbalance.per.broker.percentage,就会对这个broker进行选举
也可以手动执行,bin/kafka-preferred-replica-election.sh,但是不建议手动执行,让他自动执行就好了
Broker重要参数
auto.leader.rebalance.enable : 默认是true。自动Leader Partition平衡
leader.imbalance.per.broker.percentage : 默认是 10%。每个 broker 允许的不平衡的 leader 的比率。如果每个 broker 超过了这个值,控制器 会触发 leader 的平衡
leader.imbalance.check.interval.seconds : 默认值 300 秒。检查 leader 负载是否平衡的间隔时间
log.segment.bytes : Kafka 中 log 日志是分成一块块存储的,此配置是 指 log 日志划分 成块的大小,默认值 1G
log.index.interval.bytes : 默认 4kb,kafka 里面每当写入了 4kb 大小的日志 (.log),然后就往 index 文件里面记录一个索引
log.retention.hours : Kafka 中数据保存的时间,默认 7 天
log.retention.minutes : Kafka 中数据保存的时间,分钟级别,默认关闭
log.retention.ms : Kafka 中数据保存的时间,毫秒级别,默认关闭
log.retention.check.interval.ms : 检查数据是否保存超时的间隔,默认是 5 分钟
log.retention.bytes : 默认等于-1,表示无穷大。超过设置的所有日志总 大小,删除最早的 segment
log.cleanup.policy : 默认是 delete,表示所有数据启用删除策略; 如果设置值为 compact,表示所有数据启用压缩策略
num.io.threads : 默认是 8。负责写磁盘的线程数。整个参数值要占 总核数的 50%,handler线程池大小
num.replica.fetchers : 副本拉取线程数,这个参数占总核数的 50%的 1/3
num.network.threads : 默认是 3。数据传输线程数,这个参数占总核数的 50%的 2/3,processor线程池大小
log.flush.interval.messages : 强制页缓存刷写到磁盘的条数,默认是 long 的最大值,9223372036854775807。一般不建议修改, 交给系统自己管理
log.flush.interval.ms : 每隔多久,刷数据到磁盘,默认是 null。一般不建 议修改,交给系统自己管理
时间轮
Kafka内部有很多延时任务(延迟拉取、延迟生产、延迟删除等),没有基于JDK Timer来实现,那个插入和删除任务的时间复杂度是O(nlogn),而是基于了自己写的时间轮来实现的,时间复杂度是O(1),其实Netty中也有时间轮的实现(比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源)
简单来说,一个时间轮(TimerWheel)就是一个数组实现的存放定时任务的环形队列,数组每个元素都是一个定时任务列表(TimerTaskList),这个TimerTaskList是一个环形双向链表,链表里的每个元素都是定时任务(TimerTask)
时间轮是有很多个时间格的,一个时间格就是时间轮的时间跨度tickMs,wheelSize就是时间格的数量,时间轮的总时间跨度就是tickMs * wheelSize(interval),然后还有一个表盘指针(currentTime),就是时间轮当前所处的时间
currentTime指向的时间格就是到期,需要执行里面的定时任务
比如说tickMs = 1ms,wheelSize = 20,那么时间轮跨度(inetrval)就是20ms,刚开始currentTime = 0,这个时候如果有一个延时2ms之后执行的任务插入进来,就会基于数组的index直接定位到时间轮底层数组的第三个元素
因为tickMs = 1ms,所以第一个元素代表的是0ms,第二个元素代表的是1ms的地方,第三个元素代表的就是2ms的地方,直接基于数组来定位就是O(1)是吧,然后到数组之后把这个任务插入其中的双向链表,这个时间复杂度也是O(1)
所以这个插入定时任务的时间复杂度就是O(1)
然后currentTime会随着时间不断的推移,1ms之后会指向第二个时间格,2ms之后会指向第三个时间格,这个时候就会执行第三个时间格里刚才插入进来要在2ms之后执行的那个任务了
这个时候如果插入进来一个8ms之后要执行的任务,那么就会放到第11个时间格上去,相比于currentTime刚好是8ms之后,对吧,就是个意思
每个插入进来的任务,他都会依据当前的currentTime来放,最后正好要让currentTime转动这么多时间之后,正好可以执行那个时间格里的任务
那如果这个时候来一个350毫秒之后执行的定时任务呢?已经超出当前这个时间轮的范围了,那么就放到上层时间轮,上层时间轮的tickMs就是下层时间轮的interval,也就是20ms
wheelSize是固定的,都是20,那么上层时间轮的inetrval周期就是400ms,如果再上一层的时间轮他的tickMs是400ms,那么interval周期就是8000ms,也就是8s,再上一层时间轮的tickMs是8s,interval就是160s,也就是好几分钟了,以此类推即可
按照之前说的,假如说有一个定时450ms的任务,会被放到第三层时间轮的第二个时间格,他的时间是400ms,那么这个时间轮的指针转到这里,发现这个任务其实还有50ms才到期,此时就会把他重新放回到第二层时间轮
*第一层时间轮:1ms 20
第二层时间轮:20ms * 20
第三层时间轮:400ms 20*
对于第三层时间轮而言,每一个格子都是400ms,对于450ms的任务,就放在第三层时间轮的第二个格子里就好了,假设说currentTime指向的是第一个格子,然后400ms以后就会走到第二个格子,此时400ms已经过去了
发现这个任务是450ms以后才执行的,此时还有50ms才可以执行这个任务
降层,第二层时间轮,20ms,第一个格子是0,第二个格子是20ms,第三个格子是40ms,所以此时还剩50ms的任务可以放在第二层时间轮的第三个格子里,第二层时间轮转到第三个格子时,40ms过去了,还剩10ms
降层,第一层时间轮,1ms,放在第10个格子里就可以了,10ms过后指针转过去就可以执行这个延时任务了
推进时间轮
基于数组和双向链表来O(1)时间度可以插入任务
但是推进时间轮怎么做呢?搞一个线程不停的空循环判断是否进入下一个时间格吗?那样很浪费CPU资源,所以采取的是DelayQueue
每个时间轮里的TimerTaskList作为这个时间格的任务列表,都会插入DelayQueue中,设置一个延时出队时间,DelayQueue会自动把过期时间最短的排在队头,然后专门有一个线程来从DelayQueue里获取到期任务列表
某个时间格对应的TimerTaskList到期之后,就会被线程获取到,这种方式就可以实现时间轮推进的效果,推进时间轮基于DelayQueue,时间复杂度也是O(1),因为只要从队头获取即可
延迟拉取场景
假如就是消费速度大于生产速度的场景,kafka消费者去broker拉取消息,broker已经没有最新的数据了,怎么办?一直不断的请求拉取吗?很显然不可以,为什么?因为一旦broker一段时间压根没有新数据产生,你这么玩,是不是浪费很多网络请求,空轮询,做无用功。所以,kafka是这么干的
Kafka在处理拉取请求时,会先读取⼀次⽇志⽂件,如果收集不到⾜够多(fetchMinBytes,由参数fetch.min.bytes配置,默认值为1)的消息,那么就会创建⼀个延时拉取操作(DelayedFetch)以等待拉取到⾜够数量的消息。当延时拉取操作执⾏时,会再读取⼀次⽇志⽂件,然后将拉取结果返回给follower副本
延迟生产场景
比如一个leader和两个follower的场景,由于客户端设置了acks为-1,那么需要等到follower1和follower2两个副本都收到消息后才能告知客户端正确地接收了所发送的消息。如果在⼀定的时间内,follower1副本或follower2副本没能够完全拉取到消息,那么就需要返回超时异常给客户端。⽣产请求的超时时间由参数request.timeout.ms配置,默认值为30000,即30s。
那么这⾥等待消息写⼊follower1副本和follower2副本,并返回相应的响应结果给客户端的动作是由谁来执⾏的呢?在将消息写⼊leader副本的本地⽇志⽂件之后,Kafka会创建⼀个延时的⽣产操作(DelayedProduce),⽤来处理消息正常写⼊所有副本或超时的情况,以返回相应的响应结果给客户端
延时操作需要延时返回响应的结果,⾸先它必须有⼀个超时时间(delayMs),如果在这个超时时间内没有完成既定的任务,那么就需要强制完成以返回响应结果给客户端。其次,延时操作不同于定时操作,定时操作是指在特定时间之后执⾏的操作,⽽延时操作可以在所设定的超时时间之前完成,所以延时操作能够⽀持外部事件的触发
就延时⽣产操作⽽⾔,它的外部事件是所要写⼊消息的某个分区的HW(⾼⽔位)发⽣增⻓。也就是说,随着follower副本不断地与leader副本进⾏消息同步,进⽽促使HW进⼀步增⻓,HW每增⻓⼀次都会检测是否能够完成此次延时⽣产操作,如果可以就执⾏以此返回响应结果给客户端;如果在超时时间内始终⽆法完成,则强制执⾏
Kafka副本同步机制
Kakfa集群架构
Kakfa Broker端源码分析
3、消费者
Consumer核心逻辑
Condumer状态机流转
1) 每个consumer都发送JoinGroup请求
2) 选择一个consumer作为leader,也就是coordinator
3) 把要消费的topic情况发送给leader消费者
4) leader会负责指定消费方案
5) 把消费方案发送给coordinator
6) Coordinator就把消费方案下发给各个consumer
7) 每个消费者都会和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s),该消费者会被移除,并触发再平衡或者消费者处理消息时间过长(max.poll.interval.ms5分钟),也会触发再平衡
注意 : 这里其实就有一个场景,比如说我消费者在消费某一条数据的时候,不管是这个消息比较大的原因还是Full GC或者是下游有问题的原因,一直处于处理不完成的,也就是说5分钟都没有再次向broker进行重新拉区,这个时候就会把当前的consumer剔除,进行rebalance
Consumer分区分配策略
offset的默认维护位置
从0.9版本开始,consumer默认将offset保存在Kafka 一个内置的topic中,该topic为__consumer_offsets
Kafka0.9版本之前, consumer默认将offset 保存在Zookeeper中
__consumer_offsets 主题里面采用key和value 的方式存储数据。key是 group.id+topic+ 分区号,value 就是当前offset的值。每隔一段时间,kafka 内部会对这个 topic 进行 compact,也就是每个 group.id+topic+分区号就保留最新数据
在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false, 默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false
bin/kafka-console-consumer.sh -- bootstrap-server hadoop102:9092 --topic journey_group --group test
注意 : 指定消费者组名称,更好观察数据存储位置(key是 group.id+topic+分区号)
bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server hadoop102:9092 -- consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageForm atter" --from-beginning
Kafka Offset的提交
为了使我们能够专注于自己的业务逻辑,Kafka提供了自动提交offset的功能
自动提交offset的相关参数 :
enable.auto.commit : 是否开启自动提交offset功能,默认是true
auto.commit.interval.ms : 自动提交offset的时间间隔,默认是5s
手动提交offset
虽然自动提交offset十分简单便利,但由于其是基于时间提交的,开发人员难以把握offset提交的时机。因此Kafka还提供了手动提交offset的API
手动提交offset的方法有两种 : 分别是commitSync(同步提交)和commitAsync(异步提交)。
两者的相同点是,都会将本次提交的一批数据最高的偏移量提交;
不同点是,commitSync(同步提交)阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败)
而commitAsync(异步提交) 则没有失败重试机制,故有可能提交失败
commitSync(同步提交) : 必须等待offset提交完毕,再去消费下一批数据
commitAsync(异步提交) : 发送完提交offset请求后,就开始消费下一批数据了
由于同步提交 offset 有失败重试机制,故更加可靠,但是由于一直等待提交结果,提交的效率比较低
虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会受到很大的影响。因此更多的情况下,会选用异步提交offset的方式
重复消费 : 已经消费了数据,但是offset没提交
漏消费 : 先提交offset后消费,有可能会造成数据的漏消费
指定offset消费
auto.offset.reset = earliest | latest | none 默认是 latest
(1) earliest : 自动将偏移量重置为最早的偏移量,--from-beginning
(2) latest(默认值) : 自动将偏移量重置为最新偏移量
(3) none : 如果未找到消费者组的先前偏移量,则向消费者抛出异常
任意指定 offset 位移开始消费
Set<TopicPartition> assignment= new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
// 遍历所有分区,并指定 offset 从 1700 的位置开始消费
for (TopicPartition tp: assignment) {
kafkaConsumer.seek(tp, 1700);
}
指定时间消费
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
Map<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从1天前开始消费的每个分区的offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null){
kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
}
}
如感兴趣,点赞加关注,谢谢!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。