一、Kafka使用场景
Kafka的定位:消息中间件,消息引擎,分布式实时流处理平台
- 大数据领域:网站行为分析,日志聚合,应用监控,流数据处理在线和离线数据处理
- 数据集成:将数据导入离线数据仓库
- 流计算集成:与流计算引擎集成
二、Kafka架构图
-
生产者发送消息给broker的时候,会设置两个条件,一个是满足消息数量时发送,一个是满足延时时间时发送,这样就可以避免一条一条发送的模式浪费时间,又可以避免消息数量始终不满足时无法发送消息。
-
消费者不再接收broker主动发送的消息,只能主动拉取消息,这样可以避免消费者接收过多消息导致消费者崩溃,因此Kafka只有pull(拉取),没有push(推送)。
-
生产者发送消息给broker的一个topic,消费者会订阅一个topic来拉取消息。topic可以理解为一类消息的集合,起到队列的作用。消费者可以订阅多个topic,多个消费者也可以订阅一个topic
-
当生产者发送的消息指向一个不存在的topic,那么broker会自动创建一个topci,这个需要用配置来关闭,auto.create.topics.enable=true设置为false即可关闭自动创建。
-
topic可以视作queue队列,当有几十万条消息被发送到一个topic时,大量的消息被集中堆积,导致并发访问速度慢,扩展不便等问题。kafka通过分片解决了这个问题,即将一个topic中的消息分到多个partition中进行存储,在创建topic的时候,就可以指定该topic的分片数量,在这里被称之为partition(分区)。
-
在分区文件中,消息以.log文件的形式存储,而且所有的消息都以累加的方式不断顺序增加,不会删除,因此想要追溯历史消息,只需要按照逆序查找,添加消息时,只需要按照顺序增加。
-
在分区文件中,还有以.index文件和.timeindex文件的形式存储的索引,因为消息是有编号的,而且消息永远不删除,所以想要快速找到消息,就必须设计索引。
-
当一个broker挂掉之后,其中的数据也就永远丢失,所以kafka同样对数据做了复制备份,做到高可用。一个集群中的多个broker会通过复制topic数据来成为副本,通过设置副本数量来规定broker成为副本的数量,当副本数量为1时,就只有一个副本,也就是它本身。当副本数量少于集群中的broker数量时,任意一个broker都可能成为副本,当然,也可能不是。当副本数量等于集群中的broker数量时,一定是所有的broker都是副本,不存在一个broker存在两个副本的情况,因为没有意义,当broker宕机时数据一样全部丢失。当副本数量多于集群中的broker数量时,kafka会拒绝创建并报错,因为无法做到,做到也没有意义。
-
在上图中,红色字体的节点为leader节点,黑色字体的节点为follower,只有leader节点才能为客户端提供读写功能,因此无法实现读写分离,但也避免了读写不一致的问题。
-
segment 段是为了避免.log文件过大对其做出的切分,log.segment.bytes可以设置每一段的大小,当超过这一大小时,就会再生成三个文件(.log,.index,.timeindex)并以偏移量作为命名以区分。
-
consumer group消费者组,引入这个概念是为了当有多个消费者在同一个消费者组里,它们消费同一个topic时,一定是一个消费者消费一个partition,不可能出现一个以上的消费者消费同一个partition的情况。当消费者多于分区时,一定是一个消费者消费一个partition,多于的消费者将没有分区可以消费,而且一旦开始消费,这个消费关系就会被固定下来,这样就会让始终没有消息可以消费的消费者始终空闲。当消费者小于分区时,一个消费者可以消费多个分区。
-
当一个新的消费组开始对topic进行消费时,不受到上述限制,可以选择从最早的消息开始消费,也可以从最晚的消息开始消费,这样就可以做到消息的重复消费
-
consumer offest偏移量,当消费者消费一部分消息之后因为某些原因重启,消费者一般不希望从第一条消息开始重复消费,这样就需要记录已经消费到的消息数目,这就是偏移量的作用。每一条消息都有一个编号offest,而且从0开始计数。
-
消费者和offest之间的关联关系被存储在一些特殊的topic中,_consumer_offest-序号,默认是50个,可以进行设置,它会将consumer group的名字hash之后对50取模,得到的余数存储到对应的序号里。
在经过如上讲解之后,这张最后的总体结构图的描述就非常清晰了。
- 共有三个broker
- 共有两个topic:topic 0和topic 1
- topic 0有两个分区:partition 0和partition 1,每个分区共有三个副本
- topic 1共有一个分区:partition 0,每个分区共有三个副本
- 红色分区为leader分区,黑色为follower分区
- 绿色线是消息同步
- 蓝色线是写入消息
- 橙色线是读取消息
- 有两个消费者组:consumer group 0和consumer group 1
- consumer group 0消费topic 0的两个分区
- consumer group 1消费topic 0的两个分区和topic 1的一个分区
- consumer group 1中的consumer 0消费 topic 0的partition 0和topic 1的partition 0
- consumer group 1中的consumer 1消费 topic 0的partition 1
- consumer group 1中的consumer 2没有可以消费的消息
三、Kafka在Spring中的应用
1、消费端代码
一)自动提交
Properties props= new Properties();
//props.put("bootstrap.servers","127.0.0.1:9093,127.0.0.2:9094,127.0.0.3:9095");
props.put("bootstrap.servers","127.0.0.1:9092");
props.put("group.id","test-group");
// 是否自动提交偏移量,只有commit之后才更新消费组的 offset
props.put("enable.auto.commit","true");
// 消费者自动提交的间隔
props.put("auto.commit.interval.ms","1000");
// 从最早的数据开始消费 earliest | latest | none
props.put("auto.offset.reset","earliest");
props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
// 订阅topic
consumer.subscribe(Arrays.asList("mytopic"));
try {
while (true){
ConsumerRecords<String,String> records=consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String,String> record:records){
System.out.printf("offset = %d ,key =%s, value= %s, partition= %s%n" ,record.offset(),record.key(),record.value(),record.partition());
}
}
}finally {
consumer.close();
}
二)手动提交
Properties props= new Properties();
//props.put("bootstrap.servers","127.0.0.1:9093,127.0.0.2:9094,127.0.0.3:9095");
props.put("bootstrap.servers","127.0.0.1:9092");
props.put("group.id","test-group");
// 是否自动提交偏移量,只有commit之后才更新消费组的 offset
props.put("enable.auto.commit","false");
// 消费者自动提交的间隔
props.put("auto.commit.interval.ms","1000");
// 从最早的数据开始消费 earliest | latest | none
props.put("auto.offset.reset","earliest");
props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
// 订阅 topic
consumer.subscribe(Arrays.asList("commit-test"));
final int minBatchSize = 200;
List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
// 手动提交
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d ,key =%s, value= %s, partition= %s%n" ,record.offset(),record.key(),record.value(),record.partition());
buffer.add(record);
}
if (buffer.size() >= minBatchSize) {
// 同步提交
consumer.commitSync();
buffer.clear();
}
}
2、生产端代码
Properties pros=new Properties();
//props.put("bootstrap.servers","127.0.0.1:9093,127.0.0.2:9094,127.0.0.3:9095");
props.put("bootstrap.servers","127.0.0.1:9092");
pros.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
pros.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 0 发出去就确认 | 1 leader 落盘就确认| all(-1) 所有Follower同步完才确认
pros.put("acks","1");
// 异常自动重试次数
pros.put("retries",3);
// 多少条数据发送一次,默认16K
pros.put("batch.size",16384);
// 批量发送的等待时间
pros.put("linger.ms",5);
// 客户端缓冲区大小,默认32M,满了也会触发消息发送
pros.put("buffer.memory",33554432);
// 获取元数据时生产者的阻塞时间,超时后抛出异常
pros.put("max.block.ms",3000);
// 创建Sender线程
Producer<String,String> producer = new KafkaProducer<String,String>(pros);
for (int i =0 ;i<10;i++) {
producer.send(new ProducerRecord<String,String>("mytopic",Integer.toString(i),Integer.toString(i)));
}
//producer.send(new ProducerRecord<String,String>("mytopic","1","1"));
//producer.send(new ProducerRecord<String,String>("mytopic","2","2"));
producer.close();
四、Kafka在SpringBoot中的应用
1、配置
server.port=7271
#spring.kafka.bootstrap-servers=127.0.0.1:9093,127.0.0.2:9094,127.0.0.3:9095
spring.kafka.bootstrap-servers=127.0.0.1:9092
# producer
spring.kafka.producer.retries=1
spring.kafka.producer.batch-size=16384
spring.kafka.producer.buffer-memory=33554432
spring.kafka.producer.acks=all
spring.kafka.producer.properties.linger.ms=5
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# consumer
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-commit-interval=1000
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
2、消费端代码
@Component
public class ConsumerListener {
@KafkaListener(topics = "springboottopic",groupId = "springboottopic-group")
public void onMessage(String msg){
System.out.println("----收到消息:"+msg+"----");
}
}
3、生产端代码
@Component
public class KafkaProducer {
@Autowired
private KafkaTemplate<String,Object> kafkaTemplate;
public String send(@RequestParam String msg){
kafkaTemplate.send("springboottopic", msg);
return "ok";
}
}
五、进阶使用
1、幂等性处理
重发消息时一定需要幂等性处理,当消息重复发送时业务不能重复进行。kafka的broker帮助消费者做出消息重复性处理。
// 消息幂等性
props.put("enable.idempotence", true);
当这个属性为true时,broker将判断消息的重复性,它的依据有两个:PID(producer id)和sequence number
- PID:生产者编码,每一个生产者都会有一个唯一编码
- sequence number:消息编号,当消息编号小于等于生产者当前处理编号时,证明消息重复。消息编号比当前编号大于1时,证明消息是正确的。消息编号比当前编号超过1时,证明消息有遗漏。但是这种有序性不是全局性的,只有在同一个partition上或者在生产者一次会话中消息编号才是有序的,一旦发送到不同分区或者生产者重启或停止此次会话,就会失去有序性,这样做是为了让kafka降低性能消耗。
2、事务
一)需要事务的情景
1.发送多条消息
3. 发送消息到多个topic或多个partition
4. 消费后发送消息 consume-process-produce
二)使用方式
// 事务ID,唯一
props.put("transactional.id", UUID.randomUUID().toString());
Producer<String,String> producer = new KafkaProducer<String,String>(props);
// 初始化事务
producer.initTransactions();
try {
producer.beginTransaction();
//消费和发送在同一段代码中可以使用这个方法
//producer.sendOffsetsToTransaction();
producer.send(new ProducerRecord<String,String>("transaction-test","1","1"));
producer.send(new ProducerRecord<String,String>("transaction-test","2","2"));
// Integer i = 1/0;
producer.send(new ProducerRecord<String,String>("transaction-test","3","3"));
// 提交事务
producer.commitTransaction();
} catch (KafkaException e) {
// 中止事务
producer.abortTransaction();
}
开启事务后,当程序出现异常时,消息不会提交,因此消费者不会受到任何消息,只有正常提交,才能被消费者消费。
三)实现原理
- 2PC:两段提交,由于需要跨分区处理事务,kafka实现的思想类似于分布式事务的实现,即两段提交,先预提交,等到所有的事务都预提交之后才真正的提交,否则就必须放弃。
- transaction coordinator:事务协调者,既然启用两段提交,就需要一个协调者,初始化事务是,就是为生产者匹配一个协调者来协调大家一起提交。
- topic__transaction_state:事务日志,所有的事务都会被日志记录,以防止协调者挂掉重启之后,找不到之前需要协调的日志,在kafka中,这个记录也是用一个特殊的topic实现,即_transaction_state
- transaction.id:生产者事务ID,如果生产者挂掉,就需要用唯一的事务ID来判断生产者还未处理的事务。
四)实现步骤
A.:生产者通过initTransactions API向Coordinator注册事务ID
B:Coordinator记录事务日志
C:生产者把消息写入目标分区
D:分区和Coordinator的交互。当事务完成后,消息的状态应该是已提交,这样消费者才能消费到
五、特性
- 高吞吐,低延迟
- 高伸缩性
- 持久性,可靠性
- 容错性
- 高并发