kafka部署在了 IP为:192.168.56.10
zookeeper部署在了ip为:192.168.56.11,端口为2181(尝试部署在一台虚拟机的时候行不通)
vgrant init centos/7
vagrant up
vagrant ssh
kafka所在的虚拟机文件在:C:\Users\yin_q>
zookeeper所在的虚拟机文件在:C:\Windows\system32>
第一篇章:
启动kafka:cd /usr/local/kafka/kafka/bin
./kafka-server-start.sh -daemon ../config/server.properties
启动zookeeper:
cd /usr/local/zookeeper/bin
./zkServer.sh start
进入到zk中的节点看id是 0 的broker有没有存在:
cd /usr/local/zookeeper/bin
./zkCli.sh
ls /brokers/ids/
创建名为“test”的topic:
./kafka-topics.sh --create --zookeeper 192.168.56.11:2181 --replication-factor 1 --partitions 1 --topic test
查看当前kafka内有哪些topic:
./kafka-topics.sh --list --zookeeper 192.168.56.11:2181
发送消息:
./kafka-console-producer.sh --broker-list 192.168.56.10:9092 --topic test
消费消息:
方式一:从最后一条消息的偏移量+1开始消费
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --topic test
方式二:从头开始消费
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --from-beginning --topic test
消息储存在 :/usr/local/kafka/data/kafka-logs/主题-分区/00000000.log中 ,消息是顺序存储,消息是有偏移量的,消费时可以指明偏移量进行消费
单播消息:一个消费组里只会有一个消费者能消费到某一个topic中的消息。于是可以创建多个消费者,这些消费者在同一个消费组中
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --consumer-property group.id=testGroup --topic test
多播消息:
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --consumer-property group.id=testGroup1 --topic test
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092 --consumer-property group.id=testGroup2 --topic test
查看当前主题下有哪些消费组
./kafka-consumer-groups.sh --bootstrap-server 192.168.56.10:9092 --list
查看消费组中的具体信息:比如当前偏移量、最后一条消息的偏移量、堆积的消息数量
./kafka-consumer-groups.sh --bootstrap-server 192.168.56.10:9092 --describe --group testGroup
Currennt-offset: 当前消费组的已消费偏移量
Log-end-offset: 主题对应分区消息的结束偏移量(HW)
Lag: 当前消费组未消费的消息数
为一个主题“test1”创建多个分区:
./kafka-topics.sh --create --zookeeper 192.168.56.11:2181 --replication-factor 1 --partitions 2 --topic test1
查看topic的分区信息:
./kafka-topics.sh --describe --zookeeper 192.168.56.11:2181 --topic test1
(定期将自己消费分区的offset提交给kafka内部topic:__consumer_offsets,提交过去的时候,key是consumerGroupId+topic+分区号,value就是当前offset的值,kafka会定 期清理topic里的消息,最后就保留最新的那条数据 因为__consumer_offsets可能会接收高并发的请求,kafka默认给其分配 50 个分区(可以 通过offsets.topic.num.partitions设置),这样可以通过加机器的方式抗大并发。 通过如下公式可以选出consumer消费的offset要提交到__consumer_offsets的哪个分区 公式:hash(consumerGroupId) % __consumer_offsets主题的分区数)
第二篇章:Kafka集群及副本
搭建kakfa集群,3个broker
vi server.properties:
broker.id= 0
listeners=PLAINTEXT://192.168.56.10:9092
log.dir=/usr/local/data/kafka-logs
vi server1.properties
broker.id= 1
listeners=PLAINTEXT://192.168.56.10:9093
log.dir=/usr/local/data/kafka-logs-1
vi server2.properties
broker.id= 2
listeners=PLAINTEXT://192.168.56.10:9094
log.dir=/usr/local/data/kafka-logs-2
启动三台服务器:
./kafka-server-start.sh -daemon ../config/server.properties
./kafka-server-start.sh -daemon ../config/server1.properties
./kafka-server-start.sh -daemon ../config/server2.properties
查看是否成功:
进入zookeeper中的bin下:
./zkCli.sh
ls /brokers/ids/
在集群中,不同的副本会被部署在不同的broker上。下面例子:创建 1个主题, 2 个分区、 3 个副本:
./kafka-topics.sh --create --zookeeper 192.168.56.11:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic
查看topic情况:
./kafka-topics.sh --describe --zookeeper 192.168.56.11:2181 --topic my-replicated-topic
说明:isr: 可以同步的broker节点和已同步的broker节点,存放在isr集合中。如果isr中的节点性能较差,会被踢出isr集合
leader:kafka的读写操作,都发生在leader,leader负责把数据同步给follower。当leader挂了,经过主从选举,从多个follwer中选举产生一个新的leader
follower:接收leader的同步的数据
broker,主题,分区,副本:集群中有多个broker,创建主题是可以指明主题有多少个分区(把消息拆分到不同的分区中储存),可以为分区创建多个副本,不同的副本存放在不同的broker中;
kafka集群消息的发送:
./kafka-console-producer.sh --broker-list 192.168.56.10:9092,192.168.56.10:9093,192.168.56.10:9094 --topic my-replicated-topic
kafka集群消息的消费:
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092,192.168.56.10:9093,192.168.56.10:9094 --from-beginning --topic my-replicated-topic
带有消费组的kafka集群消息的消费:
./kafka-console-consumer.sh --bootstrap-server 192.168.56.10:9092,192.168.56.10:9093,192.168.56.10:9094 --from-beginning --consumer-property group.id=testGroup --topic my-replicated-topic
第三篇章: java客户端连接kafka
1.生产者的基本实现:
引入依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
public class MyProducer{
private final static String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1.设置参数
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
"192.168.56.10:9092, 192.168.56.10:9093, 192.168.56.10:9094");
// 把发送的key和value从字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 2.创建生产消息的客户端,传入参数
Producer<String, String> producer = new KafkaProducer<String, String>(props);
// 3.创建消息
// key:决定了往哪个分区发(若分区不存在,则自动创建), value:要发送的内容, 未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, "my_keyvalue", "hellokafka");
// 4.同步发送消息,得到消息发送的元数据并输出(应该用try-catch)
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() +
" |partition-" + metadata.partition() + "|offset-" + metadata.offset());
// 4.异步发送方法
// producer.send(producerRecord, new Callback(){
// @Override
// public void onCompletion(RecordMetadata recordMetadata, Exception e) {
// if(e != null){
// System.out.println("消息发送失败:" + e.getStackTrace());
// }
// if(recordMetadata != null){
// System.out.println("异步方式发送消息结果:" + "topic-" + recordMetadata.topic() +
// " |partition-" + recordMetadata.partition() + "|offset-" + recordMetadata.offset());
// }
// }
// });
// Thread.sleep(100000000000000L);
}
}
同步发送:如果生产者发送消息没有收到ack,生产者就会阻塞,等待3s,如果还没有收到ack,就会重发,重发3次还不行就认为发送失败
异步发送:生产者发消息,发送完后不用等待broker给回复,直接执行下面的业务逻辑。可以提供callback,让broker异步的调用callback,告知生产者,消息发送的结果
=======================基本配置==========================
关于生产者的ack参数配置:
★ ack配置
在同步发送的前提下,生产者在获得集群返回的ack之前会阻塞。那么什么时候会返回ack呢?ack有以下三个配置
-ack=0:kafka-cluster不需要任何broker收到消息,立即返回ack给生产者。效率最高,但是最不安全,容易丢失数据;
-ack=1:多副本中的leader收到消息,并把消息写到本地log中,才返回ack给生产者。性能和安全性均衡;
-ack=-1/all:再加上配置min-insync.replicas=2(默认为1,推荐>=2),此时需要leader和一个follower同步完成后,才会返回ack。最安全单性能最差。
props.put(ProducerConfig.ACKS_CONFIG, "1");
★ 重试配置
发送失败后的重试配置,发送失败会重试,默认间隔100ms,默认重试3次
// props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);
// props.put(ProducerConfig.RETRIES_CONFIG,3);
★ 消息缓冲区配置
kafak默认创建一个消息缓冲区,用来存放要发送的消息,默认为32m,kafka本地线程会去缓冲区中一次拉16k的数据,发送到broker,如果拉不到16k,就间隔10ms将已拉到的数据发给broker
// props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
补充:kafka生产者消息分区策略:
轮询分区策略
随机分区策略
按key分区分配celue
自定义分区策略
2.消费者的基本实现:
public class MyConsumer {
private final static String TOPIC_NAME = "my-replicated-topic";
private final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
// 1.设置参数
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
"192.168.56.10:9092, 192.168.56.10:9093, 192.168.56.10:9094");
// 消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 2.创建一个消费者客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// 3.消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
// 4.poll消息
while (true){
// poll()是拉取消息的长循环
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000)); // 每次花1s拉取消息,拉完就往下执行
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s",
record.partition(), record.offset(), record.key(), record.value());
}
}
}
}
=======================基本配置==========================
★ 消费者提交方式 无论是手动还是自动提交,都要把消费者所属消费组+消费主题+分区+偏移量,这些消息提交到集群的_consumer_offsets主题中
自动提交(默认):消费者poll消息下来后就自动提交offset 会丢失消息,因为消费者在消费前提交offset,可能还没有消费消息就挂了
// props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); // 自动提交offset的间隔时间
手动提交:
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
手动同步提交:在消费完消息后调用同步提交的方法,当集群返回ack前一直阻塞,返回ack后表示提交成功,执行之后的逻辑
while (true){
// poll()是拉取消息的长循环
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s",
record.partition(), record.offset(), record.key(), record.value());
}
// 所有消息已消费完
if(records.count() > 0){ // 还有消息
// 手动同步提交offset,然后阻塞到offset提交成功(常用手动同步提交,因为一般提交后也没有什么操作了)
consumer.commitSync();
}
}
手动异步提交:在消费完消息后提交,不需要等到集群ack,直接执行之后的逻辑,可以设置一个回调方法,供集群调用
while (true){
// poll()是拉取消息的长循环
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d, offset = %d, key = %s, value = %s",
record.partition(), record.offset(), record.key(), record.value());
}
// 所有消息已消费完
if(records.count() > 0){ // 还有消息
// 手动异步提交offset,不会阻塞,可以继续执行后面的逻辑
consumer.commitAsync(new OffsetCommitCallback(){
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception){
if(exception != null){
System.err.println("Commit failed for " + offsets);
System.err.println("Commit failed exception: " + exception.getStackTrace());
}
}
})
}
}
★ 长轮询poll消息
默认情况下,消费者一次会poll500条消息
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500); // 一次poll最大拉取消息的条数,可以根据消费速度的快慢来设置
设置poll长轮询的时间为1000ms
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000)); // 每次花1s来拉取消息
只要满足poll的条数要求或者时间要求,就可以继续往下执行for循环。 也就是说,如果在1s内poll的条数达到500条或者到了1s还没有poll到500条消息,会往下执行。
如果两次poll的间隔超过30s,集群会认为该消费者的消费能力过弱,将该消费者踢出消费组,触发rebalance机制(造成性能开销),可以让一次poll的消息条数少一些。
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
★ 消费者的健康状态检查
消费者每隔1s就给kafka集群发送心跳
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
如果集群有10s没接收到心跳, 就把该消费者踢出消费组,触发该组的rebalance机制,将该分区分给该组的其它消费者
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
★ 指定分区、偏移量或时间进行消费
指定分区0进行消费:
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
指定偏移量进行消费:
// consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
// consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0))); //从头开始消费
// consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10); //指定offset从10开始消费
指定时间进行消费:根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到该offset之后的消息开始消费
List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
//从1小时前开始消费
long fetchDataTime = new Date().getTime - 1000 * 60 * 60;
Map<TopicPartition, Long> map = new HashMap<>();
for(PartitionInfo par=topicPartitions){
map.put(new TopicPartition(TOPIC_NAME,par.partition()),fetchDataTime);
}
Map<TopicPartition, OffsetAndTimestamp> parMap=consumer.offsetsForTimes(map);
for(Map.Entry(TopicPartition,OffsetAndTimestamp)entry:parMap.entrySet()){
TopicPartition key=entry.getKey();
OffsetAndTimestamp value=entry.getValue();
if(key==null||value==null)continue;
Long offset=value.offset();
System.out.println("partition-"+key.partition()+"|offset-"+offset);
if(value!=null){
consumer.assign(Arrays.asList(key));
consumer.seek(key,offset);
}
}
★ 新消费组的消费offset规则
新消费组的消费者在启动之后,默认会从当前分区的最后一条消息的offset+1开始消费(新消息)
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") // 默认从新消息开始消费
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") // 第一次从头开始消费,之后开始消费新消息(最后消费的offset+1)