1、自定义保存offset
关于代码手写的补充:新加进消费者(consumer)不应该从0开始消费而应该从之间消费者消费完的地方开始消费,这个工作本来之间是由kafka服务器告知的,但是现在我们采用自定义的方式,kafkaserver不知道消费位置信息了,所以我们需要手动写代码。
ConsumerManual.java
public class ConsumerManual {
private static Map<TopicPartition, Long> offset = new HashMap<TopicPartition, Long>();
private static String file = "e:/offset";
public static void main(String[] args) throws IOException, InterruptedException {
//1、实例化consumer对象
Properties properties = new Properties();
properties.load(Consumer.class.getClassLoader().getResourceAsStream("consumer1.properties"));
KafkaConsumer<String, String> consumer =
new KafkaConsumer<String, String>(properties);
//2、订阅话题,拉取消息
consumer.subscribe(Collections.singleton("first"),
new ConsumerRebalanceListener() {
//分区分配之间做的事
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
//提交旧的offset
commit();
}
//分区分配之后做的事
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
//获取新的offset
readOffset(partitions);
for (TopicPartition partition : partitions) {
Long os = offset.get(partition);
if (os == null) {//如果parttion没有被消费过从头开始
consumer.seek(partition, 0);//seek方法表示从哪消费
} else {//如果parttion消费过那就从之前消费结束位置开始
consumer.seek(partition, os);
}
}
}
});
while (true) {
ConsumerRecords<String, String> records = consumer.poll(2000);
//原子绑定
{
for (ConsumerRecord<String, String> record : records) {
//消费
System.out.println(record);
offset.put(new TopicPartition(record.topic(), record.partition()), record.offset());
}
commit();
}
}
}
/**
* 从自定义介质中读取offset到缓存(map)
*
* @param partitions
*/
private static void readOffset(Collection<TopicPartition> partitions) {
ObjectInputStream objectInputStream = null;
Map<TopicPartition, Long> temp;
try {
//开流
objectInputStream = new ObjectInputStream(new FileInputStream(file));
temp = (Map<TopicPartition, Long>) objectInputStream.readObject();
} catch (Exception e) {
temp = new HashMap<TopicPartition, Long>();
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//从全部分区offset中读取我们分配到的分区的offset
for (TopicPartition partition : partitions) {
offset.put(partition, temp.get(partition));
}
}
/**
* 将缓存中的offset提交到自定义介质中
*
* @param
*/
private static void commit() {
//1、先从文件中读取旧的所有offset
ObjectInputStream objectInputStream = null;
Map<TopicPartition, Long> temp;
try {
//开流
objectInputStream = new ObjectInputStream(new FileInputStream(file));
temp = (Map<TopicPartition, Long>) objectInputStream.readObject();
} catch (Exception e) {
temp = new HashMap<TopicPartition, Long>();
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//2、合并我们的offset
temp.putAll(offset);
//3、将新的offset写出去
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(temp);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2、自定义Interceptor
1)、原理
Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑。
对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,producer允许用户指定多个interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。Intercetpor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:
(1)configure(configs)
获取配置信息和初始化数据时调用。
(2)onSend(ProducerRecord):
该方法封装进KafkaProducer.send方法中,即它运行在用户主线程中。Producer确保在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响目标分区的计算。
(3)onAcknowledgement(RecordMetadata, Exception):
该方法会在消息从RecordAccumulator成功发送到Kafka Broker之后,或者在发送过程中失败时调用。并且通常都是在producer回调逻辑触发之前。onAcknowledgement运行在producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率。
(4)close:
关闭interceptor,主要用于执行一些资源清理工作
如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个interceptor,则producer将按照指定顺序调用它们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。
2)、实操
TimeInterceptor.java
/**
* 自定义前缀
*/
public class TimeInterceptor implements ProducerInterceptor<String, String> {
private String prefix;
/**
* 自定义Record
*
* @param record 原始Record
* @return 修改后的Record
*/
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
Long timestamp = record.timestamp();
return new ProducerRecord<String, String>(
record.topic(),
record.partition(),
record.timestamp(),
record.key(),
prefix + record.value(),
record.headers()
);
}
/**
* 收到 ACK以后调用
*
* @param metadata
* @param exception
*/
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
/**
* 关闭Producer时候调用
*/
@Override
public void close() {
}
/**
* 定义拦截器的方法
*
* @param configs
*/
@Override
public void configure(Map<String, ?> configs) {
prefix = (String) configs.get("prefix");
}
}
CountInterceptor.java
/**
* 数一下消息发送成败的数量
*/
public class CountInterceptor implements ProducerInterceptor<String,String> {
private long success=0;
private long fail=0;
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return record;
}
/**
* 收到ACK后做计数
* @param metadata
* @param exception
*/
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
if(exception==null){
success++;
}else{
fail++;
}
}
/**
* 发送完成后输出结果
*/
@Override
public void close() {
System.out.println("成功了"+success+"条");
System.out.println("失败了"+fail+"条");
}
@Override
public void configure(Map<String, ?> configs) {
}
}
Producer.java
public class Producer {
public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
//1、实例化kafka集群
Properties properties = new Properties();
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("acks", "all");
properties.setProperty("bootstrap.servers", "test:9092");
properties.setProperty("prefix", "xiaoyoupei");
List<String> interceptors = new ArrayList<String>();
interceptors.add("com.atguigu.interceptor.TimeInterceptor");
interceptors.add("com.atguigu.interceptor.CountInterceptor");
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//2、用集群对象发送是数据
for (int i = 0; i < 10; i++) {
Future<RecordMetadata> future = producer.send(new ProducerRecord<String, String>(
"first",
Integer.toString(i),
"Value" + i
),
//回调函数
new Callback() {
/**
* 当我们的send收到服务器的ack以后,会调用onCompletion方法
* @param metadata 消息发送到那个分区,传递的元数据的返回
* @param exception 发送失败返回exception
*/
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(metadata);
}
}
});
//RecordMetadata recordMetadata = future.get();//同步 不加就是异步
System.out.println("发完了" + i + "条");
}
//3、关闭资源
producer.close();
}
}
Consumer.java
public class Consumer {
public static void main(String[] args) throws IOException, InterruptedException {
//1、实例化consumer对象
Properties properties = new Properties();
properties.load(Consumer.class.getClassLoader().getResourceAsStream("consumer1.properties"));
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//2、用这个对象接收消息
//Collections.singleton单例集合
consumer.subscribe(Collections.singleton("first"));
while(true){
//从订阅的话题中拉取数据,后接拉取的时间(这边设置超过2s失败)
ConsumerRecords<String, String> poll = consumer.poll(2000);
if(poll.count()==0){
Thread.sleep(100);
}
//消费拉取的数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println(record);
}
//consumer.commitSync();//同步提交
consumer.commitAsync();//异步提交
}
//3、关闭consumer
//consumer.close();
}
}
执行生产者查看消费者的变化
3、flume对接Kafka
1)、配置flume(flume-kafka.conf)(flume1.9.0)
# define
a1.sources = r1
a1.sinks = k1
a1.channels = c1
# source
a1.sources.r1.type = exec
a1.sources.r1.command = tail -F -c +0 /opt/module/datas/flume.log
a1.sources.r1.shell = /bin/bash -c
# sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.topic = first
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.kafka.producer.acks = 1
a1.sinks.k1.kafka.producer.linger.ms = 1
# channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
2) 启动kafkaIDEA消费者
3) 进入flume根目录下,启动flume
$ bin/flume-ng agent -c conf/ -n a1 -f jobs/flume-kafka.conf
4) 向 /opt/module/datas/flume.log里追加数据,查看kafka消费者消费情况
$ echo hello >> /opt/module/datas/flume.log
4、Kafka监控
1)、Kafka Monitor
①、上传jar包KafkaOffsetMonitor-assembly-0.4.6.jar到集群
②、在Kafka目录下创建kafka-offset-console文件夹
③、将上传的jar包放入刚创建的目录下
④、在当前目录下创建启动脚本start.sh
#!/bin/bash
nohup java -cp KafkaOffsetMonitor-assembly-0.4.6-SNAPSHOT.jar \
com.quantifind.kafka.offsetapp.OffsetGetterWeb \
--offsetStorage kafka \
--kafkaBrokers test:9092,test1:9092,test2:9092 \
--kafkaSecurityProtocol PLAINTEXT \
--zk test:2181,test1:2181,test2:2181 \
--port 8086 \
--refresh 10.seconds \
--retain 2.days \
--dbName offsetapp_kafka >/dev/null 2>&1 &
echo $! > pid
⑤、在当前目录创建mobile-logs文件夹
⑥、启动脚本
⑦、登录页面test:8086端口查看详情
2)、Kafka Manager(实用)
①、上传压缩包kafka-manager-2.0.0.2.zip到集群(这里版本需要注意,这个版本包括以下kafka版本)
②、解压(unzip kafka-manager-2.0.0.2.zip)
③、修改配置文件conf/application.conf
kafka-manager.zkhosts="kafka-manager-zookeeper:2181"
修改为:
kafka-manager.zkhosts="test:2181,test1:2181,test2:2181"
④、运行bin/kafka-manager,如果没有执行权限加执行权限
⑤、查看网页test:9000
添加Cluster,只需加Name、Zookeeper、version即可,然后下划save
5、kafka面试题
Kafka中的ISR、AR又代表什么?
ISR:与leader保持同步的follower集合
AR:分区的所有副本Kafka中的HW、LEO等分别代表什么?
LEO:没个副本的最后条消息的offset
HW:一个分区中所有副本最小的offsetKafka中是怎么体现消息顺序性的?
每个分区内,每条消息都有一个offset,故只能保证分区内有序。Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
拦截器 -> 序列化器 -> 分区器Kafka生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?
“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?
正确消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
offset+1有哪些情形会造成重复消费?
那些情景会造成消息漏消费?
先提交offset,后消费,有可能造成数据的重复当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
1)会在zookeeper中的/brokers/topics节点下创建一个新的topic节点,如:/brokers/topics/first
2)触发Controller的监听程序
3)kafka Controller 负责topic的创建工作,并更新metadata cachetopic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
可以增加bin/kafka-topics.sh --zookeeper localhost:2181/kafka --alter --topic topic-config --partitions 3
topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
不可以减少,现有的分区数据难以处理。Kafka有内部的topic吗?如果有是什么?有什么所用?
__consumer_offsets,保存消费者offsetKafka分区分配的概念?
一个topic多个分区,一个消费者组多个消费者,故需要将分区分配个消费者(roundrobin、range)简述Kafka的日志目录结构?
每个分区对应一个文件夹,文件夹的命名为topic-0,topic-1,内部为.log和.index文件如果我指定了一个offset,Kafka Controller怎么查找到对应的消息?
聊一聊Kafka Controller的作用?
负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作。Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
partition leader(ISR),controller(先到先得)失效副本是指什么?有那些应对措施?
不能及时与leader同步,暂时踢出ISR,等其追上leader之后再重新加入Kafka的那些设计让它有如此高的性能?
分区,顺序写磁盘,0-copy