Kafka学习视频心得(三)自定义offset、kafka拦截器、监控、面试题

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端口查看详情

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q6ilisTZ-1612440981874)(../../AppData/Roaming/Typora/typora-user-images/image-20210204192501239.png)]

2)、Kafka Manager(实用)

①、上传压缩包kafka-manager-2.0.0.2.zip到集群(这里版本需要注意,这个版本包括以下kafka版本)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dIVrBtah-1612440981876)(../../AppData/Roaming/Typora/typora-user-images/image-20210204200701707.png)]

②、解压(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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mBVlVL4m-1612440981877)(../../AppData/Roaming/Typora/typora-user-images/image-20210204201008750.png)]

添加Cluster,只需加Name、Zookeeper、version即可,然后下划save

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sPHZ6tAY-1612440981878)(../../AppData/Roaming/Typora/typora-user-images/image-20210204201131182.png)]

5、kafka面试题

  1. Kafka中的ISR、AR又代表什么?
    ISR:与leader保持同步的follower集合
    AR:分区的所有副本

  2. Kafka中的HW、LEO等分别代表什么?
    LEO:没个副本的最后条消息的offset
    HW:一个分区中所有副本最小的offset

  3. Kafka中是怎么体现消息顺序性的?
    每个分区内,每条消息都有一个offset,故只能保证分区内有序。

  4. Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
    拦截器 -> 序列化器 -> 分区器

  5. Kafka生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oEsAc72x-1612440981879)(../../AppData/Roaming/Typora/typora-user-images/image-20210204201419534.png)]

  6. “消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?
    正确

  7. 消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
    offset+1

  8. 有哪些情形会造成重复消费?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xskntlLz-1612440981880)(../../AppData/Roaming/Typora/typora-user-images/image-20210204201439643.png)]

  9. 那些情景会造成消息漏消费?
    先提交offset,后消费,有可能造成数据的重复

  10. 当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
    1)会在zookeeper中的/brokers/topics节点下创建一个新的topic节点,如:/brokers/topics/first
    2)触发Controller的监听程序
    3)kafka Controller 负责topic的创建工作,并更新metadata cache

  11. topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
    可以增加

    bin/kafka-topics.sh --zookeeper localhost:2181/kafka --alter --topic topic-config --partitions 3
    
  12. topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
    不可以减少,现有的分区数据难以处理。

  13. Kafka有内部的topic吗?如果有是什么?有什么所用?
    __consumer_offsets,保存消费者offset

  14. Kafka分区分配的概念?
    一个topic多个分区,一个消费者组多个消费者,故需要将分区分配个消费者(roundrobin、range)

  15. 简述Kafka的日志目录结构?
    每个分区对应一个文件夹,文件夹的命名为topic-0,topic-1,内部为.log和.index文件

  16. 如果我指定了一个offset,Kafka Controller怎么查找到对应的消息?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ilF5trj7-1612440981881)(../../AppData/Roaming/Typora/typora-user-images/image-20210204201519816.png)]

  17. 聊一聊Kafka Controller的作用?
    负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作。

  18. Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
    partition leader(ISR),controller(先到先得)

  19. 失效副本是指什么?有那些应对措施?
    不能及时与leader同步,暂时踢出ISR,等其追上leader之后再重新加入

  20. Kafka的那些设计让它有如此高的性能?
    分区,顺序写磁盘,0-copy

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

友培

数据皆开源!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值