用java代码向kafka异步发送消息
注意关闭linux虚拟机防火墙,保证主机能够ping通linux,修改运行代码的主机hosts文件
# win10hosts文件目录
C:\Windows\System32\drivers\etc
# 依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>这里的版本对应kafka版本</version>
</dependency>
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* @author 29467
* @date 2022/10/13 20:25
*/
public class CustomerProducer {
public static void main(String[] args) {
// properties文件
Properties properties = new Properties();
// 连接集群 bootstrap.server
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092, hadoop102:9092");
// 选择对应的key和value序列化类型 key.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 发送数据
for(int i = 0; i< 5; i++){
kafkaProducer.send(new ProducerRecord<>("first","hello"+i));
}
// 关闭资源
kafkaProducer.close();
}
}
启动kafka的消费者打印
./kafka-console-consumer.sh --bootstrap-server hadoop101:9092 --topic first
运行代码可见消费者打印的消息
kafka回调异步发送
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* @author 29467
* @date 2022/10/13 20:55
*/
public class CustomerProducerCallback {
public static void main(String[] args) {
// properties文件
Properties properties = new Properties();
// 连接集群 bootstrap.server
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092, hadoop102:9092");
// 选择对应的key和value序列化类型 key.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 发送数据
for(int i = 0; i< 5; i++){
kafkaProducer.send(new ProducerRecord<>("first", "hello" + i), new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e == null){
System.out.println("主题:"+recordMetadata.topic()+"分区:"+recordMetadata.partition());
}
}
});
}
// 关闭资源
kafkaProducer.close();
}
}
可见回调函数打印的结果在控制台
kafka同步发送
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
/**
* @author 29467
* @date 2022/10/13 21:08
*/
public class CustomerProduceSync {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// properties文件
Properties properties = new Properties();
// 连接集群 bootstrap.server
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092, hadoop102:9092");
// 选择对应的key和value序列化类型 key.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 发送数据
for(int i = 0; i< 5; i++){
kafkaProducer.send(new ProducerRecord<>("first","hello"+i)).get();
}
System.out.println("发送完毕");
// 关闭资源
kafkaProducer.close();
}
}
分区
kafka分区好处
- 便于合理使用储存资源,每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据储存在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
- 提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
ProducerRecord类
- 指明partition的情况下,直接将指明的值作为partition的值,例如partition=0,所有的数据写入分区0
- 没有指明partition值但有key的情况下,将key的hash值与topic的partition数进行取余得到partition值。例如:key1的hash值为5,key2的hash值为6,topic的partition数为2,那么key1对应的value1写入1号分区,key2对应的value2写入0号分区。
- 既没有partition值又没有key值的情况下,kafka采用sticky partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,kafka再随机一个分区进行使用。(和上一次的分区不同)。例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到了,kafka会再随机一个分区进行使用(如果还是0则会继续随机)。
自定义分区器
继承Partitioner类,重写partition方法
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
/**
* @author 29467
* @date 2022/10/14 15:19
*/
public class MyPartitioner implements Partitioner {
@Override
public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
// 获取数据
String msgValue = o1.toString();
int partition;
if(msgValue.contains("thenema")){
partition = 0;
}else{
partition = 1;
}
return partition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
然后在调用的producer类中的properties添加使用
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "com.thenema.kafka.producer.MyPartitioner");
生产者提高吞吐量
batch.size:批次大小,默认为16k
linger.ms:等待时间,修改为5-100ms
compression.type:压缩snappy
RecordAccumulator:缓冲区大小,修改为64m
供参考的java代码
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* @author 29467
* @date 2022/10/14 16:00
*/
public class CustomerProducerParameters {
public static void main(String[] args) {
// 配置
Properties properties = new Properties();
// 连接kafka集群
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop101:9092, hadoop102:9092");
// 序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 缓冲区大小(33554432b,32mb)
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
// 批次大小(16kb)
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// linger.ms(等待时间1ms)
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// 压缩(gzip,snappy等等)
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
// 创建生产者
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 发送数据
kafkaProducer.send(new ProducerRecord<>("first","I am the value"));
}
}
数据的可靠性
ACK应答级别
0:生产者发送过来的数据,不需要等数据落盘应答
数据可靠性分析:丢数
1:生产者发送过来的数据,Leader收到数据后应答
数据可靠性分析:丢数
-1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。
Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2)。
如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认为30s,例如2超时,(leader:0,isr:0,1)
这样就不用等长时间联系不上或者已经故障的节点。
数据可靠性分析
如果分区副本设置为1个,或者ISR里应答的最小副本数量(min.insync.replicas 默认为1)设置为1,和ack=1的效果是一样的,仍然有丢数的风险(leader:0,isr:0)
数据完全可靠条件=ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
可靠性总结
acks=0,生产者发送过来的数据就不管了,可靠性差,效率高
acks=1,生产者发送过来的数据Leader应答,可靠性中等,效率中等。
acks=-1,生产者发送过来的数据Leader和ISR队列里面的所有Follower应答,可靠性高,效率低。
在生产环境中,acks=0很少使用,acks=1一般用于传输普通日志,允许丢失个别数据;acks=-1一般用于传输和钱相关的数据,对可靠性要求比较高的场景。
参考java代码
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* @author 29467
* @date 2022/10/13 20:25
*/
public class CustomerProducerAcks {
public static void main(String[] args) {
// properties文件
Properties properties = new Properties();
// 连接集群 bootstrap.server
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092, hadoop102:9092");
// 选择对应的key和value序列化类型 key.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// acks
properties.put(ProducerConfig.ACKS_CONFIG,"1");
// 重试次数
properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 3);
// 创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 发送数据
for(int i = 0; i< 5; i++){
kafkaProducer.send(new ProducerRecord<>("first","hello"+i));
}
// 关闭资源
kafkaProducer.close();
}
}
kafka数据重复
至少一次:ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
最多一次:ACK级别设置为0
至少一次可以保证数据不丢失,但是不能保证数据不重复
最多一次可以保证数据不重复,但是不能保证数据不丢失
精确一次:对于一些非常重要的信息,要求数据既不能重复,也不能丢失
幂等性
幂等性指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
精确一次 = 幂等性 + 至少一次(ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2)
重复数据的判断标准:具有<PID, Partition, SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的,Partition表示分区号,Sequence Number是单调自增的。
所以幂等性只能保证的是在单分区单会话内不重复。
使用幂等性:开启参数enable.idempotence默认为true,false为关闭
kafka事务原理
说明:开启事务,必须开启幂等性。
Producer在使用事务功能前,必须先自定义一个唯一的transactional.id。有了transactional.id,即使客户端挂了,他重启后也能继续处理未完成的事务。
java参考代码
package com.thenema.kafka.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
/**
* @author 29467
* @date 2022/10/13 20:25
*/
public class CustomerProducerTransactions {
public static void main(String[] args) {
// properties文件
Properties properties = new Properties();
// 连接集群 bootstrap.server
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop101:9092, hadoop102:9092");
// 选择对应的key和value序列化类型 key.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transactional_id_01");
// 创建kafka生产者对象
KafkaProducer<String,String> kafkaProducer = new KafkaProducer<>(properties);
// 初始化
kafkaProducer.initTransactions();
// 开始事务
kafkaProducer.beginTransaction();
try{
// 发送数据
for(int i = 0; i< 5; i++){
kafkaProducer.send(new ProducerRecord<>("first","hello"+i));
}
// 提交事务
kafkaProducer.commitTransaction();
}catch (Exception e){
// 中止事务
kafkaProducer.abortTransaction();
}finally {
// 结束事务
kafkaProducer.close();
}
// 关闭资源
kafkaProducer.close();
}
}
数据有序
单分区:有序
多分区:分区与分区间无序
数据乱序
- kafka在1.x版本之前保证数据单分区有序,条件如下:max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)
- kafka在1.x以后版本保证数据单分区有序,条件如下:开启幂等性:max.in.flight.requests.per.connection需要设置小于等于5。未开启幂等性:max.in.flight.requests.per.connection需要设置为1。
- 在kafka1.x以后,启用幂等性后,kafka服务端会缓存producer发来的最近的5个request的元数据,故无论如何,都可以保证最近5个request的数据都是有序的。
zookeeper中存储的kafka信息
使用kafka的bin目录下的zookeeper-shell.sh
连接zookeeper
zookeeper-shell.sh hadoop:2181
在zookeeper的服务端存储的Kafka相关信息
- /kafka/brokers/ids 记录有哪些服务器
- /kafka/brokers/topics/first/partitions/0/state 记录谁是Leader,有哪些服务器可用
kafka broker总体工作流程
- broker启动后在zk中注册
- broker向zookeeper注册controller,谁先注册谁是leader
- 由选举出来的controller监听brokers节点变化
- controller决定leader选举
- AR是kafka分区中所有副本的统称
- 选举规则:在isr中存活为前提,按照AR中排在前面的优先。
- controller将节点信息上传到zookeeper
- 其他controller从zk同步相关信息
- 假设broker中的leader挂了之后,Controller会监听到节点的变化,选举新的leader,在isr中存在的节点,按照AR中的顺序从前往后选。最后更新leader以及ISR。
服务器上下线
可以在zookeeper的kafka/brokers/ids下看到已连接的kafka
服役一个新节点
- 新建一个服务器,完成相关设置后连接zookeeper集群
- 创建一个要均衡的主题
vim topics-to-move.json
{
"topics": [
{"topic":"first"}
],
"version":1
}
- 生成一个负载均衡的计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop101:9092 --topics-to-move-json-filetopics-to-move.json --broker-list "0,1,2,3" --generate
- 创建副本存储计划(所有的副本存储在broker0、broker1、broker2、broker3中)
vim increase-replication-factor.json
输入以下内容
{"version" :1, "partitions" : [ { "topic" : "first" , "partition" :0, "replicas" : [3,1], "log dirs" : [ "any" , "any"] } , { "topic" : "first", "partition" :1, "replicas": [0,2], "log_dirs":[ "any" , "any"] },{ "topic" : "first" , "partition":2, "replicas": [1,3], "log_dirs": [ "any", "any"]}, { "topic":"first" , "partition" : 3, "replicas": [2,0], "log_dirs" : [ "any" , "any"] },{"topic" : "first" , "partition" :4, "replicas": [3,2], "log dirs" : [ "any", "any"] } , { "topic" :"first", "partition" :5, "replicas": [0,3] , "log dirs": ["any" , "any"] },{ "topic" : "first" , "partition":6, "replicas": [1,0], "log_dirs" : [ "any" , "any"]}]}
- 执行副本储存计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop101:9092 --reassignment-json-file increase-replication-factor.json --execute
- 验证副本计划
bin/kafka-ressign-partitions.sh --bootstarp-server hadoop101:9092 --reassignment-json-file increase-replication-factor.json --verify
退役旧的节点
- 执行负载均衡的操作
- 先按照退役一台节点,生成执行计划,然后按照退役时操作流程执行负载均衡
- 创建一个要负载均衡的主题
vim topics-to-move.json
{
"topics": [
{"topic":"first"}
],
"version":1
}
- 创建执行计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop101:9092 --topics-to-move-json-filetopics-to-move.json --broker-list "0,1,2" --generate
- 创建副本存储计划(所有的副本存储在broker0、broker1、broker2中)
vim increase-replication-factor.json
{"version":1, "partitions":[ { "topic":"first" , "partition":0, "replicas":[2,0,1], "log_dirs":[ "any" , "any" , "any"]}, { "topic": "first" , "partition":1, "replicas": [ 0,1,2], "log dirs":[ "any" , "any" , "any"] },{ "topic":"first" , "partition":2, "replicas":[1,2,0],"log dirs": [ "any", "any", "any"]}]}
- 执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop101:9092 --reassignment-json-file increase-replication-factor.json --execute
- 验证副本存储计划
bin/kafka-ressign-partitions.sh --bootstarp-server hadoop101:9092 --reassignment-json-file increase-replication-factor.json --verify
- 执行停止命令