kafka

Kafka

数据源直接到计算引擎的话会产生的问题:
1、数据不安全,在传输过程中,可能丢失
2、数据源的产生的数据的速度不恒定,计算引擎的处理数据有上限,可能会产生数据挤压
3、数据源和计算引擎的耦合度太高,维护起来不方便

因此在数据源和计算引擎之间加一个消息中间件,kafka
kafka的特点:
1、高吞吐量、低延迟:kafka每秒可以处理大量的消息,延迟低至几毫秒,每个topic可以分多个partition,有多个consumer group对partition进行consume操作
2、可扩展性 kafka集群支持热扩展 增加节点个数
3、持久性、可靠性 消息被持久化到磁盘(落地磁盘),并且支持数据备份防止数据丢失
4、容错性 允许集群中有节点失败 (若副本数据为n,允许n-1个节点失败(只剩1个可以工作))
5、高并发 支持数千个客户端同时多写

即 削峰填谷 解耦合

在大数据流式计算领域中,kafka主要作为计算系统的前置缓存和输出结果缓存

作为消息中间件需要满足的条件
1、数据存储可靠(副本,持久到磁盘)
2、读写能力强(吞吐量大)
3、容错性高
4、高并发
消息队列的两种模式

点对点模式
	消费者主动拉取数据,消息收到之后清楚消息

发布/订阅模式(消息的生产者不会直接将消息发送给特定的订阅(消费)者,而是将发布的消息分为不同的topic,订阅(消费)者只接收感兴趣的消息)
可以有多个topic(eg 浏览、点赞、收藏等)
消费者在消费数据之后,不删除数据
每个消费者相互独立,都可以消费到数据
消费者可以订阅不同的topic

what kafka

kafka是基于scala、java语言开发的分布式 消息发布-订阅系统,具有高吞吐量、低延迟的特性,许多大数据实时流式处理系统比如 strom、spark、flink都可以集成

存储系统:通常消息队列会把消息持久化到磁盘系统,防止信息丢失,来提高消息可靠性,kafka的消息持久化制剂和多副本机制是其能够作为通用数据存储系统来使用

消息系统:kafka和传统的消息队列,例如 RabbitMQ/RocketMQ/ActiveMQ 支持流量削峰、服务解耦、异步通信等核心功能

流处理平台: kafka不仅能够与大多数流式计算框架完美整合,并且自身也提供了一套了一个完整的流式处理库,
即kafka Streaming ,kafka Streaming提供了类似flink的窗口、聚合、变换、连接等功能

即 kafka是一个 分布式的 基于 发布/订阅 模式的消息中间件,主要引用于大数据实时流计算领域 起解耦合和削峰填谷的作用

kafka架构

Producers 生产者 向topic的某个partition的leader进行写入数据


Brokers 容器,每一台机器都是一个broker 每个broker都必须有一个唯一id


Consumers 消费者 消费某个topic中的数据,消费的只能是topic的partition的leader里面的信息


zookeeper 管理kafka节点,集群中有哪些topic,topic有那些partition,server(broker)在线情况,等元信息和状态信息都需要在集群内部和客户端共享,可以使用zookeeper作为管理
topic和partition
topic
kafka中存储数据的逻辑分类,类似于数据库中表的概念
	eg:将app端日志、微信小程序端日志等不同数据源的数据放入到不同的topic中

partition 分区 提升kafka的吞吐量
topic中数据的具体管理单元 类似于hbase表中的region(将表横向分割)
- 每个partition都有一个kafka broker服务器进行管理
- 每个topic可以划分为多个partition,分不到不同的broker上管理
- 每个partition都可以有多个副本,保证数据安全

分区对于kafka集群来说,实现topic的负载均衡,提高写入、读出的并发度,提高吞吐量

便于合理使用存储资源 每个partition在一个broker上存储,可以把海量的数据按照分区切割成一块一块的数据存储在多台broker上,合理控制分区任务,实现负载均衡的效果

提高并行度 生产者可以以partition为单位发送数据,消费者可以以分区为单位进行消费数据
分区副本replication-factor

每个topic 的partition都可以有多个副本,用来提高数据的可靠性
每个partition副本中,必定有一个leader副本,就是follower副本,(observer副本)
follower副本定期找leader同步最新的数据,对外提供服务的只有leader

分区follower
partition replica中的一个角色,他通过心跳通信不断地从leader中拉取、复制数据(只负责备份)
如果leader所在的节点宕机,follower中会选举出新的leader

消息偏移量 offset
partition内部每条消息都会被分配到一个递增的id(offset)
通过offset 可以快速定位到消息的存储位置
kafka只保证按一个partition中的消息的顺序,不保证一个topic的整体(多个partition之间)的顺序
每个分区的偏移量都是从0开始
kafka消息队列
添加数据的时候,是按照顺序添加的,偏移量会递增,不能够对topic里面的partition中的具体数据进行修改,只能够进行追加
自我推导设计:
- kafka是用来存数据的;
- 现实世界数据有分类,所以存储系统也应有数据分类管理功能,如mysql的表;kafka有topic;
- 如一个topic的数据全部交给一台server存储和管理,则读写吞吐量有限;
- 所以,一个topic的数据应该可以分成多个部分(partition)分别交给多台server存储和管理;
- 如一台server宕机,这台server负责的partition将不可用,所以,一个partition应有多个副本;
- 一个partition有多个副本,则副本间的数据一致性难以保证,因此要有一个leader统领读写;
- 一个leader万一挂掉,则该partition又不可用,因此还要有leader的动态选举机制;
- 集群有哪些topic,topic有哪几个分区,server在线情况,等等元信息和状态信息需要在集群内部及客户端之间共享,则引入了zookeeper;
- 客户端在读取数据时,往往需要知道自己所读取到的位置,因而要引入消息偏移量维护机制;
broker服务器:一台 kafka服务器就是一个broker。一个kafka集群由多个 broker 组成。
生产者producer:消息生产者,就是向kafka broker发消息的客户端。
消费者consumer
- consumer :消费者,从kafka broker 取消息的客户端。
- consumer group:消费组,单个或多个consumer可以组成一个消费组;
消费组是用来实现消息的广播(发给所有的 consumer)和单播(发给任意一个 consumer)的手段

基本操作

kafka的bin目录下提供了许多命令行工具,用于管理集群的变更
kafka-console-consumer.sh   用于消费数据
sh脚本作用
kafka-console-consumer.sh用于消费消息
kafka-console-producer.sh用于生产消息
kafka-topics.sh用于管理topic
kafka-server-stop.sh用于关闭kafka服务
kafka-server-start.sh用于开启kafka服务
kafka-configs.sh用于管理配置
kafka-consumer-perf-test.sh用于测试消费性能
kafka-producer-perf-test.sh用于测试生产性能
kafka-dump-log.sh用于查看数据日志内容
kafka-preferred-replica-election.sh用于优先副本的选举
kafka-reassign-partitions.sh用于分区重分.000000000000 配
topic管理
(可以把zk集群都写上,防止一台宕机,还可以连接其他的)
查看topic列表 list
kafka-topics.sh --list --zookeeper note01:2181

查看某个topic的详细信息
kafka-topics.sh --describe --zookeeper note01:2181 --topic topic名

创建一个topic(使用--Bootstrap-server 和使用--zookeeper是一样的)
kafka-topics.sh --create --bootstrap-server note01:9092 --topic topic名 --partitions 分区数 --replication-factor 副本数

副本数需要小于等于broker的数量
使用该种方式副本的存储位置是由系统决定的

手动决定
kafka-topics.sh --create --topic tpc-1  --zookeeper linux01:2181 --replica-assignment 0:1:3,1:2:6

该topic,将有如下partition:(2个分区 3个副本)
partition0 ,所在节点: broker0、broker1、broker3
partition1 ,所在节点: broker1、broker2、broker6

删除topic
kafka-topics.sh --zookeeper note01:2181 --delete --topic topic名
再删除topic的时候,server.properties 需要该参数是处于开启状态
delete.topic.enable=true(默认为true)

使用该脚本进行topic删除主题,本质上只是在zookeeper上的/admin/delete_topic路径下建立一个与岱山主题同名的节点,用来标记该主题是待删除咋宏泰,然后由kafka控制器异步完成

增加分区数
kafka-topics.sh --alter --zookeeper note01:2181 --topic topic名 --partition 数

kafka只支持增加分区(增加分区不会影响到之前的分区信息)不支持减少分区
原因 减少分区代价太大,(数据迁移,日志段拼接合并),需要保证数据的有序性和可靠性

减少分区 破坏消息的顺序性
如果减少某个分区,必然会丢失某些分区的消息,如果选择将他们合并(追加)到其他的分区,那么就会破坏kafka单个分区的有序性,如果要保证在删除单个分区时保持数据的有序性,是一件非常麻烦的事情


减少分区意味着有大量的数据将要移动,在移动过程还需要保证数据在每个分区内严格的有序性,如果要实现这种操作,必然是一个非常繁重的任务,这就与kafka的高吞吐,低延迟的理念就违背了

kafka中,topic的partition里面的具体的消息是不能够删除的,只能够追加写(消息队列先进先出,如果需要删除某个消息,[可能需要将该消息所在的队列中的消息出列,去掉他之后,在按照顺序进队列],所需要的资源开销,时间开销等都非常大,与kafka高吞吐、低延迟的理念违背

但是可以将整个topic进行删除,标记性删除


kafka作为消息中间件,他的作用是用来传递消息的至于传递什么样的消息kafka并不关心,他的重心在于消息的传递
动态配置topic
kafka-topics.sh  --zookeeper linux01:2181  --alter  --topic tpc2 --config compression.type=gzip 
# --config compression.type=gzip  修改或添加参数配置
# --add-config compression.type=gzip  添加参数配置
# --delete-config compression.type  删除配置参数

topic配置参数文档地址: https://kafka.apache.org/documentation/#topicconfigs
生产者
kafka-console-producer.sh --broker-list note01:9092 --topic topic名
可以指定分区生产消息,不指定的话将随机分配到topic的partition里面
随机分配
实现随机分配的代码只需要两行,如下
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size()); 
消费者
kafka-console-consumner.sh

消费者在消费消息的时候,需要指定订阅的topic,还可以指定消费的起始偏移量
起始偏移量的策略
1、earliest 起始点
2、latest 最新
3、指定 offset (分区号:偏移量) 必须指定topic partition offset
4、从之前所记录的的偏移量开始消费

--from-beginning 指定从最前面开始消费
如果不加--from-beginning  就需要分情况讨论了,如果之前记录过消费的位置,那么就从之前消费的位置开始消费,如果说之前没有记录过之前消费的偏移量,那么就从最新的位置开始消费

-- 指定从最前面开始消费
kafka-console-consumer.sh --bootstrap-server note01:9092 --topic test01 --from-beginning

-- 不指定他消费的位置的时候,就是从最新的地方开始消费
kafka-console-consumer.sh --bootstrap-server linux01:9092 --topic test

-- 从指定的offset(需要指定偏移量和分区号)
kafka-console-consumer.sh --bootstrap-server linux01:9092 --topic test --offset 2 
--partition 0
kafka 消费过的数据不会被删除,依然存在
消费者组
消费者组是kafka为了提高消费并行度的一种机制
在kafka的底层逻辑中,任何一个消费者都有自己所属的组(如果没有指定,系统会分配一个组id)
组和组之间,没有任何的关系,大家都可以消费到自己想要的目标topic的所有数据
但是组内的各个消费者, 就只能够读取到自己所分配到的partitions中的消息

kafka中的消费组,可以动态增加或减少消费者,而且消费组中的消费者数量发生任意变化,都会重新分配分区消费任务(消费者组再均衡策略)
如何让多个消费者组成一个组: 就是让这些消费者的groupId相同即可
1、消费者组是提高消费者并行度的一种策略
2、消费者后面可以跟一个id,如果不同的消费者的组id相同,那他们就会落在同一个消费者组里面
3、同一个消费者组中的两个消费者永远不可能消费到同一个分区的数据
4、如果消费者组里面的消费者的数量大于分区的数量,会出现有些消费者消费不到数据
5、消费者组合消费者组之间没有必然的联系,互不干扰,互不影响
6、消费者组中的消费者的数量可以随意增加或减少,当消费者组中的消费者数量发生变化时,kafka都会对此消费者组进行重新分配消费任务(消费者再均衡策略)

kafka API

生产者 API
官网 https://kafka.apache.org/documentation/#producerapi

1、创建生产者对象,并配置其所需要的参数
2、构建待发送的信息 ProducerRecode()
3、发送信息 send()
4、关闭实例 close()

采用默认分区的方式,将消息散列到各个分区

// 生产者所需要的配置信息
Properties properties = new Properties();

//key.serializer value.serializer bootstrap.servers 为必须配置的选项

properties.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("bootstrap.servers","note01:9092,note02:9092,note03:9092");
//选择配置 。。。见kafka官网
// ack模式,取值有0,-1,1(all) all是最慢的但是是最安全的,服务器应答生产者的成功策略
properties.setProperty("acks","all");
//创建一个生产者
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);

for (int i = 0; i < 100; i++) {
ProducerRecord<String, String> record = new ProducerRecord<String,String>("study","is java message"+i);
// 发送数据
producer.send(record);
}

producer.close();
kafka的生产者可以持续不断的向topic中发送数据
kafka在构建生产者的时候必须提供的配置参数是 
1、kafka集群地址 ProducerConfig.BOOTSTRAP_SERVERS_CONFIG
2、key的序列化方式 (通过反射获取到全类名)
    eg:
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()
3、value的序列化方式
    eg:
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()

kafka有自己的序列化方式,在发送数据的时候,需要使用自己的序列化方式(自定义对象需要实现kafka的序列化接口)

在构建kafka的生产者时,不需要指定topic,在发送数据对象的时候指定topic
eg:
ProducerRecord<String, String> record = new ProducerRecord<String,String>("study","is java message"+i);
producer.send(record);

其他的生产者参数
compression.type
指定消息的压缩方式,默认为"none",可以配置为 "gzip""snappy""lz4"

batch.size
消息累加器中每一个Batch存放数据的大小,默认值为16KB,积攒的16k就发出去,如果没有到16k,但是到达了lenger.ms设置的时间,也会发送数据,如果将默认值调大,可以增加几群的吞吐量,但是会导致发送数据的延迟增高.

linger.ms
batch发送的延迟时间,默认是0,指的是sender线程过来读取数据的时候,不管有没有满足16k的大小,都会立刻发送出去.
消费者 API
消费者在消费的时候遵循以下逻辑:
1、如果为消费者指定消费那个topic的那个partition的那个offset,那么消费者就会重指定的位置开始消费数据
2、如果并没有指定消费者消费的位置,则先去 __consumer_offsets(记录着消费者上一次消费的位置+1)
	1、如果在该topic里面找到了消费者的group id,就会从__consumer_offsets里面记录的位置开始消费数据
	2、如果,在该topic里面没有找到该消费者的group id,就会寻找 auto.offset.reset 这个参数设置的是啥
	("auto.offset.reset"可以配置的参数 latest, earliest,默认的是latest,从最新的位置开始消费,earliest 从最开始的地方开始消费)
__comsumer_offsets
__consumer_offsets 里面记录的信息 默认5秒添加记录(不管有没有消费)
group_id
topic
partition
offset
提交时间

即那个消费组消费到了那个topic的那个partition的那个offset(此offset是上一次消费到的offset+1)
__consumer_offsets中记录的消费偏移量,代表的是,消费者下一次要读取的位置
一般的消费步骤
1、创建消费者对象,配置消费者对象所需要的配置信息
2、订阅topic (subscribe)
	消费指定分区 assign
3、poll 拉取数据并消费
4、定期向__consumer_offsets 主题提交消费位移 offset
5、关闭消费者 close
订阅整个topic
//订阅整个topic

        Properties properties = new Properties();
        properties.setProperty("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
        properties.setProperty("bootstrap.servers","note01:9092,note02:9092,note03:9092");
        //必须为消费者设定一个group.id
        properties.setProperty("group.id","java");
        properties.setProperty("enable.auto.commit","true");//在__consumer_offsets中记录消费者组的上一次的读取记录
        //创建一个消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        //告诉消费者消费那个topic
        consumer.subscribe(Arrays.asList("study"));
        while (true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
            for (ConsumerRecord<String, String> record : poll) {
                String topic = record.topic();
                long offset = record.offset();
                String key = record.key();
                String value = record.value();
                System.out.println(topic+"==>"+offset+"==>"+key+"==>"+value);
            }
        }
订阅某个partition
//订阅topic的某个分区 指定offset

Properties properties = new Properties();
        properties.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        properties.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"note01:9092,note03:9092,note02:9092");
        properties.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"zss");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
        //创建topicpartition
        TopicPartition study = new TopicPartition("study", 0);
		//为消费者分配指定的partition
        consumer.assign(Arrays.asList(study));
        //告诉消费者从指定partition的那个offset开始消费
        consumer.seek(study,0);
        while (true){
            ConsumerRecords<String, String> poll = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
            for (ConsumerRecord<String, String> recode : poll) {
                System.out.println(recode);
            }
        }

动态订阅
正则的方式订阅主题(实现动态订阅topic)
在为消费者指定消费topic的时候,使用正则表达式的方式(subscribe(Patttern)) 订阅,在这之后,又有人创建了topic,正则表达式可以匹配到,那么消费者就可以消费到新添加的topic里面的消息了
如果程序需要消费多个主题,并且可以处理不同的类型,那这种订阅方式就很有效

eg:
consumer.subscribe(Pattern.compile("topic.*"));
subscribe和assign的区别
通过 subscribe方法订阅的主题具有消费者再分配的均衡功能
	多个消费者的情况下可以根据分区分配策略来自动分配消费者与分区的关系,当消费组的消费者增加或减少的时候,
	分区分配关系会自动调整,以实现消费负载均衡以及故障自动转移

通过assign方式订阅分区,是不具备消费者自动负载均衡的
	其实这一点从assign方法参数可以看出端倪,两种类型subscribe()都有ConsumerRebalanceListener类型参数的方法,而assign()方法却没有。
重复、丢失消费数据问题
关于重复和丢失消费数据的问题的根本原因是
消费者消费数据的偏移量的提交和业务处理逻辑之间的关系
提交的偏移量  =  消费完的record的偏移量  +  1
因为,__consumer_offsets中记录的消费偏移量,代表的是,消费者下一次要读取的位置

当 "enable.auto.commit"="true"时,处于自动提交的状态
	1、偏移量提交之后,程序宕机,此时业务逻辑还没来得及执行 
		在重启程序之后,消费者将在提交的偏移量那里开始读取,这时会造成 数据漏读现象
	2、在业务逻辑执行完之后,程序宕机,此时消费者消费的偏移量还没来得及提交
		在重启程序之后,消费者将重新读取上一次消费的数据,此时会造成 重复读数据现象
当 "enable.auto.commit"="false"时,手动提交 半自动
由我们自己控制消费者偏移量(offset)的提交时机
eg:在完成业务逻辑的时候,手动提交 
	同步提交 consumer.commitSync();  会阻塞
	异步提交 consumer.commitAsync(); 不会阻塞
	还是会有重复读数据的情况
如果业务逻辑落到的数据库支持幂等性,还自己去重
当 "enable.auto.commit"="false"时 全手动
将消费者消费的偏移量不提交到 __consumer_offsets中
eg:在MySQL中仿照 __consumer_offsets 创建一张表,将消费者的group_id,topic,partition,offset+1信息记录下来,
将提交消费者偏移量和业务数据落库绑定到一个事务当中,成功这都成功,不成功则回滚状态
(mysql 在进行 insert delete update等的操作的时候,自动开启事务,提交事务 conn.commit();[connmysql驱动的连接对象] 事务回滚 conn.rollback();
eg:
      try {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(Integer.MAX_VALUE));
                for (ConsumerRecord<String, String> record : records) {
                    String data = record.value();
                    String[] arr = data.split(",");
                    String id = arr[0];
                    String name = arr[1];
                    String age = arr[2];

                    pps.setInt(1, Integer.parseInt(id));
                    pps.setString(2, name);
                    pps.setInt(3, Integer.parseInt(age));
                    pps.execute();

                    //埋个异常,看看是不是真的是这样
//                    if (Integer.parseInt(id) == 5) {
//                        throw new SQLException();
//                    }

                    long offset = record.offset();
                    int partition = record.partition();
                    String topic = record.topic();
                    pps_offset.setString(1, topic + "_" + partition);
                    pps_offset.setInt(2, (int) offset + 1);
                    pps_offset.setInt(3, (int) offset + 1);
                    pps_offset.execute();
                    //提交jdbc事务
                    connection.commit();
                }
            } catch (Exception e) {
                connection.rollback();
            }
提交offset总结
全自动
1、"auto.offset.commit"="true"
2、定时提交到__consumer_offsets

半自动
1、"auto.offset.commit"="false"
2、手动触发提交 consumer.commitAsync(); (异步提交,同步提交 commitSync)
3、提交到 __consumer_offsets

全手动
1、"auto.offset.commit"="false"
2、将消费者的消费位移保存到自己的数据表中 如mysql/zk/redis
3、提交到自己所涉及的存储,初始化的时候也需要到自己定义的存储位置去查询消费位移

消费者组再均衡策略

触发消费者再均衡策略的事件
1、有新的消费者加入到消费者组
2、有消费者宕机、下线(并不一定是真的下线,eg:长时间的GC,网络延迟导致消费者很长时间未向GroupCoordinator发送心跳等情况)GroupCoordinator会认为该消费者已经下线
3、有消费者主动退出消费组 unsubscribe 取消订阅
4、消费者组所对应的GroupCoorinator节点发生了更改
5、消费者组内所订阅的任意一个主题或者主题的分区数量发生变化

都会触发消费者再均衡策略,将一个分区的消费权从一个消费者移动到另一个消费者称为再均衡,如何Rebalance也设计分区再均衡策略

通过配置消费者参数
partition.assignment.strategy 默认的是range

Rebalance过程如下
1、所有成员向coordinator发送请求,请求入组,一旦所有成员都发送了请求,Coordinator会从中选择一个consumer作为leader(此leader为group leader 他的选举是随机的),并把组成员以及订阅信息发送给leader
2、leader开始分配消费方案,指明具体那个consumer负责消费那些topic的那些partition,一旦完成分配,leader会将这个方案提交给coordinator,coordinator接收到分配方案之后,会把方案发送给各个consumer,这样组内所有的成员就知道自己该消费那些partition了
对于Rebalance来说,GroupCoorinator起着至关重要的作用
Range Strategy
对应的类是
org.apache.kafka.clients.consumer.RoundRobinAssignor

先将消费者按照client.id的字典顺序进行排序,然后按照 topic 逐个进行处理
策略:
	针对一个topic 将其partition的总数 / 消费者数 = 商n ... 余数m
	则每个消费者至少分到 n个分区,并且前 m 个消费者每人多分一个分区
	
缺陷:
	容易分配不均衡
	stop the world 在重新分配期间 会将所有消费者手中的分区回收,重新进行分配,此期间消费者不在消费数据
	消费者原先消费的分区,现在可能分配给其他消费者(消费乱序)
Round-Robin Strategy
对应的类是
org.apache.kafka.clients.consumer.RangeAssignor

策略
	将所有的topic分区组成一个TopicPartition列表,并对TopicPartition列表按照其hashcode排序,然后按照轮询的方式分配给消费者(轮询的方式 你一个我一个)
	
解决了消费分配不均的问题

依然存在
	stop the world 依旧会把消费者手中的所有分区收回
	消费者原先的消费分区,还是可能会分给其他人
Sticky Strategy
对应的类是
org.apache.kafka.clients.consumer.ConsumerPartitionAssignor

策略
	range  的基础上强化
	还是会全部回收分区,依旧造成 stop the world
	在重新分配的时候,尽量归还原先消费者所消费的分区(不保证一定归还)
Cooperative Sticky Strategy
对应的类
org.apache.kafka.clients.consumer.ConsumerPartitionAssignor

与sticky策略一致
支持coopeartive再均衡策略,在再均衡过程中,不会让消费者这取消掉所有的分区然后再重新消费,消费者可以依旧消费自己之前消费的partition
GroupCoordinator
每一个消费者在服务端对应一个GroupCoordinator对其进行管理,GroupCoordinator是kafka服务端中用于管理消费者组的组件

消费者客户端中有ConsumerCoordinator组件负责与GroupCoordinator进行交互
二者最重要的职责是负责执行消费者Rebalance操作

ConsumerCoordinator 再均衡发生时,所有消费者都会停止工作,等待新方案的同步

cooperative把原来eager协议的一次性全局再均衡,化解成了多次的小均衡,并最终达到全局均衡的收敛状态
再均衡监听器
再均衡监听器  ConsumerRebalanceListener
如果想控制消费者在发生再均衡时执行一些特定的工作,可以通过订阅主题时注册“再均衡监听器”来实现;
场景举例:在发生再均衡时,处理消费位移
如果A消费者消费掉的一批消息还没来得及提交offset,而它所负责的分区在rebalance中转移给了B消费者,则有可能发生数据的重复消费处理。此情形下,可以通过再均衡监听器做一定程度的补救

 eg:
 //消费数据
        consumer.subscribe(Arrays.asList("test"), new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {

            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                //获取到上一次消费到的位移
                for (TopicPartition tp : partitions) {
                    int partition = tp.partition();
                    String topic = tp.topic();
                    try {
                        get_offset.setString(1, "mm");
                        get_offset.setString(2, topic);
                        get_offset.setInt(3, partition);
                        ResultSet resultSet = get_offset.executeQuery();
                        if (resultSet.next()) {
                            long offset = resultSet.getLong("offset");
                            consumer.seek(tp, offset);
                        } else {
                            consumer.seek(tp, 0);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

kafka数据的存储结构

kafka 数据存储的位置
在kafka的配置文件 server.properties 中 log.dir所配置的路径
整体存储结构

topic
     partition0  
     	partition0有副本数 副本有leader follower  segment(是一个统称)由.index .log .timeindex   				还有一个文件记录 leader-epoch-checkpoint 其他文件
     partition1
     	。。。。
     	
leader-epoch-checkpoint 记录了leader的纪元号,还记录了当前leader往kafka中写了多少数据的offset,后来得leader会跟着之前的leader写的数据后面接着写
存储的目录名
	topic名-分区号 
	eg:study-0 
00000.index 是索引文件(稀疏索引) 记录着 .log文件中offset的偏移量
00000.log   是存储的数据
00000.timeindex 是时间戳索引文件(是__consumer_offsets中记录着的committimestamp)对应的.index中的offset

.index .log .timeindex 的文件名是以他们中的第一条消息的offset命名的,文件内的offset还是以0开始,在查找的使用的是二分查找法查找文件名,文件中的offset=文件名中的offset值+文件内的相对offset值
.timeindex中的offset是和.index中一一对应的

.index .log .timeindex三个文件为一组,称为segment
当以下情况之一,会发生分片
1、当 .log文件大小超过1G(broker端配置的log.segment.bytes 默认1073741824,即1GB)

2、当 .log文件中的offset的大小超过了int的最大值时
	(追加的消息的偏移量与当前日志分段的起始偏移量之间的差值大于Integer.MAX_VALUE, 即要追加的消息的偏移量不能转变为相对偏移量offset - baseOffset > Integer.MAX_VALUE)
	
3、当 .log文件的第一条数据存活的时间达到了七天(会删除,全部清空,即数据清空)
	(最小时间戳与当前系统的时间戳的差值大于log.roll.ms或log.roll.hours参数配置的值。如果同时配置了log.roll.ms和log.roll.hours 参数,那么 log.roll.ms 的优先级高,默认情况下,只配置了log.roll.hours参数,其值为168,即7天)

4、当 .index 或 .timeindex文件大小达到了10MB
消息message的存储结构
客户端代码中,消息的封装有两种 ProducerRecode ConsumerRecode
即kafka 的每个message是由一对对k-v构成
应该包含
crc 消息校验码 4字节
key length
key
value length
value

attributes 消息压缩所使用的的编码,timestamp

Controller

Controller是kafka的核心组件,作用是帮助zookeeper管理和协调整个kafka集群
它负责维护整个集群中所有分区和副本的状态及分区leader的选举

controller与zookeeper交互,获取与更新集群中的元数据信息,其他broker不直接与zookeeper进行通信,而是通过与Controller进行通信并同步Controller中的元数据信息

kafka中的每个broker都可以当Controller,但是只能有一个Controller,遵循的是先先到zookeeper中注册,谁就是Controller

脑裂问题:当一个controller节点宕机之后,会立刻有新的节点注册替代之前的controller,但是之前的controller恢复之后并不知道自己已经被替代,这样集群中相当于有两个controller,这时就看纪元号,谁的纪元号大谁就是现在的controller


分区的负载分布
客户端请求创建topic时,每个分区副本在那个broker上的分配,是由Controller进行决定的
在分区负载逻辑中,会产生两个随机数,
第一个随机数确定0号分区的leader的位置,后续分区的leader依次顺延+1
第二个随机数确定每个分区第一个副本的位置,在leader的broker上往后顺延(随机数+1)
       此台broker就是第一个分区副本的位置,其他分区副本依次+1
       
手动分配:
创建topic的时候,使用--replica-assignment  0:1:2,1:2:3,2:3:4 逗号隔开,第一个代表leader在哪个broker上,后面代表副本在哪个broker上,如果指定的机器不存在,那么就先标记,等检测到有集群出现,就创建副本,否则不创建副本,kafka没有副本补全机制

分区leader的选举机制
分区leader的副本选举由Controller负责具体实施

当创建分区(创建主题或者增加分区,即有创建分区的动作)或者leader下线(此时分区需要选举一个新的leader上线来对外提供服务)的时候,都需要执行leader的选举机制

选举策略 按照ISR集合中副本的顺序查找第一个存活的副本,并且这个副本在ISR集合中

一个分区的ar集合在partition分配的时候就被指定了,并且只要不发生重分配的情况,集合内副本的顺序是保持不变的,二分区的ISR集合中的副本顺序可能会改变

kafka如何保持数据一致性

谈论kafka保持数据一致性需要从三个方面来进行分析
生产者:kafka具有幂等性,有事务,设置acks=-1,isr最小同步副本数大于2
broker:副本机制(生产者和消费者只对接leader),落地磁盘,并且还有hw(高水位线)leader-epoch保证数据一致性
消费者:幂等性、事务(kafka如何精准一次性消费)

kafka具有幂等性

一个操作重复做,也不会影响最终的结果,是解决数据的乱序和重复发送问题的

会针对每一条数据除了offset以外,还会生成一个producer_id,并为每一个目标分区维护一个"消息序列号"
在分区中,记录这条消息的producer_id,并拿着上一条消息的producer_id + 1 等于这条数据的producer_id的话,代表这条数据没有问题
如果新数据的producer_id小于上一条数据的producer_id + 1,说明是重复写入的数据,直接丢弃
如果新数据的producer_id大于上一条数据的producer_id + 1,说明是有数据尚未写入,或者产生了乱序,或者数据丢失,会抛出严重常:OutOfOrderSequenceException

开启幂等性:
props.put("enable.idempotence",true);
在开启幂等性功能时,如下几个参数必须正确配置:
retries > 0    max.in.flight.requests.per.connection<=5     acks = -1
如果不设置会抛出ConfigException异常

kafka如何实现精准一次性消费

kafka并不是在每一个场景下都可以保证精准一次性消费的(消费者)

kafak的精准一次性消费,与一个topic有关__consumer_offsets有关,
这个topic记录着消费者组消费topic的具体情况,在代码中设置"enable.auto.commit"="false",
如果设置为true,默认每隔5s就会提交一次消费者的消费情况到__consumer_offsets中,
如果设置为自动提交,当提交记录发生在业务逻辑处理之前,假设这时候业务逻辑出现问题,就会导致,丢失消费数据的情况,如果提交offset,在业务逻辑处理之后,此时业务逻辑已经完成落盘,但offset还没有进行提交,此时程序宕机了,在此启动程序时,会接上上一次的消费记录进行消费,即重复消费数据,
在这种情况下,我们最好是将"enable.auto.commit"="false"。
为了我们能够紧着着之前的消费记录进行消费,我们需要找到一个容器,将我们的消费记录仿照这__consumer_offsets进行存储,
这需要看我们具体的业务逻辑,加入我们的业务逻辑是将消费到的数据落库到mysql数据库当中,我们最好可以利用mysql事务的特征,在mysql数据库中创建一张提交偏移量的表,将我们的业务处理逻辑,和提交偏移量绑定成一个事务,要么都成功,要么都失败,这样是可以实现精准一次性消费的。
但如果业务数据不是写在mysql当中,而是写在redis当中,虽然redis中也具备事务,但他的事务是一个伪事务,不能够做到要么都成功,要么都失败,但是他也是可以进行精准一次性消费的,因为redis具备幂等性,那这样我们可以通过手动提交的方式,在业务从逻辑处理之后进行提交(consumer.commitAsync();/consumer.commitSync();),在这种情况下只可能会重复读取数据,但是由于redis的幂等性,即使我重复读取数据,最终也只会保留下来一条记录。
如果是将消费到的topic消息保存到了文件、hdfs等一些既不支持事务又不支持幂等性的场所,那么这个时候是没有办法保证一次性精准消费的。

kafka的acks应答机制

副本和同步副本的区别
副本是在创建partition之初就已经规定好了,数量以及在那个broker上的存放这就已经规定好了,而同步副本并不是,
irs[] 中存放这存活的partition副本的位置,他的数量和排列顺序是会发生改变的,当有broker宕机,当经过30s之后,irs就会将该broker上的副本移出,此时irs同步副本的数量将减少,等到broker活过来了,向leader请求同步数据,当该broker上的partition中的数据信息同步到最新的位置时,将把他重新加入到irs同步副本当中

同步副本又称为ISR,follower通过保持心跳通信来同步数据,和leader保持心跳通信的follower就叫做同步副本,如果30秒没有向leader报告,就会被认为掉线,被踢出ISR同步副本列表,即使30s内保持心跳了,数据没有同步完成,也不会拉到同步副本中

acks:kafka集群的应答机制,一共有三个值 0 ,1 ,-1(all)
如果是0,那么消息发出去就不管了,不管接没接收到,都继续发送,这样效率最高,但是可能会丢数据
如果是1,会把数据发给leader,等leader数据落地磁盘之后会响应,不等他同步其他的同步副本,就直接发送下一条,相对比较均衡,但是如果leader宕机也有可能会丢数据,概率比较小
如果是-1,会等leader数据落地磁盘,并把所有的数据都同步给同步副本,并且所有的同步副本中的数据都落地磁盘之后,才会响应,继续发送数据,这样效率最低,数据最安全,但是如果同步副本数只有一个,也会有数据安全问题,最少配置两个同步副本,可以修改服务端的一个参数(分区最小ISR数[min.insync.replicas]>=2),来避免此问题,如果副本数低于设置值,kafka集群会直接报错.

max.requset.size
生产者发送一条消息,消息的最大值是1MB,还涉及一些其他参数的联动,broker端的message.max.bytes参数,默认10b,连用时,消息最大值是以最小参数值为准

retries和retry.backoff.ms
重试次数和每次重试的间隔时间,避免无效的频繁的重试

max.in.flight.requests.per.connection
设置飞行中的请求的每一个节点上的requset同时存在的个数.

kafka的CAP保证

cap 作为 分布式 的基础理论
一致性 consistency
可用性 available
副本容错性 partition tolerance

这三者在分布式系统中不能够很好的满足,最多满足其中两个特性,kafka在满足ap的同时兼顾c

分区容错性:指的分布式系统中的某个节点或者网络分区出现了故障的时候,整个系统仍然能对外提供满足一致性和可用性的服务。也就是说部分故障不影响整体使用。事实上我们在设计分布式系统时都会考虑到bug,硬件,网络等各种原因造成的故障,所以即使部分节点或者网络出现故障,我们要求整个系统还是要继续使用的(不继续使用,相当于只有一个分区,那么也就没有后续的一致性和可用性了)

可用性:一直可以正常的做读写操作。即客户端一直可以正常访问并得到系统的正常响应。用户角度来看就是不会出现系统操作失败或者访问超时等问题。

一致性:在分布式系统完成某写操作后任何读操作,都应该获取到该写操作写入的那个最新的值。相当于要求分布式系统中的各节点时时刻刻保持数据的一致性。

kafka生产者原理

分为了两个线程操作,主线程和sender线程,首先创建了生产者对象(KafkaProducer),数据发送的时候要先经过拦截器,按照条件过滤数据,再经过序列化器,将发送的数据进行序列化,最后经过分区器,决定这条数据应该落到哪个分区,这个分区器是逻辑上的分区,给数据打上标记,再放入消息累加器中,kafka的数据是一批一批的发送的,由消息累加器控制,对数据进行分类,同一个分区的数据放在一起打包成producerBtach,打包分批发送,sender线程过来接收数据,在sender线程中将producerBatch转变成request对象,是从逻辑到物理的转换,把request对象放进飞行中的请求(InFilghtRequestS)中,在飞行中请求中确定每个requset需要发往哪台机器上,遵循队列原则,先进先出,提交给selector准备发送,将requset发送到kafka集群上,kafka集群返回消息,如果发送成功,可以继续发送下一条,飞行中的请求中的requset就会从内存中被清理掉,如果发送失败,selector会重新再发送一遍(重试策略).

同一个节点上如果上一条数据发送失败,集群返回重新发送,下一条数据已经发送成功了,那么数据会出现乱序的情况,可以规定一个节点一次只能存在一个requset,如果成功,应答之后才能继续发送下一个,如果失败那么就重新发送,重新发送的途中不允许有另外的requset发送,但是这样会影响集群的吞吐量
重写分区器
// 实现接口 implements org.apache.kafka.clients.producer.Partitioner
// 重写方法 partition
    

 // 实现拦截器
 // 实现接口 implements org.apache.kafka.clients.producer.ProducerInterceptor<String,String>
 //   <String,String> kv的泛型
    
 // 重写方法 onSend

kafka的事务

是伪事务,不支持回滚,但是能做到要么都成功,要么都失败
kafka中的数据时以日志追加的形式出现的,往里面写了数据,数据就存在于文件中,log文件不能修改.
开启事务 --> 提交事务 --> 如果错误需要回滚事务,kafka不支持回滚事务,支持放弃事务,给错误事务一个标志,然后再次执行事务,这样错误的数据和成功的数据都会被存储在kafka中,只展示正确的数据,能否读到错误的数据,由一个参数控制
isolation.level=read_uncommitted(默认值)   --隔离级别,读未提交,读错误的数据
isolation.level=read_committed    --隔离级别,读已提交,读正确的数据
props.setProperty(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");//开启事务
props.setProperty(ProducerConfig.ACKS_CONFIG, "-1");//acks应答机制
props.setProperty(ProducerConfig.RETRIES_CONFIG, "3");//
props.setProperty(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "3");
props.setProperty(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "t001");//事务id
props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "32000");//batch大小 单位kb
props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "20");


/**
        - retries > 0 默认值  2147483647  3
        - max.in.flight.requests.per.connection<=5  Default:	5
        - acks = -1   Default:	all    老版本是1
        */


/**
		//初始化事务
        producer.initTransactions();

        //开启事务
        producer.beginTransaction();

        //提交事务
        producer.commitTransaction();

        //放弃事务
        producer.abortTransaction();

*/

kafka采用的是顺序写,比随机写省去了寻址时间

读已提交,读正确的数据


```java
props.setProperty(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");//开启事务
props.setProperty(ProducerConfig.ACKS_CONFIG, "-1");//acks应答机制
props.setProperty(ProducerConfig.RETRIES_CONFIG, "3");//
props.setProperty(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, "3");
props.setProperty(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "t001");//事务id
props.setProperty(ProducerConfig.BATCH_SIZE_CONFIG, "32000");//batch大小 单位kb
props.setProperty(ProducerConfig.LINGER_MS_CONFIG, "20");


/**
        - retries > 0 默认值  2147483647  3
        - max.in.flight.requests.per.connection<=5  Default:	5
        - acks = -1   Default:	all    老版本是1
        */


/**
		//初始化事务
        producer.initTransactions();

        //开启事务
        producer.beginTransaction();

        //提交事务
        producer.commitTransaction();

        //放弃事务
        producer.abortTransaction();

*/

kafka采用的是顺序写,比随机写省去了寻址时间
  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值