Kafka快速入门
Kafka快速入门
1、MQ简介
生产者向队列发送消息,消费者从队列消费消息,先进先出属于队列类型。常用的消息中间件有RabbitMQ、RocketMQ、kafka等,使用消息中间件目的是:程序解耦,异步(请求同步要求前面的执行完成后才能执行)、数据削峰(短时间大量的请求访问同一个URL,服务器短时间内要处理大量请求有可能宕机,将用户的请求先存放到一个消息队列中,然后根据服务器的处理能力依次处理用户请求),大数据计算(将生产者产生的数据快速发送消费者,而且生成者有可能有很多人,消费者有可能也有很多人,消费中间件也是多台服务器)。
缺点:系统可用性降低:依赖服务越多,需要考虑MQ宕机。
业务复杂性提高:需要考虑消息丢失、重复消费、传递顺序。
业务一致性:主业务和从业务一致性处理。
- 异步处理
- 应用解耦
- 流量削峰
2、消息系统的原理
关联到业务数据:这条消息必须关联到某些业务,这条消息必须有存在的意义和价值。
完整性:在数据传输过程中,保证数据单元的完整,要么传输成功要么传输失败。
独立性:各个单元之间没有相互依赖,被分割的消息有可能被同一个消费者所消费。
颗粒度:粒度太小传输次数变多,集群压力大;粒度太大传输次数变少,数据有可能有很大的延迟性。
- 点对点消息传递
在点对点消息系统中,队列中的消息不能重复被消费,也就是说消费被消费完了,会立即从队列中删除。
如果有多个消费者进行消费时,也要保证消费的顺序,一个消费者只能消费一种消息。
- 发布订阅消息传递
发布和订阅消息中,队列中的消息不会在被消费后立即删除,因此队列中的消息可以被多个消费者消费。
在订阅模式下,推送消息的叫发布者,拉取消息的叫订阅者!
kafka采取poll模型,由自己控制消费速度,消费者可以按照任意偏移量进行消费。
在订阅模式下,当发布者发布消息速度大于订阅者消费消息时,会造成消息在队列中积压,因此需要设置消费者组来对发布消息进行消息消费!
- 消费传递对比
3、Kafka框架介绍
- 简介
kafka诞生于领英公司,于2011年初开源,并于2012年10月23日由apache孵化出站。kafka最初诞生是为了解决Linkedin数据管道问题。由java和scala编写的。是一种高吞吐量的分布式发布订阅消息系统,可以处理消费者在网站中的所有动作流数据。
kafka本质是一个MQ,特点:系统程序解耦、可恢复性、缓冲、削峰处理、异步通信
设计目标:降低时间复杂度、对TB级以上数据也能保证常数时间访问性能;高吞吐量、支持kafka server间的消息分区,及分布式消费,同时保证每个partition内消息的顺序传输;同时支持离线和实时处理;支持在线水平扩展。
4、kafka系统架构
生产者push消息到topic中,由于topic在不同partition中,每个分区在不同的broker中,多个broker组成kafka集群,因此生产者把消息push到kafka集群中,由于需要实现kafka高可用,需要在不同分区创建replication(leader+fellow),也就是leader和fellow不在同一个broker,最后消费者从每个分区的leader中拉取数据进行消费。
其中zookeeper管理broker、注册topic、维护offset、生产者负载均衡,在多个broker中选举出controller broker。kafka0.9版本后offset存储在kafka本地,kafka2.8之后可以脱离zookeeper工作依赖kafka自身kRaft实现选主。
- Broker
每个kafka server成为一个Broker,多个borker组成kafka集群。
一个broker可以维护多个topic。
- controller
在kafka集群中,只有一个broker可以成为Controller,行使管理和协调职责,它除了具有broker本身工作还具有Broker管理(新增Broker、Broker主动关闭、Broker故障)、topic管理(创建主题、删除主题)、Partitions管理(leader分区选举、增加分区、Rebalance分区),kafka集群中始终只有一个controller Broker。
zk选举controller机制:每个broker都会尝试在zk注册临时节点竞选控制器,第一个创建/controller节点的Broker会被指定为controller。竞争者失败的节点会继续使用观察者模式监听controller broker。如果controller宕机了,那么其他Broker会继续竞争选举出controller broker。
- Message
Message消息。通过Kafka集群进行传递的消息对象实体,存储需要传送的信息。
Message是实际发送和订阅的消息的实际载体,生产者发送到kafka集群中数据都被封装成一个个Message对象,之后存储在磁盘中。
其中 key 和 value 存储的是实际的 Message 内容,长度不固定,而其他都是对 Message 内容的统计和描述,长度固定,也就是Message数据的元数据。
实际在查找过程中,磁盘指针会根据Message的offset和Message length计算移动位数,加速Message的查找过程,因此kafka日志文件都是顺序写的,往磁盘上写数据时,就是追加数据,没有随机写的操作!
- Topic
每条发送到kafka集群的消息都有一个类别,这个类别就是topic,就像数据库的table和es的index。
一种topic消息保存在一个或多个broker上,用户只需要指定消息的topic即可,producer将消息推送到topic,订阅该topic的消费者拉取消息即可。
创建流程:controller在zk中topic节点注册watch,当topic创建好,controller监听topic中Partition中副本分配,从分配好的replication中任选一个可用broker作为leader,并将所有replication设置为新的ISR,将新的leader和ISR写入/brokers/topics/主题/partitions/n/state中,controller通过RPC向相关的Broker发送LeaderAndISRRequest。
删除流程:controller在zk的topics节点上注册watch,当topic被删除,则controller会通过watch得到该topic的副本分配。若设置关闭删除topic配置就结束,否则controller注册在/admin/delete_topics上的watch被fire,controller通过回调向对应broker发送StopReplicaRequest。
- partition
kafka中topic被分多个Partitions分区,每个topic至少有一个partition。多分区可以提高topic的处理效率。
topic是一个逻辑概念,partition是最小的存储单元和并行度,拥有topic部分数据。
每个partition都是一个单独的log文件,每条记录都以追加形式写入。
当生产者产生数据时候,根据分配策略选择分区,然后将消息追加到指定的分区末尾。
- 分区分配策略
- 指定partitions则直接使用。
- 未指定partition但指定key,通过对key取hash选出partition。
- partition和key都未指定使用轮询方式选出分区。
每条在分区中的消息都有一个自增的编号(offset偏移量),它表示每条消息的位置信息,是一个单调递增且不变的值,通俗点讲offset可以用来唯一标识分区的每一条记录。
每个分区都有一个唯一的编号;分区内的数据是有序,分区外数据是无序的,如果topic有多个分区,消费数据时不能保证数据的顺序,如果需要严格保证消费顺序需要将分区数设置为1。
- offset
消息写入时,需要查找分区内的offset,这个offset是生产者的offset,同时也是这个分区最新最大的offset。也就是说我们在写入数据的时候,要实现断点续传!
某一个业务通过修改偏移量达到重新读取消息的目的,偏移量由用户控制。其中不会有线程安全,消息被消费后,不会马上被删除。这样可以让多个业务重复使用kafka,但是最终消息会在默认一周(168小时)后被删除!
- Replication
数据会存放在topic的partition中,partition会存放到Broker上,但是有可能会因为Broker损坏导致数据丢失。
kafka为一个partition生成多个副本,并且把它们分散到不同的Broker上。
如果一个Broker故障了,Consumer可以在其他Broker上找到partition的副本,继续获取信息。
我们将分区分为leader和follower:leader负责写入和读取数据;follower负责备份;保证了数据的一致性!
- kafka分配replication的算法如下
- 将所有broker(假设共n个Broker)和待分配的partition排序
- 将第i个partition分配到第(i%n)个broker上。
- 将第i个partition的第j个副本分配到第(i+j)% n个broker上
- Replication数不能大于Broker数,否则无意义!
- Leader: 每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的 partition。
- Follower: Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower, Follower与Leader保持数据同步。
- Producer
生产者数据的发布者,该角色将消息发布到Kafka的topic中。Broker接收到生产者发送的消息后,Broker会将该消息追加到数据的Segment(topic在分区中的数据存储结构)文件中!
- Consumer
kafka消息者消费消息与其他消息队列不一样,它需要消费者自己从topic拉取消息。根据partition偏移量依次读取消息,读完一条消息后,消费者会推进parttition中下一个offset,继续读取消息!
- consumer group
为了防止消费者消费能力跟不上,我们使用消费者组进行消费,消费者组有多个消费者,每一消费者都有指定的group ID,整个消费者组共享全局偏移量(一个topic有多个分区,每个分区都有自己的偏移量),防止数据重复读取。
- Zookeeper
kafka集群使用zookeeper负责集群元数据管理、控制器选择等操作。
在每个Broker启动时候,都会和Zookeeper进行交互,这样zookeeper就存储了集群中所有的主题、配置、副本。
zookeeper常见节点:Broker注册、Topic注册、生产者负载均衡、offset维护
5、kafka环境安装
前提:已经安装好zookeeper
- 配置kafka
上传kafka-0.11.2.tgz文件到服务器中,并解压到指定的目录中
tar -zxvf kafka-0.11.2.tgz -C /usr/local/kafka/
配置${KAFKA_HOME}/config/server.properties
#每个节点id值不同
broker.id=0
#每个节点IP不同
listeners=PLAINTEXT://192.168.147.110:9092
#存储数据文件的目录 需要先创建好相关目录 mkdir -p /var/kafka/kafka-logs
log.dirs=/var/kafka/kafka-logs
#配置zk及存放元数据节点目录
zookeeper.connect=node1:2181,master:2181,node2:2181/kafka0110
把配置好的kafka,分发到其他节点,只需要改一下配置
#每个节点id值不同
broker.id=[节点自定义id值]
#每个节点IP不同
listeners=PLAINTEXT://[节点IP]:9092
配置环境变 /etc/profile (三台节点都要配置)
export KAFKA_HOME=/usr/local/kafka/kafka_2.12-0.11.0.3
export PATH=${KAFKA_HOME}/bin:$PATH
执行source /etc/profile 命令配置生效
- 启动kafka集群
#控制台启动
kafka-server-start.sh /usr/local/kafka/kafka_2.12-0.11.0.3/config/server.properties
#后台启动
kafka-server-start.sh --daemon /usr/local/kafka/kafka_2.12-0.11.0.3/config/server.properties
-
常见命令
- 创建主题
kafka-topics.sh --zookeeper node01:2181/kafka0110 --create --replication-factor 2 --partitions 3 --topic userlog
- 查看所有主题
kafka-topics.sh --zookeeper node01:2181/kafka0110 --list
- 创建生成者
kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic userlog
- 创建消费者
kafka-console-consumer.sh --zookeeper node01:2181/kafka0110 --from beginning --topic userlog
- 指定消费者组
kafka-console-consumer.sh --bootstrap-server node01:9092 --topic userlog -- consumer-property group.id=yjx
- 删除主题
提前在配置文件server.properties增加设置,默认未开启 delete.topic.enable=true
- 删除主题命令
kafka-topics --delete --topic userlog --zookeeper node01:2181,node02:2181,node03:2181/kafka0110
6、kafka数据存储
- 文件存储结构
topic在物理层面以partition为分组,一个topic可以分成若干个partition,每个partition对应一个文件夹。partition文件夹命名规则为:topic名称+分区序号
为了防止log文件过大导致数据检索效率低下,将每个partition分为多个Segment(默认是1G文件为一个sement或者7天生成一个Segment)。
- Segment文件由三部分组成,分别为".index"文件、".log"文件、”.timeindex“文件
- partition全局的第一个Segment从0开始,后续每个Segment文件名为当前Segment文件第一条消息的offset值。
- 数值大小为64位,20位数字字符长度,没有数字用0填充
优点:加快查询效率、删除数据时减少IO读写
- 问题:为什么不直接将offset作为索引第一列,而用相对偏移量作为第一列?
- 直接offset作为索引第一列,随着offset越来越大,索引变得非常大,查询效率会降低。
- 通过计算相对偏移量,可以在数据量大的情况下,节省索引的空间,提高查询效率。
- 索引与数据
索引分为稠密索引和稀疏索引,所谓稠密索引就是每个偏移量建立一个索引,而所谓稀疏索引就是跨越多个偏移量建立一个索引。
kafka选择的就是稀疏索引,.log文件每增加4kb,index文件中增加一条索引记录!
- 查看index文件命令
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.index --print-data-log
- 查看timestamp文件命令
kafka-run-class.sh kafka.tools.DumpLogSegments --files 00000000000000000000.timeindex --
print-data-log
注意:理论上index和ts文件中的offset是每块4kb数据大小的起始offset大小。通俗点说就是每4KB数据记一次(offset=0,1,2,3,4...记录)。而实际开发是消息批处理,只记录消息批处理最后一个偏移量(offset=41,82,123,164...记录)。
- .index位移索引(理论)
- 实际index文件记录图
- .ts时间戳索引
- .snapshot文件,记录producer的事务信息。
- 数据检索
步骤一、根据offset计算这条offset是这个文件中的第几条,也就是索引文件中第一列:offset=要查找的全局offset — 当前Segment文件第一个全局offset(log文件名上的序号)
步骤二、读取.index索引文件,根据二分检索,从索引中找到离这条数据最近偏小的位置;
步骤三、读取.log 文件从最近位置读取到要查找的数据;这样将要查找的数据范围从1G缩小到4K。
- 文件清除策略
一、设置过期时间:默认是保存7天,最大时间戳超过7天就删除;给需要删除的文件加上
.delete
。最后由专门的定时任务删除.delete
后缀文件;通过设置以下参数配置删除(server.properties文件)
二、选择清除策略:delete删除或者compact压缩,默认是删除。
- delete删除
当不活跃的segment的时间戳是大于设置的时间的时候,当前segment就会被删除
log.cleanup.policy = delete
- compact压缩
- og.cleanup.policy = compact
- 对于相同key的不同value值,只保留最后一个版本。
三、确认清楚时机
- Kafka的segment数据段清除不是及时的,他更像JVM垃圾回收那样,先打上deleted清除标 记,在下一次清除的时候一起回收。
- 删除时从,默认强制把偏移量移动到最近一个起始位置开始消费。
- 文件存储问题
如果index和timeindex触发时机都是4K,那么timeindex和index的偏移量是否会完美契合?
理论上两者应该是完美契合的;如果有差别应该在segment正常roll或者清除的时候添加一条timeindex;
7、kafka生产消费API
- 导入pom.xml文件
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>0.11.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.3</version>
</dependency>
- 创建kafka工具类
package com.zwf.kafkaApi;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import java.util.Properties;
/**
* @author MrZeng
* @version 1.0
* @date 2023-12-21 16:07
*/
//kafka连接工具类
public class KafkaUtils {
//创建生产者
public static KafkaProducer<String,String> createProducer(){
//创建配置文件列表
Properties properties=new Properties();
//kafka地址,多个用逗号隔开
properties.setProperty("bootstrap.servers","node1:9092,master:9092,node2:9092");
//设置写出数据格式
properties.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
//应答方式
properties.put("acks","all");
//错误重试次数
properties.put("retries",1);
//批量写出 16KB为一批数据
properties.put("batch.size",1024*16);
//创建生产者
KafkaProducer<String,String> kafkaProducer=new KafkaProducer<>(properties);
return kafkaProducer;
}
//创建消费者
public static KafkaConsumer<String,String> createConsumer(String groupId){
//读取配置文件
Properties properties=new Properties();
properties.put("bootstrap.servers","node1:9092,master:9092,node2:9092");
//设置groupId
properties.put("group.id",groupId);
//zk超时时间
properties.put("zookeeper.session,timeout.ms","1000");
//反序列化
properties.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
//当消费者第一次消费时,从最低的偏移量开始消费
properties.put("auto.offset.reset","earliest");
//设置自动提交
properties.put("auto.commit.enable","true");
//消费者自动提交偏移量的时间间隔 1s
properties.put("auto.commit.interval.ms","1000");
//创建消费者对象
KafkaConsumer<String,String> kafkaConsumer=new KafkaConsumer<>(properties);
//返回消费对象
return kafkaConsumer;
}
}
- kafkaProducer.java
package com.zwf.kafkaApi;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
/**
* @author MrZeng
* @version 1.0
* @date 2023-12-21 16:06
*/
public class Producer {
//生产者
public static void main(String args[]){
//创建生产者对象
KafkaProducer<String, String> producer = KafkaUtils.createProducer();
//开始生产数据
new Thread(()->{
System.out.println("生产者开始发送数据……");
for (int i=0;i<1000;i++){
//创建Message
String key="test";
String value="page:"+i+".jsp,tt:"+System.currentTimeMillis();
// 指定分区数/hash(key)/轮询 本次使用轮询分发到不同分区
ProducerRecord<String,String> record=new ProducerRecord<>("accessLog",value);
//发送消息到kafka
producer.send(record);
//稍微停滞一下
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
}
- kafkaConsumer.java
package com.zwf.kafkaApi;
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.common.TopicPartition;
import java.util.Arrays;
import java.util.List;
/**
* @author MrZeng
* @version 1.0
* @date 2023-12-21 16:07
*/
public class Consumer {
public static void main(String[] args) {
//创建消费者
KafkaConsumer<String,String> consumer=KafkaUtils.createConsumer("bigData");
//订阅主题
consumer.subscribe(Arrays.asList("accessLog"));
while (true){
//每100ms拉取一条记录
ConsumerRecords<String, String> records = consumer.poll(100);
//获取每个分区中的记录
for (TopicPartition partition:records.partitions()){
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String,String> record:partitionRecords){
System.out.println(record.topic()+":"+record.partition()+":"+record.offset()+":"+record.key()+":"+record.value());
}
//同步执行 同步拉取每个分区的数据
consumer.commitAsync();
}
}
}
}
8、kafka生产者
- 数据发送流程图

架构流程:main线程KafkaProducer创建消息对象ProducerRecord,然后通过生产者拦截器。生产者拦截器对消息的key和value做一定处理,交给序列化器。序列化器对message中的key和value做序列化处理,然后交给分区器,分区器给消息分配分区,并发送给消息收集器;sender线程获取消息收集器中的消息,将原本<分区,Deque>形式再次封装成<Node,List>形式,其中Node表示集群中Broker节点。对于网络连接,生产者客户端是与具体Broker节点建立连接,也就是向具体的Broker节点发送消息。不关心消息属于哪个分区,Sender进一步将Message封装成<Node,request>形式,最后发往各个节点。sender将Request交给Selector准备发送,Selector将Request发送到对应kafka节点(Broker)Selector将结果反馈给InFlightRequest,主线程清理消息累加器已经发送完毕的数据。
- 消息累加器
main线程调用Send()方法将已经和topic绑定好的消息,以分区为单位维护一个双端队列,将消息缓存起来。当达到一定的条件,会唤醒Sender线程发送RecordAccumulator里面的消息。
累加器相关参数:


- 数据分区发送
分区优点:水平拓展、消息顺序读取、提高并行度、负载均衡
早期版本:将生产者数据封装成ProducerRecord对象,三种方式:指定分区、指定key hash值对分区数取模、单条数据轮询。(使用DefaultPartitioner类)
高版本:指定分区就去分区;指定key情况下使用key hash值对分区数取模;粘性分区(一个数据批,直到数据批塞满16KB消息时再发送到不同分区。)
- 自定义分区器
使用Partition接口,重写partition方法,配置生产者的分区器。
- 代码实现、
创建主题
kafka-topics.sh --zookeeper node1:2181,master:2181,node2:2181/kafka0110 \
--topic test-custom-partition \
--create --partitions 5 \
--replication-factor 2
指定5个分区消费者
kafka-console-consumer.sh --bootstrap-server node1:9092,master:9092,node2:9092 --topic test-custom-partition --partition 0
kafka-console-consumer.sh --bootstrap-server node1:9092,master:9092,node2:9092 --topic test-custom-partition --partition 1
kafka-console-consumer.sh --bootstrap-server node1:9092,master:9092,node2:9092 --topic test-custom-partition --partition 2
kafka-console-consumer.sh --bootstrap-server node1:9092,master:9092,node2:9092 --topic test-custom-partition --partition 3
kafka-console-consumer.sh --bootstrap-server node1:9092,master:9092,node2:9092 --topic test-custom-partition --partition 4
编写java代码
- 枚举类
package com.zwf.kafka_partition;
public enum CustomEnum {
// 订单
ORDER("0", "订单"),
// 支付
PAYMENT("1", "支付"),
// 商品
COMMODITY("2", "商品"),
// 库存
STOCK("3", "库存"),
// 其他
OTHER("4", "其他");
private String code;
private String desc;
CustomEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
- 自定义分区类
package com.zwf.kafka_partition;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author MrZeng
* @version 1.0
* @date 2023-12-23 21:38
* 自定义分区
*/
public class CustomPartitioner implements Partitioner {
/**
*
* @param topic 主题名
* @param key 分区编号
* @param bytes
* @param o1
* @param bytes1
* @param cluster 集群
* @return
*/
@Override
public int partition(String topic, Object key, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
int num = partitionInfos.size();
if(Objects.equals(CustomEnum.ORDER.getCode(),key)){
return 0;
}
if(Objects.equals(CustomEnum.PAYMENT.getCode(),key)){
return 1;
}
if(Objects.equals(CustomEnum.COMMODITY.getCode(),key)){
return 2;
}
if(Objects.equals(CustomEnum.STOCK.getCode(),key)){
return 3;
}
return num-1;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
- 测试分区类
package com.zwf.kafka_partition;
import com.zwf.kafkaApi.KafkaUtils;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
/**
* @author MrZeng
* @version 1.0
* @date 2023-12-23 21:46
*/
public class TestCustomPartitionProducer {
public static void main(String[] args) {
//主题名
String topic_name="test-custom-partition";
//创建配置文件列表
Properties properties=new Properties();
//kafka地址,多个用逗号隔开
properties.setProperty("bootstrap.servers","node1:9092,master:9092,node2:9092");
//设置写出数据格式
properties.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
//应答方式
properties.put("acks","all");
//使用自定义分区器*****************************************
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomPartitioner.class);
//错误重试次数
properties.put("retries",1);
//批量写出 16KB为一批数据
properties.put("batch.size",1024*16);
//创建生产者
KafkaProducer<String,String> producer=new KafkaProducer<>(properties);
List<String> keys = Arrays.asList("0", "1", "2", "3", "4");
List<String> messages = Arrays.asList(
"这是一个订单消息",
"这是一个支付消息",
"这是一个商品消息",
"这是一个库存消息",
"这是一个其他消息"
);
for (int i=0;i<keys.size();i++){
String key = keys.get(i);
ProducerRecord<String, String> record = new ProducerRecord<>(topic_name, key, messages.get(i));
//发送消息
producer.send(record,((metadata, e) -> {
if (e == null) {
System.out.format("消费发送成功:主题:%s,分区:%s\n",
metadata.topic(), metadata.partition());
} else {
System.out.println("消息发送失败:" + e.getMessage());
}
}));
}
producer.close();
}
}
9、集群架构定义
- AR、ISR、OSR定义
AR(Assigned Replicas):全部副本成员。
ISR(In sync Replicas):加入同步队列的副本,leader也属于ISR中的一部分。
OSR(Out Sync Replicas):离开同步队列的副本,即使副本在ISR中也有可能和Leader的数据也是不同。
判定ISR和OSR标准:ISR中的follow默认10s内没有向leader发送心跳包就会被移除到OSR,如果再次发出心跳就从OSR恢复到ISR中。(老版本除了没发心跳包判定,还有ISR中follow与leader数据相差4000条被移除!)
- LEO HW定义
LEO(Log end offset):因为leader和follower的偏移量是不同的,代表下一条待写入数据的偏移量,leader和每个follower有可能有不同的LEO。
HW(high watermark):高水位线是通过LEO计算出来的,基于leader的LEO和所有ISR中副本的LEO取所有的LEO最小值。HW决定消费者可以最大读取的偏移量。为了防止最大偏移量(LEO)节点宕机了,其他follow都小于宕机节点的偏移量(LEO)无法读取数据而产生的最高水位线概念!
- CheckPoint
Recovery checkPoint:数据已经落盘的偏移量(offset)。
replication-offset-checkpoint: 用来存储每个replica的HW,表示已经被commited的offset消息。
log-start-offset-checkpoint: 标识日志的起始偏移量。
cleaner-offset-checkpoint: 存了每个log最后清理的offset。
-
replication故障处理
-
leader故障后,优先从ISR中选出一个新的leader,为保证副本一致性,竞选失败的Follower会将各自的log文件高于HW部分数据截掉,然后从新leader中同步数据。这只能保证数据一致性,并不能保证数据完整性!
- 流程:Leader节点宕机后,会从ISR列表中移除,然后controller broker从ISR中重新选举出新的leader,然后所有ISR的Follower要删除当前水位线以上的数据,与新的leader同步数据,宕机后的leader节点恢复后进入OSR列表,然后删除高水位线以上的数据,与新的leader同步数据,最后加入ISR列表中!
-
Follower故障后,Follower会被临时踢出ISR列表中到OSR列表中,这个期间Leader和Follower继续接收数据,也有可能发生Leader选举等事件,待Follower恢复后,Follower会读取本地磁盘记录的上次的HW,并将日志文件大于等于HW的部分截取掉,从HW重新向Leader进行同步,等待Follower的LEO大于等于该Partition的HW,即Follower追上Leader之后,就可以重新加入ISR
-
-
Unclean Leader Election
如果ISR中所有replication都宕机了,所有replication都进入OSR中,称为非同步副本,这时候包括Leader也宕机了,需要重新选举新的Leader,由于OSR中的副本数据大大落后于老的Leader数据,这时候从OSR中选举出新的Leader,就叫脏数据主节点选举;
kafka默认关闭这种机制,如果开启这种机制会造成数据不一致,但是提高了集群的高可用,对外还能提供服务!符合CP原则,一般不建议开启,为了数据高可用性牺牲数据一致性不值当!
- 数据发送保证
- 消息语义:
- At Most Once:最多一次消费,数据可能会丢失,但是不会产生数据重复消费。ACK级别设置为0。
- At Least Once:至少一次消费,数据肯定不会丢失,但是可能会导致数据被重复处理Ack级别为-1或者ALL。
- Exactly Once:精确一次消费,对于一些重要的消息,数据不仅不会丢失,也不会被重复处理,该语义基础是至少一次消费+幂等性+事务,ACK级别设置为1。
- 幂等性和事务避免了数据的重复消费,就是生产者不论向Broker发送多少次重复数据,Broker端只会持久化一条数据。
- 消息确认机制
消息确认机制有多种模式,可以保证数据不丢失,但是可能引起消息的重复消费!
- kafka为用户提供了三种可靠性级别,用户根据级别在处理性能和数据安全两个因素之间权衡!
- acks=0,表示生产者发送消息后,不管消费是否接收成功,只发一次,所以最多发送一条,这种安全性极低,处理效率最快。
- acks=1,表示生产者发送消息后,leader得到消息后应答,生成者继续发下一条消息,这种安全性和效率都适中,生产者等不到leader应答就会重新发送,所以至少发送一条。
- acks=-1/all,表示生产者发送消息后,leader得到消息后,follower要与leader成功同步后应答,最后leader应答后,才会继续发送下一条消息,这种数据最安全但是处理效率最差,生产者等不到ISR中所有副本应答就会重新发送一条,所以至少发送一条。
- 消息发送丢失
- 消息不是一直都会发生成功,也可能发送失败。发送失败分为可重复恢复错误和不可重试恢复错误。当然不管是什么错误,只要发送失败,客户端就会自动进行失败重试。
- 可重复恢复错误:找不到leader;找不到目标分区,重新发送就能成功!
- 不可重试恢复错误:消息体过大、缓冲区满了,重试没有用!
- 消息发送重复
生产者向leader发送消息,leader接收到消息后,follower立即进行消息同步,leader还未来的及向生产者应答,生产者再次向leader发送消息,此时leader接收到重复消息。
解决方案:kafka幂等性+kafka事务
- kafka幂等性
目的:保证在消息重发的时候,消息者不会重复处理。即使在消费者收到重复消息时,重复处理也能保证最终结果的一致性。
数学概念:f(f(x))=f(x)。f函数表示对消息的处理。保证数据一致!
实现:kafka引入producer ID(pid)和Sequence Number。在每个新的Producer初始化时,会分配一个唯一的producerID,这个ProducerId对使用者不可见。对于每个pid,producer发送数据的每个Topic和partition都对应一个从0开始单调递增的SequenceNumber(sn)值。
如果生产者SN - 分区内SN=1 Broker接收这个数据 。
如果生产者SN - 分区内SN>1 中间有数据丢失,不允许写入,Producer 抛出 InvalidSequenceNumber 异常
如果生产者SN - 分区内SN<1 有重复数据写入,不允许写入,Producer 抛出 DuplicateSequenceNumber 异常。
实现方式:通过参数 enable.idempotence=true 开启幂等性,默认为 true 开启。
总结:具有 相同主键的消息提交 时,Broker 只会持久化一条。所以幂等性只能保证在单分区单会话内不重复。通俗点讲就是生产者消息pid中的SN与分区中的SN进行绑定!
- Kafka事务机制
通过事务机制,kafka可以实现对多个topic的多个partition的原子性的写入,即处于同一个事务内 的所有消息,不管最终需要落地到哪个topic的哪个partition, 最终结果都是要么全部写成功,要么全部写失败(Atomic multi-partition writes);kafka的事务机制,在底层依赖于幂等性生产者,也就是kafka事务一定有幂等性;事实上,开启kafka事务时,kafka会自动开启幂等生产者。
事务主题:事务相关数据存储在__transaction_state主题中,默认有50个分区,每个分区负责一部分事务。事务划分是transaction_id的hashcode对50取余,计算出该事务属于哪个分区。该分区Leader副本所在Broker节点就是Transactional.id对应的事务协调器节点。
总结:事务是基于幂等性之上,同时解决跨分区的问题即可。1、每个生产这都拥有一个自己独立的事务id;2、每次发送数据的时候,将事务id和分区信息绑定在一起事务协调器;3、当我们提交事务的时候,先将事务信息存储到事务topic中,然后在开始写入数据;当写入数据完成后,在事务topic确认信息,并将结果返回给生产者。
- 数据顺序保证
kafka只能保证同一个分区里的数据是有序的,分区之间的数据是无序的。某些情况下顺序很重要,比如先存钱再消费。如果kafka设置失败可重试,并且生产者在收到服务器响应前可以发送超过1条消息就会导致消息乱序。
- 解决方案:
- 设置失败不重试(retries=0)并且服务器响应前只发送一条数据(max.in.flight.requests.per.connection = 1)。
- 未开启幂等性并且开启服务器响应前只发送一条数据。
- 开始幂等性开启服务器响应前发送数据数不能超过5条,可以保证5个request数据有序。
- 数据序列化
Kafka 生产者将对象序列化成字节数组并发送到服务器,消费者需要将字节数组转换成对象(反序 列化)。序列化与反序列化需要匹配,推荐使用 Avro 序列化方式。
实现:
- xml文件
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-schema-registry-client</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>common-utils</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>io.confluent</groupId>
<artifactId>common-config</artifactId>
<version>5.3.0</version>
</dependency>
- 代码实现
// 生产者
Properties properties = new Properties();
properties.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer",
"io.confluent.kafka.serializers.KafkaAvroSerializer");
// 添加 schema 服务的地址,用于获取 schema
// AbstractKafkaAvroSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG =>
schema.registry.url
properties.put("schema.registry.url", "http://hadoop01:8081");
Producer<String, Object> producer = new KafkaProducer<>(properties);
// 消费者
Properties properties = new Properties();
properties.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer",
"io.confluent.kafka.serializers.KafkaAvroDeserializer");
// 添加 schema 服务的地址,用于获取 schema
properties.put("schema.registry.url", "http://hadoop01:8081");
Consumer<String, String> consumer = new KafkaConsumer<>(properties);
10、kafka消费者
kafka模型
- Consumer 采用 Pull(拉取)模式从 Broker 中读取数据。
- Consumer 采用 Push(推送)模式,Broker 给 Consumer 推送消息的速率是由 Broker 决定的,很难适应消费速率不同的消费者。
- 它的目标是尽可能以最快速度传递消息,但是这样很容易造成 Consumer 来不及处理消 息,典型的表现就是拒绝服务以及网络拥塞。
- 而 Pull 模式则可以根据 Consumer 的消费能力以适当的速率消费消息。
- Pull 模式不足之处是,如果 Kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。
- 因为消费者从 Broker 主动拉取数据,需要维护一个长轮询,Kafka 的消费者在消费数据时会 传入一个时长参数 timeout。
- 如果当前没有数据可供消费,Consumer 会等待一段时间之后再返回,这段时长即为 timeout。
消费者组
消费者组内每个消费者负责消费不同分区的数据,一个消费者组中每一个消费只能消费同一个主题下的不同分区消息,不能重复消费主题分区消息。
一个消息者可以消费多个分区的数据。
消费者组之间互不影响,所有消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
如果向消费者组内添加大于一个主题分区数,则一部分消费者会处于闲置状态,不会接收任何消息。
kafka组中每个消费者从属一个消费者组,消费者API代码必须指定Group ID,否则报错,而命令行不指定由kafka自定随机生成Group ID。
- 分区分配策略
kafka三种分配策略:
- RangeAssignor:如果分区数与消费者数一样,轮询平均分配,如果分区数大于消费者数,每个消费先平均分配,多余的分区由分区字典序排序,前面部分消费者平均分配,比后部分每个消费者可能多一个。
- RangeAssignor计算策略:假设有a个分区,有b个消费者,n=b/a+1 m=b%a 前m个多分配一个分区,后面平均分配。
缺陷:可能会出现部分消费过载的情况。
RoundRobinAssignor: 策略的原理是将消费组内所有消费者以及消费者所订阅的所有topic的partition 按照字典序排序,然后通过轮询消费者方式逐个将分区分配给每个消费者,将分区统一考虑,降低前m个消费者的压力。(轮询策略)
StickyAssignor(粘性消费策略):分区的分配尽可能的均匀;分区的分配尽可能的与上次分配的保持相同;当两者发生冲突时,第一个目标优于第二个目标。
CooperativeStickyAssignor: 新版kafka特性,stickyAssignor的合作版本。遵循与粘性分配器相同的分配逻辑,允许在粘性分配器遵循快速重新平衡协议时进行合作重新平衡。
通俗点理解:遵循StickyAssignor(粘性消费策略)前提下,原来消息者组中的消费者在进行消费分区时,有新的消费者加入,原来消息者消费的分区不会丢失,提交给粘性分配器,然后粘性分配器对全局消费者进行重新分组实现再平衡,此时提交粘性分配器的分区并没有消费,粘性分配器会把没有消费的分区继续给最新加的消费者进行消费,实现最终平衡。
- 数据积压
数据积压是消费者组消费速度跟不上生产者生产速度,导致消息在kafka中大量积压。
解决方案:1、增加分区数和消费者数,但是分区数要和消费者数一样,两者缺一不可。 2、提高消费者拉取消息的最大信息条数,批次拉取数据过少(拉取数据 / 处理时间 < 生产速度),使处理的数据小 于生产的数据,也会造成数据积压。
11、Kafka OffSet
每个副本有个LEO(log End Offset),每个分区有个HW (High Watermark)就是分区内ISR中最小的LEO。其中LEO是副本最后一条消息偏移量的下一个偏移量,消费者只能拉取小于HW偏移量的消息进行消费,而HW偏移量对消费者不可见。
消费者会维护一个offset,如果没有记录对应的offset,默认会从分区末尾开始消费,如果配置为earliest表示从分区起始处开始消费。也可以在代码中使用seek()方法指定分区具体的offset处开始消费,需要先注册分区。
图中offset=9就是consumerA的提交偏移量,下次消费时就从offset=9开始消费。如果消费者提交的偏移量大于客户端处理的最后一个消费偏移量大于1,会丢失数据,如果提交偏移量小于等于客户端处理的最后一个消费偏移量就产生数据重复消费。
- 自动提交offset
在0.9 版本之前,Consumer 默认将offset保存在zookeeper中。
从0.9版本开始,consumer默认将offset保存在kafka内置的topic中,该topic为__cosumer_offsets(该主题有50个分区,每个分区有3个副本,消费者offset分配策略:消费者组ID hash值与分区数取模分配)。
自动提交offset参数有如下:enable.auto.commit:是否开启自动提交offset功能,默认是开启!auto.commit.interval.ms:自动提交 Offset的时间间隔,默认是5000ms。
- 手动提交
程序自己提交偏移量,根据提交时是否继续处理数据可分为:
同步提交(sync commit) :必须等待offset提交完毕,才会消费下一批数据。
异步提交(async commit):发送完提交offset提交请求后,就会开始消费下一条数据。
组合提交:可以在处理数据时候使用异步提交,在等到消费者关闭的时候进行同步提交操作。偏移量也会保存到__consumer_offsets中。
- 外部管理offset
有时候也可以用户自己去管理偏移量,不依赖于Kafka自身的存储,这样可以给程序带来更多的灵活性 。
Kafka本身支持消费者每次读取指定主题的特定分区。
- Kafka本身支持消费者每次读取数据的时候重置消费偏移量,就是消费完消息后,下一次消费可以重新分配offset进行消费。
12、重置offset相关命令
更新Offset由三个维度决定:Topic的作用域、重置策略、执行方案
- 常用命令
# 更新到当前group最初的offset位置
bin/kafka-consumer-groups.sh --bootstrap-server IP:Port --group test-group --reset-offset--all-topics --to-earliest --execute
# 更新到指定的offset
bin/kafka-consumer-groups.sh --bootstrap-server IP:Port --group test-group --reset-offsets --all-topics
# 更新到当前offset位置
bin/kafka-consumer-groups.sh --bootstrap-server IP:Port --group test-group --reset-offsets --all-topics --to-current --execute
# offset位置按设置的值进行位移
bin/kafka-consumer-groups.sh --bootstrap-server IP:Port --group test-group --reset-offsets --all-topics --to-offset 6666 --execute
13、kafka新特性
- 元数据主体
2.8版本以后kafka脱离了zookeeper独立工作,把数据存储到自己内部,利用之前的log存储机制来保存数据元数据。kafka内部有一个元数据主题,元数据会像普通消息一样保存在Log中。然后内部还有KRaft来实现Controller。
- 基于zk的kafka痛点
旧版kafka基于zk实现单个Controller在分区数太大时候,如果Controller Broker挂了,会导致故障转移很慢,需要加载所有的元数据到新的Controller身上,并且需要把这些元数据同步给集群内的所有Broker。
新版kafka有Controller Quorum中,在进行leader选举切换很快,因为元数据都已经在quorum中同步了,也就是quorum的Broker都已经有全部的元数据,所以不需要重新加载元数据。并且其他Broker已经基于Log存储了一些元数据,所以只需要增量更新即可,不需要全量更新。
- 抛弃zk协调者优势:
- 运维层面:只需要单独维护kafka组件即可,减少运维压力。
- 性能层面:zk具有强一致性。如果zk集群某个节点的数据发生变更,则会通知其他zk节点同时执行更新,超过半数写完才能对外提供服务,性能明显下降,抛弃zk提高读写性能。
14、flume整合kafka
操作步骤:(案例一)
- 启动kafka服务(三台节点都要执行)
# 后台启动kafka
kafka-server-start.sh -daemon ${KAFKA_HOME}/config/server.properties
# 控制台启动kafka
kafka-server-start.sh ${KAFKA_HOME}/config/server.properties
- 创建主题(node1)
kafka-topics.sh --zookeeper node1:2181/kafka0110 --create --replication-factor 2 --partitions 3 --topic baidu
- 启动消费者(任何节点都可)
kafka-console-consumer.sh --zookeeper node1:2181/kafka0110 --from-beginning --topic baidu
- 编写flume生产者配置文件上传${FLUME_HOME}/job/(flume2kafka.conf) (node1)
##定义a1的三个组件的名称
a1.sources = r1
a1.sinks = k1
a1.channels = c1
##定义Source的类型
a1.sources.r1.type = netcat
a1.sources.r1.bind = localhost
a1.sources.r1.port = 44444
##定义Channel的类型
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
##定义Sink的类型
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = baidu
a1.sinks.k1.kafka.bootstrap.servers = node1:9092,master:9092,node2:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1
a1.sinks.k1.kafka.producer.compression.type = snappy
##组装source channel sink
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
- 启动flume插件(node1)
flume agent -n a1 -c config -f /usr/local/flume/apache-flume-XX/config/flume2kafka.conf
Dflume.root.logger=INFO,console
- 启动telnet进行数据写入到flume(node1)
telnet localhost 44444
观察消费者与telnet消息读写情况。
案例二
- 启动kafka、创建主题、创建消费者与以上一致
kafka-topics.sh --zookeeper node1:2181/kafka0110 --create --replication-factor 2 --partitions 3 --topic baidu
- 模拟数据日志文件数据
mkdir -p /var/flumeData/
mkdir -p /var/flume_tailDir/
ping www.baidu.com >> /var/flumeData/baidu.log
- 编写flume config文件上传到${FLUME_HOME}/job/ (flume2kafkaDemo.conf)
##定义a1的三个组件的名称
a1.sources = r1
a1.sinks = k1
a1.channels = c1
##定义Source的类型
a1.sources.r1.type = TAILDIR
## 配置存放偏移量信息的数文件
a1.sources.r1.positionFile = /var/flume_tailDir/taildir2logger_position.json
a1.sources.r1.filegroups = f1
#配置需要的读取的文件
a1.sources.r1.filegroups.f1 = /var/flumeData/baidu.log
a1.sources.r1.headers.f1.headerKey1 = f1
a1.sources.r1.fileHeader = true
##定义Channel的类型
a1.channels.c1.type = memory
## 信道最大存放1000个Event
a1.channels.c1.capacity = 1000
## 信道单次传输100个Event
a1.channels.c1.transactionCapacity = 100
##定义Sink的类型
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = baidu
a1.sinks.k1.kafka.bootstrap.servers = node1:9092,master:9092,node2:9092
# 一个批次中要处理的消息数,默认为 100。设置较大的值可以提高吞吐量,但是会增加延迟。
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = -1
a1.sinks.k1.kafka.producer.linger.ms = 1
a1.sinks.k1.kafka.producer.compression.type = snappy
##组装source channel sink
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
- 启动flume(node1)
xxxxxxxxxx flume agent -n a1 -c config -f /usr/local/flume/apache-flume-XX/config/flume2kafkaDemo.conf Dflume.root.logger=INFO,console
- 启动消费者(任何节点都可)
kafka-console-consumer.sh --zookeeper node1:2181/kafka0110 --from-beginning --topic baidu
观察消费者控制台
15、kafka高效读写
- 顺序读写
kafka通过topic分类->消息分区->数据分块(二分查找)->稀疏索引二分查找->顺序读写
Kafka Producer 生产的数据,要写入到 log 文件中的,写的过程是一直追加到文件末端,为顺序写。
缺点:不能删除数据,会保留所有的数据,每个消费者对每个topic都有一个offset用来表示读取到第几条数据。
- 零拷贝(Zero Copy)
Kafka 的数据处理操作都是由 Kafka 生产者和 Kafka 消费者来完成的。Kafka Broker 的应用层不需要关心数据的存储,所以就不用走应用层,传输效率高。 零 Copy 并不是真的不进行数据 Copy,其实还是需要进行两次数据 Copy 的,只不过这两次都是通 过 DMA 进行 Copy 的,无需 CPU 参与,所以叫做零 Copy。
- 页缓存&sendFile
Kafka 在写入磁盘文件的时候,可以直接写入 Page Cache,也就是仅仅写入内存中,接下来由操作 系统自己决定什么时候把 Page Cache 里的数据真的刷入磁盘文件中。通过这一个步骤,就可以将 磁盘文件写性能大幅提升,因为这里相当于是在写内存,而不是在写磁盘.
sendfile 是将读到内核空间的数据转到 Socket Buffer,进行网络发送,适合大文件传输,只需要2次上下文切换(用户态 -> 内核态 -> 用户态)和 2 次拷贝(磁盘文件 DMA 拷贝到内核缓冲 区,内核缓冲区 DMA 拷贝到协议引擎)。相比kafka内存映射机制省略一次CPU copy,大大提高效率。
16、spark streaming基本概念
spark Streaming流式计算
数据以批的形式通过一个先进先出的队列,spark engine从队列中获取一个个批数据封装成RDD算子,然后进行处理,RDD经过操作变成一个个算子保存在内存中,根据流式业务需求可以对中间的结果进行叠加或者存储到外部设备中。
DStream是一组RDD算子,即RDD的一个序列!
- 工作原理
receiver task是7*24小时一直在执行,一边接受数据,将一段时间内接收来的数据保存到Batch中。
假设BatchInterVal为5s,那么将会接收每隔5秒封装成一个batch中,batch没有分布式计算特性,batch数据又封装成一个RDD最终封装成一个DStream中,然后sparkStreaming会启动一个job计算。
- 工作细节
假设batchinterval为5s,每隔5s通过sparkstreaming得到一个DStream,在第6秒的时候计算5秒的数据,假设执行任务时间是3s,那么第6~9s一边在接收数据,一边计算任务,9到10秒只是在接收数据,第11秒的时间又一边接收数据,一边计算任务!
问题:如果job执行的时间大于batchinterval,会造成批数据未及时处理,累积在一起,最后导致OOM,如果同时设置storageLeval包含disk,内存放不下会溢写到磁盘中,加大处理延时。
- 容错性
图中椭圆表示一个RDD,实心圆表示RDD分区,RDD是不可再分的分布式计算数据集,然后多个RDD封装成一个DStream流,DStream流中最后一个RDD计算出来的结果进行落盘操作!输入的数据是可容错机制,任意一个RDD的分区出错或不可用,都可以利用原始数据重新计算,每个RDD都是通过血缘连接。
Spark Streaming将流式计算分解成多个Spark Job,对于每一段数据的处理都会经过Spark DAG图分解以及Spark的任务集的调度过程。对于目前版本的Spark Streaming而言,其最小的Batch Size的选取在0.5~2秒钟之间(Storm 目前最小的延迟是100ms左右)。
DStream是由连续多个特定时间的RDD算子组成,DStream是对RDD增强,行为表现和RDD计算差不多。
注意:滑动间隔和窗口间隔的大小一定得设置为批处理间隔的整数倍。
- 相关代码概念理解

代码中表示5秒中,形成一个批数据!
窗口大小:相当于有很多个批数据。
滑动间隔大小:表示每次处理几个批数据(处理批数据个数=窗口大小/滑动间隔大小),因此窗口大小要与滑动间隔大小是整数倍!
- 如何在外部高效输出数据!
foreachRDD()将每个RDD计算出的结果传输到外部设备中!但是如果把RDD计算结果写入数据库中,一般人可能会直接在foreachRDD算子里创建连接,这是错误的,这样的话会造成大量的资源浪费,每处理完一个RDD就释放,然后处理完RDD就重新建立连接进行新的RDD计算!
正确的做法是在对一个分区输出的数据建立连接,使用foreachPartition()方法,在方法内创建数据库连接,并且最好使用数据库连接池进行优化连接,因为数据库连接池会把处理完的连接放回池内,要是使用的时候直接从池中取出来,不会删除再创建,进一步优化数据读取性能!
17、SparkStreaming整合kafka
目的就是sparkStreaming读取kafka中的消息进行流处理!
官方文档:
https://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html
- 导入依赖
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>0.11.0.3</version>
<!--排除kafka中lz4压缩包依赖,防止与spark-streaming-kafka中lz4依赖冲突!-->
<exclusions>
<exclusion>
<groupId>net.jpountz.lz4</groupId>
<artifactId>lz4</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.3</version>
<exclusions>
<!--排除kafka中lz4压缩包依赖,防止与spark-streaming-kafka中lz4依赖冲突!-->
<exclusion>
<groupId>net.jpountz.lz4</groupId>
<artifactId>lz4</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
<version>2.4.6</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.12</artifactId>
<version>2.4.6</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>2.4.6</version>
</dependency>
前提安装好scala环境和java环境,开启kafka集群、spark集群、hive集群、hadoop集群
- 创建主题
kafka-topics.sh --zookeeper node01:2181/kafka0110 --create --replication-factor 2 --partitions 3 --topic userlog
- 创建生产者(在控制台输入数据传入kafka)
kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic topic_spark_msg
- spark流处理代码(从kafka中获取数据进行处理)
package com.zwf.kafka_streaming
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.KafkaUtils
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
/**
* @author MrZeng
* @date 2023-12-28 10:08
* @version 1.0
*/
object SparkStreamingToKafka {
//sparkStreaming读取kafka方式
def main(args: Array[String]): Unit = {
val sparkConf=new SparkConf().setMaster("local[2]").setAppName("hello_kafka")
//创建sparkStreaming 设置一秒钟生成一个批数据
val streaming = new StreamingContext(sparkConf, Seconds(5))
//配置kafka
val kafkaPram=Map[String,Object](
"bootstrap.servers" -> "node1:9092,master:9092,node2:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "spark_bigdata",
"auto.offset.reset" -> "latest", //可以设置earliest
"enable.auto.commit" -> (false: java.lang.Boolean)
)
//设置消费主题
val topics=Array("topic_spark_msg")
val kafka_DStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(streaming, PreferConsistent, Subscribe[String, String](topics, kafkaPram))
val total: DStream[(String, Int)] = kafka_DStream.map(_.value()).flatMap(_.split("\\s+")).map((_, 1)).reduceByKey(_ + _)
total.print()
streaming.start()
streaming.awaitTermination()
}
}
kafka新老版本推送数据的区别(spark1.5分界)?
老版本:基于receiver的方法采用kafka高级消费API,每个executor不断拉取数据,同时保存excutor内存与HDFS的预写日志。当消息写入WAL日志后,自动更新zk的offset,但是不能保证至少一次信息语义,可能会出现上一次消息数据已经写入WAL日志,但是offset更新失败的情况,kafka还会按照上一次offset发消息。通过减少blockInterval提高并行度。
新版本:基于direct方法获取kafka简单消费者API,executor不再从kafka连续读取信息,kafka分区与RDD分区一一对应,消除了receiver和WAL日志,Driver从kafka中获取批数据offset范围,executor根据offset范围从kafka拉取数据进行消费,offset保证数据的唯一性,消除了不一致性,达到仅一次语义,但是需要自己来管理offset,Direct模式的并行度由kafka中的topic分区数决定,也就是RDD默认分区数与kafka分区数一致。
偏移量都是存在offset中的_offset_customer主题中。
18、SparkStreaming管理offset
- 存储offset
spark输出一次数据,就存储一次offset偏移量。
自动提交:默认是更新数据就新增保存一次offset偏移量!
手动提交:将偏移量与输出一起存储在原子事务中!
- checkpoint
将offset保存在checkpoint(检查点)中,好处不需要自动手动操作或者管理.
缺点:1、修改业务逻辑的时候,数据会读取失败。2、RDD的检查点会产生保存到可靠性存储的成本,这可能导致RDD获得检查点的那些批次处理时间增加。3、批处理时间间隔过久会导致每次任务数量变大。
- kafka itself
将偏移量还是存储到kafka的__offset_ comsumer主题中,不用关心代码版本,业务与偏移量管理解耦。
缺点:业务数据大时,会对kafka集群造成压力,数据不安全!
- 自定义offset存储
把每批次offset存储到一个外部存储系统包括HBase、HDFS、Zookeeper、Kafka等。
- 代码实现
检查点(checkpoint)保存批数据的offset
package com.zwf.kafka_streaming
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.{SparkConf, TaskContext}
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* @author MrZeng
* @date 2023-12-28 10:08
* @version 1.0
*/
object SparkStreamingToKafka_checkpoint {
//sparkStreaming读取kafka方式
def checkpointFunction: StreamingContext = {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("hello_kafka")
//创建sparkStreaming 设置一秒钟生成一个批数据
val streaming = new StreamingContext(sparkConf, Seconds(5))
//把offset自定义存储到外部设备
streaming.checkpoint("hdfs://master:8020/sparkStreaming/checkpoints")
//配置kafka
val kafkaPram = Map[String, Object](
"bootstrap.servers" -> "node1:9092,master:9092,node2:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "spark_bigdata",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
//设置消费主题
val topics = Array("topic_clickhouse_two")
val kafka_DStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(streaming, PreferConsistent, Subscribe[String, String](topics, kafkaPram))
kafka_DStream.foreachRDD(rdd=>{
//获取分区数 分区数与kafka分区数一致
println("------rdd.partitions.size----------------"+rdd.partitions.size)
//获取批数据的偏移量范围 HasOffsetRanges对象保存有topic partition fromOffset utilOffset
//获取offsetRanges方法
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition(x=>{
//获取每个分区批数据的 主题 分区数 起始偏移量 提交偏移量
val offsetRange: OffsetRange = ranges(TaskContext.get().partitionId())
println("kafka主题名"+offsetRange.topic+"===kafka分区号==="+offsetRange.partition+"===kafka from offset==="+offsetRange.fromOffset+"====kafka util offset==="+offsetRange.untilOffset)
})
})
streaming
}
def main(args: Array[String]): Unit = {
//检查checkpoint是否存在 不存在就在hdfs上创建检查点 存在就直接在检查点获取最新批数据偏移量
val context: StreamingContext = StreamingContext
.getOrCreate("hdfs://master:8020/sparkStreaming/checkpoints", checkpointFunction _)
context.start()
context.awaitTermination()
}
}
kafka自己管理offset 需要提交偏移量,实现最少一次消息语义
package com.zwf.kafka_streaming
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.{CanCommitOffsets, HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, TaskContext}
/**
* @author MrZeng
* @date 2023-12-28 10:08
* @version 1.0
*/
object SparkStreamingToKafka_kafkaSelf {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("hello_kafka")
//创建sparkStreaming 设置一秒钟生成一个批数据
val streaming = new StreamingContext(sparkConf, Seconds(5))
//配置kafka
val kafkaPram = Map[String, Object](
"bootstrap.servers" -> "node1:9092,master:9092,node2:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "spark_bigdata",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics=Array("topic_clickhouse_two")
val kafka_DStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(streaming, PreferConsistent, Subscribe[String, String](topics, kafkaPram))
//处理每个RDD
kafka_DStream.foreachRDD(rdd=>{
println("====kafka分区数"+rdd.partitions.size+"=========")
//将RDD转化为HasOffsetRanges对象并调用offsetRanges方法
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition(partition=>{
//获取每个分区的主题 分区号 偏移量
val offsetRange: OffsetRange = ranges(TaskContext.get().partitionId())
//打印数据的元数据
println("[kafka分区主题]"+offsetRange.topic+"[kafka分区数]"+offsetRange.partition+"[起始偏移量]"+offsetRange.fromOffset+"[提交偏移量]"+offsetRange.untilOffset)
})
//kafka自身维护offset 手动提交每一批数据的偏移量
kafka_DStream.asInstanceOf[CanCommitOffsets].commitAsync(ranges)
})
streaming.start()
streaming.awaitTermination()
}
}
自定义管理offset
package com.zwf.kafka_streaming
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.{CanCommitOffsets, HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, TaskContext}
/**
* @author MrZeng
* @date 2023-12-28 10:08
* @version 1.0
*/
object SparkStreamingToKafka_customer {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[2]").setAppName("hello_kafka")
//创建sparkStreaming 设置一秒钟生成一个批数据
val streaming = new StreamingContext(sparkConf, Seconds(5))
//配置kafka
val kafkaPram = Map[String, Object](
"bootstrap.servers" -> "node1:9092,master:9092,node2:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "spark_bigdata",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
//外部读取offset 自定义存储offset
val offsets: Map[TopicPartition, Long] = Map(
new TopicPartition("topic_clickhouse_two", 0) -> 100L,
new TopicPartition("topic_clickhouse_two", 1) -> 50L,
new TopicPartition("topic_clickhouse_two", 2) -> 70L
)
val topics=Array("topic_clickhouse_two")
val kafka_DStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(streaming, PreferConsistent, Subscribe[String, String](topics, kafkaPram,offsets))
//处理每个RDD
kafka_DStream.foreachRDD(rdd=>{
println("====kafka分区数"+rdd.partitions.size+"=========")
//将RDD转化为HasOffsetRanges对象并调用offsetRanges方法
val ranges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
rdd.foreachPartition(partition=>{
//获取每个分区的主题 分区号 偏移量
val offsetRange: OffsetRange = ranges(TaskContext.get().partitionId())
//打印数据的元数据
println("[kafka分区主题]"+offsetRange.topic+"[kafka分区数]"+offsetRange.partition+"[起始偏移量]"+offsetRange.fromOffset+"[提交偏移量]"+offsetRange.untilOffset)
})
})
streaming.start()
streaming.awaitTermination()
}
}
19、数据反压机制
spark处理的速率小于生产的速率,容易造成OOM,就需要反压机制,实现数据计算平衡,防止集群崩溃!
spark中RateController监控数据计算结束后的结果(处理结束时间、处理时间、等待时间、数据处理时间),通过RateElimator计算获取新的生产速率,发送给令牌桶机制作限流处理!因此反压机制要启动,必须先处理一批数据(第一批数据不能过大,否则反压机制没有启动就OOM),处理的数据越多越准确!
反压机制真正起作用时需要至少处理一个批
令牌桶会源源不断产生令牌,如果令牌不被消耗或者消耗的速率小于生产的速率,令牌不断增多,直到把桶填满溢出来,这时候令牌数量永远不会大于桶的最大容量,在处理数据的时候需要获取令牌,计算没有完成就无法获取令牌,计算完成就从桶里获取令牌进行下一次计算,没有获取令牌就处于阻塞状态!
20、SparkStreaming事务处理
只有一次消息语义是实时处理的难点之一,因此需要事务处理解决消息丢失或者重复消费问题,最终达到只有一次消息语义(kafka解决只有一次消息语义使用策略是:至少一次消息语义+幂等性+事务)!
SparkStreaming 为了保证数据安全分为Driver容错机制和Executor容错机制:
- Driver容错机制:Direct模式下,Driver只保存批数据的offset,然后Executor根据offset拉取数据进行计算,最终把结果写入HDFS上。但是为了防止offset数据的丢失了,Driver会设置检查点把offset放进Hdfs中;而老版的Receiver模式下是通过重启WAL日志(在HDFS中),恢复数据!
- Excutor容错机制:在计算时出现异常,RDD算子可以通过血缘追踪重新计算保证数据安全;设置检查点把数据暂时存入磁盘中持久化备份保存,下次计算直接从检查点读取数据重新计算(从另一台节点读取)。

本文来自博客园,作者:戴莫先生Study平台,转载请注明原文链接:https://www.cnblogs.com/smallzengstudy/p/17924448.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南