一、提前准备
1、三台服务器
2、安装好zookeeper;
3、安装好kafka( tar -zxvf kafka_2.11-0.11.0.0.tgz -soft/)
4、修改配置文件
#broker 的全局唯一编号,不能重复哦
broker.id=0
#删除 topic 功能使能
delete.topic.enable=true
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的现成数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志存放的路径
log.dirs= 自 己 修 改
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#配置连接 Zookeeper 集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181
二、操作
1、Linux命令行操作
①启动&关闭
bin/kafka-server-start.sh -daemon config/server.properties
bin/kafka-server-stop.sh stop
②查看当前服务器中的所有 topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --list
③创建topic
bin/kafka-topics.sh --zookeeper hadoop102:2181 --create --replication- factor 3 --partitions 1 --topic first
zookepper集群地址、副本数、分区数、主题名
④发送消息
bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic first
上面的意思是在控制台启动生产者,然后就可以逐行发送消息了。注意连接的是broker-list
⑤消费消息
bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first
或下面这个命令
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first
这样就能接收消息了,如果想从头接加上–from-beginning
2、API操作
①导入依赖(生产和消费是同一个依赖)
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
②生产数据
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class CustomProducer {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群,broker-list
props.put("acks", "all");
props.put("retries", 1);//重试次数
props.put("batch.size", 16384);//批次大小
props.put("linger.ms", 1);//等待时间
props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
//创建一个生产者对象
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++) {
//发送数据,first主题、key是i、value也是i,最后有个回调函数
producer.send(new ProducerRecord<String, String>("first",
Integer.toString(i), Integer.toString(i)), new Callback() {
//回调函数,该方法会在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println("success->" + metadata.offset());
} else {
exception.printStackTrace();
} }
});
}
producer.close();
} }
③消费数据
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
public class CustomConsumer {
public static void main(String[] args) {
Properties props = new Properties();
//Kafka 集群
props.put("bootstrap.servers", "hadoop102:9092");
//消费者组,只要 group.id 相同,就属于同一个消费者组
props.put("group.id", "test");
//关闭自动提交 offset
props.put("enable.auto.commit", "false");
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<>(props);
consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);//消费者拉取数据
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (exception != null) {
System.err.println("Commit failed for" + offsets);
}
}
});
} } }
三、细节说明
1、原理图
①几个概念:生产者、消费者、消费者组、kafka集群、Broker、Zookeeper、偏移量、主题、分区、副本
2、存储机制
①数据存放位置
kafka目录data文件夹下有多个以主题-分区命名的文件夹,每个文件夹里面有以.index和.log文件结尾的文件,成对出现;
②log文件里面只放数据,index文件放的是索引,指向log文件。所以查找的时候快很多;
③先用二分查找找到是哪个index文件(因为index文件命名是以偏移量命名的),然后index文件里面存的每条数据大小一定,所以能很快定位到,然后就可以指向log文件啦。
四、生产者系列
0、发送流程
Kafka 的 Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。
main 线程将消息发送给 RecordAccumulator,Sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker。
1、将 producer 发送的数据封装成一个 ProducerRecord 对象。
①必须指定主题;
②如果指定分区,那么会到分区中,没有分区那么根据key的hash值与分区数取余,如果没有key那么就随机划分;
2、数据可靠性(ack)
①发送时机
等所有follower回复后才通知生产者;
②isr
如果等所有的follower都回复,那么有可能等很长时间,所以引入isr。将回复快的follower选入进去,将来leader也从其中选举。如果isr中的都回复了那么就向生产者返回ack
③ack是否启用
参数为0,那么不等ack继续发,会产生数据丢失;
参数为1,生产者等待ack,但是如果leader写完了就返回ack,有可能造成follower的数据丢失。
参数为-1,生产者等待ack,且isr中所有都写完后才返回ack,但是如果此时leader挂了,那么生产者还会再发一次数据,造成数据冗余。
④幂等性机制
开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。
但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。
3、副本一致性
这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
①HW之前的数据才对外可见,这样可以从根源上保证了彼此数据一致;
②如果leader挂了,上来的从机告诉其他人多退少补。
五消费者系列
1、offset保存
①可以自动提交或手动提交offset,但均有问题:先消费后提交会产生重复消费、先提交后消费则会漏掉数据;
②自定义存储offset
Kafka 0.9 版本之前,offset 存储在 zookeeper,0.9 版本及之后,默认将 offset 存储在 Kafka的一个内置的 topic 中。除此之外,Kafka 还可以选择自定义存储 offset。 offset 的维护是相当繁琐的,因为需要考虑到消费者的 Rebalace。
当有新的消费者加入消费者组、已有的消费者退出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。
消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。
要实现自定义存储 offset,需要借助 ConsumerRebalanceListener,以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现。
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import java.util.*;
public class CustomConsumer {
private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
public static void main(String[] args) {
//创建配置信息
Properties props = new Properties();
//Kafka 集群
props.put("bootstrap.servers", "hadoop102:9092");
//消费者组,只要 group.id 相同,就属于同一个消费者组
props.put("group.id", "test");
//关闭自动提交 offset
props.put("enable.auto.commit", "false");
//Key 和 Value 的反序列化类
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<>(props);
//消费者订阅主题
consumer.subscribe(Arrays.asList("first"),
new ConsumerRebalanceListener() {
//该方法会在 Rebalance 之前调用
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
commitOffset(currentOffset);
}
//该方法会在 Rebalance 之后调用
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
currentOffset.clear();
for (TopicPartition partition : partitions) {
consumer.seek(partition,getOffset(partition));//定位到最近提交的 offset 位置继续消费
}
}
});
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);//消费者拉取数据
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
currentOffset.put(new TopicPartition(record.topic(), record.partition()), record.offset());
}
commitOffset(currentOffset);//异步提交
}
}
//获取某分区的最新 offset
private static long getOffset(TopicPartition partition) {
return 0;
}
//提交该消费者所有分区的 offset
private static void commitOffset(Map<TopicPartition, Long>
currentOffset) {
}
}
①在订阅的时候传入一个ConsumerRebalanceListener,在rebalance可以拉取和提交;
②之后每次消费都把目前位置结果记录到一个hashMap中,最后提交一下;
③可以把获取和提交的地方写一个JDBC,并加上事务,这样就可以保证消费者数据可靠性了。
六自定义拦截器
1、手写拦截器
一个类实现ProducerInterceptor接口即可
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class TimeInterceptor implements ProducerInterceptor<String, String> {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
// 创建一个新的 record,把时间戳写入消息体的最前部
return new ProducerRecord(record.topic(), record.partition(),record.timestamp(), record.key(),
System.currentTimeMillis() + "," + record.value().toString());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
// RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送过程中失败时调用
//所以可以进行发送成功/失败消息的计数
}
@Override
public void close() {
}
}
2、生产者加入拦截器链
List<String> interceptors = new ArrayList<>();
interceptors.add("自定义拦截器包的全路径 cn.learn.interceptor.TimeInterce
ptor");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);