当我们调用 poll 方法时,Kafka 会向我们发送一些消息。假设分区中有 100 条记录。当前偏移量的初始位置为 0。我们进行了第一次调用并收到了 20 条消息。现在 Kafka 会将当前偏移量移动到 20。当我们发出下一个请求时,它会从 20 开始发送更多消息,并再次将当前偏移量向前移动。偏移量是一个简单的整数,Kafka 使用它来维护消费者的当前位置。当前偏移量是指向 Kafka 在最近轮询中已发送给消费者的最后一条记录的指针。因此,由于当前的偏移量,消费者不会两次获得相同的记录。用于避免将相同的记录再次发送给同一个消费者。
现在让我们来提交偏移量,这个偏移量是消费者已经确认的关于处理的位置。提交的偏移量是指向消费者已成功处理的最后一条记录的指针。用于避免在分区重新平衡的情况下将相同的记录重新发送给新的消费者。
bin/kafka-topics.sh --zookeeper hadoop102:2181 --list
bin/kafka-topics.sh --zookeeper hadoop102:2181 --topic topic_name --describe
# 新版
bin/kafka-consumer-groups.sh --bootstrap-server hadoop102:9092 --list
# 老版
bin/kafka-consumer-groups.sh --zookeeper hadoop102:2181 --list
bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list hadoop102:9092 --topic topic_name --time -1
bin/kafka-consumer-groups.sh --bootstrap-server hadoop102:9092,hadoop103:9092 --describe --group group_id_name
(–to-current、–to-datetime ‘YYYY-MM-DDTHH:mm:SS.sss’、–to-earliest、–to-latest、–to-offset
bin/kafka-consumer-groups.sh --bootstrap-server hadoop102:9092,hadoop103:9092 --topic topic_name --group group_id_name --reset-offsets --to-latest
bin/kafka-consumer-groups.sh --bootstrap-server hadoop102:9092,hadoop103:9092 --topic topic_name --group group_id_name --reset-offsets --to-latest --excute
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092,hadoop103:9092 --topic topic_name --group group_id_name
所有副本列表=ISR+OSR(Outof-Sync Replicas)
时间延迟和条数延迟都会被踢出ISR,0.10.x后只有时间延迟,扩展时新加入的也会进入OSR
leader写入新消息,consumer不能立刻消费,leader会等待所有ISR中的replicas同步后跟新HW,消息才可能被consumer消费,这样broker失效后从达到HW的broker中选取leader
新进来的消息先写到一个临时位置进行同步,ack机制确认之后,commit 改变 hw
ISR 保存在zookeeper:/brokers/topics/[topic]/partitions/[partition]/state,有两个地方会对zookeeper这个节点维护:
1.Controller 下的 LeaderSelector 选举新的 leader
2.Leader 有单独的线程定期检测 ISR 中的follower是否脱离 ISR,如果变化,将新的 ISR 的信息返回到 Zookeeper 的相关节点
kafka 集群中一个broker会被选举为 Controller,负责 partition 管理、副本状态管理、重分配partition之类的管理任务
组协调员(Group Coordinator
)负责组中成员的加入和退出。其中一个 Kafka broker 被选为组协调员。当消费者想要加入一个组时,它会向协调器发送请求。第一个加入群组的消费者成为领导者。之后加入的所有其他消费者都成为该组的成员。所以,有两个角色,一个组协调员和一个 Group Leader
。协调员负责管理组成员列表。因此,每次有新成员加入组或现有成员离开组时,组协调员都会修改列表。
Group Leader 负责执行重新 Rebalance 过程。首先获取当前成员的列表,为他们分配分区并将结果返回组协调员,然后协调者将有关他们的新分区的信息反馈给成员。
Rebalance 期间。不允许任何消费者读取任何消息。
ConsumerRecords records = consumer.poll(100);
poll 功能非常强大。它处理所有协调、Rebalance、发送给 Group Coordinator 的心跳、检查是否需要自动提交偏移量。第一次 poll 的时候,会找到一个组协调器,加入该组,接受分区分配并从这些分区中获取一些记录。每次调用 poll 时,它都会向组协调器发送心跳。如果一段时间不轮询,协调器可能会假设消费者已死亡并触发分区重新平衡。
1.自动提交偏移量
在 poll 方法中进行,检查提交时间大于自动提交的时间间隔,则提交。提交的是当前偏移量。
2.手动同步提交
处理两种场景,第一是处理失败,第二是 Rebalance。
3.手动异步提交
commitAsync 异步提交不会重试,因为失败后重新提交,再次提交的 offset 有可能比第一次的 offset 大。
使用 poll 方法获取到了大量数据,处理到一半由于向协调者心跳超时或者增加同组消费者,协调员都会发起 Rebalance。如何做到提交的偏移量中间 offset,而不是 当前 current offset。
触发 rebalance 时提交偏移量:
import java.util.*;
import org.apache.kafka.clients.producer.*;
// 生产者
public class RandomProducer {
public static void main(String[] args) throws InterruptedException {
String topicName = "RandomProducerTopic";
String msg;
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092,localhost:9093");
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);
Random rg = new Random();
Calendar dt = Calendar.getInstance();
dt.set(2016, 1, 1);
try {
while (true) {
for (int i = 0; i < 100; i++) {
msg = dt.get(Calendar.YEAR) + "-" +
dt.get(Calendar.MONTH) + "-" +
dt.get(Calendar.DATE) + "," +
rg.nextInt(1000);
producer.send(new ProducerRecord<String, String>(topicName, 0, "Key", msg)).get();
msg = dt.get(Calendar.YEAR) + "-" +
dt.get(Calendar.MONTH) + "-" +
dt.get(Calendar.DATE) + "," +
rg.nextInt(1000);
producer.send(new ProducerRecord<String, String>(topicName, 1, "Key", msg)).get();
}
dt.add(Calendar.DATE, 1);
System.out.println("Data Sent for " +
dt.get(Calendar.YEAR) + "-" +
dt.get(Calendar.MONTH) + "-" +
dt.get(Calendar.DATE));
}
} catch (Exception ex) {
System.out.println("Intrupted");
} finally {
producer.close();
}
}
}
重平衡监听器
import java.util.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.*;
// 消费者
public class RandomConsumer {
public static void main(String[] args) throws Exception {
String topicName = "RandomProducerTopic";
KafkaConsumer<String, String> consumer = null;
String groupName = "RG";
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092,localhost:9093");
props.put("group.id", groupName);
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("enable.auto.commit", "false");
consumer = new KafkaConsumer<>(props);
RebalanceListner rebalanceListner = new RebalanceListner(consumer);
// 在订阅方法调用中将侦听器对象提供给 Kafka。通过这样做,我们确保 Kafka 将调用侦听器的 onPartitionsRevoked方法。
consumer.subscribe(Arrays.asList(topicName), rebalanceListner);
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
/*System.out.println("Topic:"+ record.topic() +
" Partition:" + record.partition() +
" Offset:" + record.offset() + " Value:"+ record.value());*/
// Do some processing and save it to Database
// 告诉侦听器此特定偏移量已准备好提交。监听器不会立即提交。它只会维护它应该提交的每个分区的每个主题的最新偏移量列表,下次循环重置待提交列表
rebalanceListner.addOffset(record.topic(), record.partition(), record.offset());
}
//consumer.commitSync(rebalanceListner.getCurrentOffsets());
}
} catch (Exception ex) {
System.out.println("Exception.");
ex.printStackTrace();
} finally {
consumer.close();
}
}
}
监听器的职责:
1.维护已处理并准备提交的 offset 列表;
2.当分区消失时提交偏移量。
import java.util.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.*;
// 监听器类必须实现 ConsumerRebalanceListener 接口
public class RebalanceListner implements ConsumerRebalanceListener {
private KafkaConsumer consumer;
// 用于维护偏移量,只会保留主题和分区的最新偏移量。这些偏移量已准备好提交
private Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap();
public RebalanceListner(KafkaConsumer con) {
this.consumer = con;
}
public void addOffset(String topic, int partition, long offset) {
currentOffsets.put(new TopicPartition(topic, partition), new OffsetAndMetadata(offset, "Commit"));
}
public Map<TopicPartition, OffsetAndMetadata> getCurrentOffsets() {
return currentOffsets;
}
// 重新分配分区,rebalance 操作之后调用,获取新的偏移量
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
System.out.println("Following Partitions Assigned ....");
for (TopicPartition partition : partitions)
System.out.println(partition.partition() + ",");
}
// 撤销分区,rebalance 操作之前调用,提交旧偏移量
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("Following Partitions Revoked ....");
for (TopicPartition partition : partitions)
System.out.println(partition.partition() + ",");
System.out.println("Following Partitions commited ....");
for (TopicPartition tp : currentOffsets.keySet())
System.out.println(tp.partition());
consumer.commitSync(currentOffsets);
currentOffsets.clear();
}
}
手动维护偏移量到数据库,而不是提交到 kafka。
create database test;
use test;
create table tss_data(skey varchar(50), svalue varchar(50));
create table tss_offsets(topic_name varchar(50),partition int, offset int);
insert into tss_offsets values('SensorTopic1',0,0);
insert into tss_offsets values('SensorTopic1',1,0);
insert into tss_offsets values('SensorTopic1',2,0);
import java.util.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.*;
import java.sql.*;
public class SensorConsumer {
public static void main(String[] args) throws Exception {
String topicName = "SensorTopic";
KafkaConsumer<String, String> consumer = null;
int rCount;
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092,localhost:9093");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("enable.auto.commit", "false");
consumer = new KafkaConsumer<>(props);
TopicPartition p0 = new TopicPartition(topicName, 0);
TopicPartition p1 = new TopicPartition(topicName, 1);
TopicPartition p2 = new TopicPartition(topicName, 2);
consumer.assign(Arrays.asList(p0, p1, p2));
System.out.println("Current position p0=" + consumer.position(p0)
+ " p1=" + consumer.position(p1)
+ " p2=" + consumer.position(p2));
consumer.seek(p0, getOffsetFromDB(p0));
consumer.seek(p1, getOffsetFromDB(p1));
consumer.seek(p2, getOffsetFromDB(p2));
System.out.println("New positions po=" + consumer.position(p0)
+ " p1=" + consumer.position(p1)
+ " p2=" + consumer.position(p2));
System.out.println("Start Fetching Now");
try {
do {
ConsumerRecords<String, String> records = consumer.poll(1000);
System.out.println("Record polled " + records.count());
rCount = records.count();
for (ConsumerRecord<String, String> record : records) {
saveAndCommit(consumer, record);
}
} while (rCount > 0);
} catch (Exception ex) {
System.out.println("Exception in main.");
} finally {
consumer.close();
}
}
private static long getOffsetFromDB(TopicPartition p) {
long offset = 0;
try {
Class.forName("com.mysql.jdbc.Driver");
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "pandey");
String sql = "select offset from tss_offsets where topic_name='"
+ p.topic() + "' and partition=" + p.partition();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql);
if (rs.next())
offset = rs.getInt("offset");
stmt.close();
con.close();
} catch (Exception e) {
System.out.println("Exception in getOffsetFromDB");
}
return offset;
}
private static void saveAndCommit(KafkaConsumer<String, String> c, ConsumerRecord<String, String> r) {
System.out.println("Topic=" + r.topic() + " Partition=" + r.partition() + " Offset=" + r.offset()
+ " Key=" + r.key() + " Value=" + r.value());
try {
Class.forName("com.mysql.jdbc.Driver");
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "pandey");
con.setAutoCommit(false);
String insertSQL = "insert into tss_data values(?,?)";
PreparedStatement psInsert = con.prepareStatement(insertSQL);
psInsert.setString(1, r.key());
psInsert.setString(2, r.value());
String updateSQL = "update tss_offsets set offset=? where topic_name=? and partition=?";
PreparedStatement psUpdate = con.prepareStatement(updateSQL);
psUpdate.setLong(1, r.offset() + 1);
psUpdate.setString(2, r.topic());
psUpdate.setInt(3, r.partition());
psInsert.executeUpdate();
psUpdate.executeUpdate();
con.commit();
con.close();
} catch (Exception e) {
System.out.println("Exception in saveAndCommit");
}
}
}