Kafka笔记

Kafka

分布式基于发布/订阅模式的消息队列

消息队列

消息队列优点
  • 解耦

    只要确保遵守同样的接口约束,允许独立的扩展或修改两边的处理过程

  • 可恢复性

    系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理

  • 缓冲

    有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况

  • 灵活性

    消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃

  • 异步通信

    消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

两种模式
点对点模式(一对一)
  • 消费者主动拉取数据,消息收到后消息清除

  • 消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息。

  • 消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。

发布/订阅模式(一对多)
  • 消费者消费数据后不会清除消息
  • 消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。

基础架构

1)Producer **:**消息生产者,就是向kafka broker发消息的客户端;

2)Consumer **:**消息消费者,向kafka broker取消息的客户端;

3)Consumer Group (CG):消费者组,由多个consumer组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。

4)Broker **:**一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic。

5)Topic **:**可以理解为一个队列,生产者和消费者面向的都是一个topic;

6)Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列;

7)Replica:副本,为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。

8)leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。

**9)follower:**每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的leader。

  • Kafka集群

    Kafka集群由多个Broker组成,每个Broker拥有唯一Id.Kafka集群中有多个Topic,每个Topic可有多个分区(partition),每个分区可有多个副本(replication)

    • 一个Topic的多个分区可以存在一个Broker中,一个分区的多个副本只能存在不同的Broker中。

    • 一个分区的多个副本由一个leader和多个follower组成

    • 生产者和消费者读写数据面向leader

    • follower主要同步leader数据,当leader故障后,follower代替leader

  • 生产者

    • 生产者往Topic中发布消息
  • 消费者

    • 消费者从topic中消费消息
    • 消费者消费消息以消费者组为单位进行
    • 一个消费者组内的一个消费者可以同时消费一个topic中多个分区的消息
    • 一个topic中的一个分区的消息同时只能被一个消费者组中的一个消费者消费
  • Zookeeper

    • Kafka集群中的工作需要Zookeeper,例如每个broker启动后需要向Zookeeper注册
    • Broker中大哥(controller)的选举(争抢策略)
    • Kafka 0.9版本之前消费者组的offset维护在Zookeeper中,0.9之后维护在Kafka内部

命令

查当前服务器所有topic

kafka-topics.sh --list --bootstrap-server hadoop102:9092

创建topic

kafka-topics.sh --creat --bootstrap-server hadoop102:9092 --topic second --partitions 2 --replication-factor 3

删除topic

kafka-topics.sh --bootstrap-server hadoop102:2181 --delete --topic first

修改topic分区(分区只能改大不能改小,因为分区中可能存在消费者正在读数据)

kafka-topics.sh --describe --bootstrap-server hadoop102:9092 --topic first

生产者

kafka-console-producer.sh --broker-list hadoop102:9092 --topic first

消费者

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

消费者消费数据offset重置问题:

​ 新启动的消费者组消费不到topic中的数据

  • 消费者重置消费

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --from-beginning

指定消费者组

先在consumer.properties中指定group id 或者在命令中指定

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --consumer.config /opt/module/kafka_2.11-2.4.1/config/consumer.properties

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first --group aa

如果有两个分区,组里有两个消费者,则两个消费者轮流消费

如果有两个分区,组里有三个消费者,则有一个消费者休息

工作流程和文件存储机制

  • Kafka中消息是以topic进行分类,生产者生产消息,消费者消费消息,都是面向topic的

  • topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。

  • Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个offset,以便出错恢复时,从上次的位置继续消费

  • Kafka采取了分片索引机制,将每个partition分为多个segment。每个segment对应两个文件——“.index”文件和“.log”文件

  • Kafka不会对每个文件存一个索引,而是采用稀疏索引,多个文件存入一个索引

生产者

分区原因

(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了

(2)可以提高并发,因为可以以Partition为单位读写了

分区原则

将producer发送的数据封装成一个ProducerRecord对象

  • 指明 partition 的情况下,直接将指明的值直接作为 partiton 值;

  • 没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;

  • 既没有 partition 值又没有 key 值的情况下, kafka采用Sticky Partition(黏性分区器),会随机选择一个分区,并尽可能一直使用该分区,待该分区的batch已满或者已完成,kafka再随机一个分区进行使用.

数据可靠性保证
  • ack机制保证数据的可靠性

  • 为什么kafka选的是全部follwer同步完再发送ACK:

    半数的机制:机器数要有2n+1台(挂掉一半还能运行),但是全部同步完,只用要求n+1台

  • ISR

    • in-sync replica set (ISR)
    • 意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR
    • 时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader
  • ack应答

    • acks参数设置

      • 0:提供了一个最低的延迟,partition的leader接收到消息还没有写入磁盘就已经返回ack,当leader故障时有可能丢失数据

        1: partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据

        -1(all): partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复

Log文件的HW和LEO

LEO:指的是每个副本最大的offset;

HW:指的是消费者能见到的最大的offset,ISR队列中最小的LEO。

(1)follower故障

follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

(2)leader故障

leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

Exactly Once

ack -1:Producer到Server之间不会丢失数据, At Least Once. ack 0:生产者每条消息只会被发送一次,At Most Once

幂等性结合At Least Once语义,就构成了Kafka的Exactly Once语义。即:At Least Once + 幂等性 = Exactly Once

幂等性就是指Producer不论向Server发送多少次重复数据,Server端都只会持久化一条

但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once

消费者

消费方式是pull。push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。

分配策略

RoundRobin,Range , Sticky

RoundRobin:轮询分配

Sticky:黏性,第一次分配和RoundRobin一样,再分配(消费者增加/减少)的时候,原有的分配不变,只变增多/减少的部分

Range:范围分区,分区数对消费者取余

offset

consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费

Kafka 0.9版本之前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为**__consumer_offsets**

高效读写数据

  • producer生产数据写入到log文件中,写的过程是追加到文件末端,顺序写。顺序写省去了大量磁头寻址的时间
  • Kafka数据持久化是直接持久化到Pagecache
    • I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
    • I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
    • 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
    • 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
    • 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用
    • 持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决

相关API

Producer
异步
public class KafkaProducerDemo {
    /**
     * 生产者,异步发送,不带回调
     * 配置类:ProducerConfig
     *         ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         可以直接通过ProducerConfig和ConsumerConfig调
     */

    public static void main(String[] args) {
        //0.创建配置对象
        Properties properties = new Properties();
        //kafka集群.broker-list
        properties.put("bootstrap.servers","hadoop102:9092");
        //ack级别
        properties.put("acks","all");
        //批次大小
        properties.put("batch.size",16384);
        // 等待时间
        properties.put("linger.ms",1);
        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory",33554432);
        //kv序列化器
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        // 1. 创建生产者对象<S
        Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
        // 2. 生产数据
        for (int i = 0; i < 10; i++) {
            //1.指定partition
//             kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
            //2.指定key
//            kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
            //3.黏性
            kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
        }
        //3.关闭对象
        kafkaProducer.close();
    }
}
public class KafkaProducerDemo2 {
    /**
     * 生产者,异步发送,带回调
     * 配置类:ProducerConfig
     *         ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         可以直接通过ProducerConfig和ConsumerConfig调
     *
     */

    public static void main(String[] args) {
        //0.创建配置对象
        Properties properties = new Properties();


        //kafka集群.broker-list
        properties.put("bootstrap.servers","hadoop102:9092");
        //ack级别
        properties.put("acks","all");
        //批次大小
        properties.put("batch.size",16384);
        // 等待时间
        properties.put("linger.ms",1);
        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory",33554432);
        //kv序列化器
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        // 1. 创建生产者对象<S
        Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
        // 2. 生产数据
        for (int i = 0; i < 10; i++) {
            //1.指定partition
//             kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
            //2.指定key
//            kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
            //3.黏性
//            kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
            kafkaProducer.send(new ProducerRecord<String, String>("second", "jojojojojo" + i), new Callback() {
                /**
                 * 消息发送完成后,会调用此方法
                 * @param recordMetadata 消息的元数据
                 * @param e 当消息发送过程中,如果抛出异常,会传入该方法
                 */
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e!=null)
                        System.out.println("消息发送失败:"+e.getMessage());
                    else System.out.println("消息发送成功"+recordMetadata.topic()+";"+recordMetadata.partition()+";"+
                            recordMetadata.offset());
                }
            });
        }
        //3.关闭对象
        kafkaProducer.close();
    }
}
同步

同步和异步的区别,对线程进行了阻塞,使用回调函数,当消息成功发送后,程序再继续向下执行

public class KafkaProducerDemo3 {
    /**
     * 生产者,同步发送,带回调
     * 配置类:ProducerConfig
     *         ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         可以直接通过ProducerConfig和ConsumerConfig调
     */
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //0.创建配置对象
        Properties properties = new Properties();
        //kafka集群.broker-list
        properties.put("bootstrap.servers","hadoop102:9092");
        //ack级别
        properties.put("acks","all");
        //批次大小
        properties.put("batch.size",16384);
        // 等待时间
        properties.put("linger.ms",1);
        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory",33554432);
        //kv序列化器
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        // 1. 创建生产者对象<S
        Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
        // 2. 生产数据
        for (int i = 0; i < 10; i++) {
            //1.指定partition
//             kafkaProducer.send(new ProducerRecord<String, String>("second",0,null,"jojo"+i));
            //2.指定key
//            kafkaProducer.send(new ProducerRecord<String, String>("second","1","==jojo=="+i));
            //3.黏性
//            kafkaProducer.send(new ProducerRecord<String, String>("second","==jojo=="+i));
            Future<RecordMetadata> second = kafkaProducer.send(new ProducerRecord<String, String>("second", "jojojojojo" + i), new Callback() {
                /**
                 * 消息发送完成后,会调用此方法
                 * @param recordMetadata 消息的元数据
                 * @param e 当消息发送过程中,如果抛出异常,会传入该方法
                 */
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e != null)
                        System.out.println("消息发送失败:" + e.getMessage());
                    else System.out.println("消息发送成功" + recordMetadata.topic() + ";" + recordMetadata.partition() + ";" +
                            recordMetadata.offset());
                }
            });
            System.out.println("=================");
            //阻塞当前线程,一直等到该方法的结果返回为止
            RecordMetadata recordMetadata = second.get();
            System.out.println("========消息发送完成========");
        }
        //3.关闭对象
        kafkaProducer.close();
    }
}
指定分区器
public class KafkaProducerDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //0.创建配置对象
        Properties properties = new Properties();

        //kafka集群.broker-list
        properties.put("bootstrap.servers","hadoop102:9092");
        //ack级别
        properties.put("acks","all");
        //批次大小
        properties.put("batch.size",16384);
        // 等待时间
        properties.put("linger.ms",1);
        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory",33554432);
        //kv序列化器
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        // 配置设定自定义的partition
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.jojo.partition.MyPartitioner");

        // 1. 创建生产者对象
        Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
        // 2. 生产数据
        String va;
        for (int i = 0; i < 10; i++) {

            if (i%2==0){
                va="jojo";
            }
            else {
                va=new String(UUID.randomUUID().toString());
            }
            Future<RecordMetadata> second = kafkaProducer.send(new ProducerRecord<String, String>("second", va + i), new Callback() {
                /**
                 * 消息发送完成后,会调用此方法
                 * @param recordMetadata 消息的元数据
                 * @param e 当消息发送过程中,如果抛出异常,会传入该方法
                 */
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e != null)
                        System.out.println("消息发送失败:" + e.getMessage());
                    else System.out.println("消息发送成功" + recordMetadata.topic() + ";" + recordMetadata.partition() + ";" +
                            recordMetadata.offset());
                }
            });
            System.out.println("=================");
            //阻塞当前线程,一直等到该方法的结果返回为止
            RecordMetadata recordMetadata = second.get();
            System.out.println("========消息发送完成========");
        }
        //3.关闭对象
        kafkaProducer.close();
    }
}

public class MyPartitioner implements Partitioner {
    /**
     * 消息含有jojo的进入0号分区
     * valueBytes是要判断的东西
     * @param topic The topic name
     * @param key The key to partition on (or null if no key)
     * @param keyBytes The serialized key to partition on( or null if no key)
     * @param value The value to partition on or null
     * @param valueBytes The serialized value to partition on or null
     * @param cluster The current cluster metadata
     * @return
     */
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        String data = valueBytes.toString();
        int partition;
        if (data.contains("jojo"))
            partition=0;
        else partition=1;
        return partition;
    }

    public void close() {
    }
    public void configure(Map<String, ?> configs) {
    }
}
Consumer
public class KafkaConsumerDemo {
    public static void main(String[] args) {
        // 0. 创建配置
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092");
        properties.put("group.id", "bb");
        properties.put("enable.auto.commit", "true");
        properties.put("auto.commit.interval.ms", "1000");
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 1. 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
        // 2. 订阅主题
        ArrayList<String> topics = new ArrayList<String>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);
        //3. 持续消费数据
        while (true){
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
            }
        }
    }
}
offset
offset重置
/**
 * offset重置问题
 * 重置的情况:
 *    1.新的组当前消费组在kafka中没有消费记录
 *    auto.offset.reset    Valid Values:	[latest, earliest, none]
 *    2.要消费的offset对应的消息已经被删除
 */
public class KafkaConsumerDemo2 {
    public static void main(String[] args) {
        // 0. 创建配置
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092");
        properties.put("group.id", "bb");
        properties.put("enable.auto.commit", "true");
        properties.put("auto.commit.interval.ms", "1000");
//        properties.put("enable.auto.commit", "false");//关闭自动提交offset
//        auto.offset.reset    Valid Values:	[latest, earliest, none]
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 1. 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
        // 2. 订阅主题
        ArrayList<String> topics = new ArrayList<String>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);
        //3. 持续消费数据
        while (true){
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
            }
        }
    }
}
offset提交方式

手动提交和自动提交的区别:

当producer生产了第一波数据之后

手动提交和自动提交都能正常消费数据,手动提交是自己记录offset,自动提交会把offset提交到producer

此时,在不关闭consumer的情况下,producer生产第二波数据还是正常消费

但如果此时把两个consumer关了,再重新启动

手动提交的consumer会把以前(包括第一波)的数据全部消费一遍

自动提交的consumer只会消费第二波的数据

自动提交
/**
 * offset提交
 * 1.自动提交
 * enable.auto.commit   true
 * auto.commit.interval.ms  1000
 *
 * 2.手动提交
 * enable.auto.commit   false
 *
 */
public class KafkaConsumerDemo3 {
    public static void main(String[] args) {
        // 0. 创建配置
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092");
        properties.put("group.id", "bb");
        // 自动提交offset
        properties.put("enable.auto.commit", "true");
        // offset提交的问题
        properties.put("auto.commit.interval.ms", "1000");

        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 1. 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
        // 2. 订阅主题
        ArrayList<String> topics = new ArrayList<String>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);
        //3. 持续消费数据
        while (true){
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
            }
        }
    }
}
手动提交

手动提交分为:

/**
 * offset提交
 * 1.自动提交
 * enable.auto.commit   true
 * auto.commit.interval.ms  1000
 *
 * 2.手动提交
 * enable.auto.commit   false
 *
 */
public class KafkaConsumerDemo3 {
    public static void main(String[] args) {
        // 0. 创建配置
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "hadoop102:9092");
        properties.put("group.id", "bb");
        // 自动提交offset
        properties.put("enable.auto.commit", "false");

        // offset提交的问题
        properties.put("auto.commit.interval.ms", "1000");

        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 1. 创建消费者对象
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
        // 2. 订阅主题
        ArrayList<String> topics = new ArrayList<String>();
        topics.add("first");
        topics.add("second");
        kafkaConsumer.subscribe(topics);
        //3. 持续消费数据
        while (true){
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofSeconds(2));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println("消费到:"+record.topic()+";"+record.partition()+";"+record.key()+";"+record.value());
            }
            // 同步提交offset
//            kafkaConsumer.commitSync();
//            异步提交offset 有一个callback
            kafkaConsumer.commitAsync(new OffsetCommitCallback() {
                // 当offset提交完成后会调用该方法
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    System.out.println("offset提交结束");
                }
            });
        }
    }
}

commitSync(同步提交)和commitAsync(异步提交

无论是同步提交还是异步提交offset,都有可能会造成数据的漏消费或者重复消费。

先提交offset后消费,有可能造成数据的漏消费;而先消费后提交offset,有可能会造成数据的重复消费

所以需要通过mysql之类的组件把消费和提交做成事务

拦截器

对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等

同时,producer允许用户指定多个interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。

Intercetpor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor

// 拦截器producer
public class KafkaProducerDemo4 {
    /**
     * 生产者,异步发送,不带回调
     * 配置类:ProducerConfig
     *         ProducerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG;
     *         可以直接通过ProducerConfig和ConsumerConfig调
     */

    public static void main(String[] args) {
        //0.创建配置对象
        Properties properties = new Properties();

        //kafka集群.broker-list
        properties.put("bootstrap.servers","hadoop102:9092");
        properties.put("acks","all");
        properties.put("batch.size",16384);
        properties.put("linger.ms",1);
        properties.put("buffer.memory",33554432);
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");

        // 设置拦截器
        List<String>inters=new ArrayList<>();
        inters.add("com.jojo.kafka.interceptor.MyTimeInterceptor");
        inters.add("com.jojo.kafka.interceptor.MyCountInterceptor");
        properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,inters);

        // 1. 创建生产者对象
        Producer<String, String> kafkaProducer = new KafkaProducer<String,String>(properties);
        // 2. 生产数据
        for (int i = 0; i < 10; i++) {
            //3.黏性
            kafkaProducer.send(new ProducerRecord<String, String>("second","333==jojo==***"+i));
        }
        //3.关闭对象
        kafkaProducer.close();
    }
}
// 拦截器1
public class MyTimeInterceptor implements ProducerInterceptor<String,String> {
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        //1.获取消息的value
        String value = record.value();
        String res = System.currentTimeMillis()+"-->"+value;
        //2.重新构建新的消息对象
        ProducerRecord<String, String> stringStringProducer = new ProducerRecord<String, String>(record.topic(), record.partition(), record.key(), res);
        return stringStringProducer;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }

    @Override
    public void close() {
    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
    /**
     * 拦截器的核心处理方法
     * @param record 被拦截处理的消息
     * @return
     */
}

//拦截器2
public class MyCountInterceptor implements ProducerInterceptor<String,String> {
    private int success;
    private int fail;
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return record;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception!=null){
            fail++;
        }
        else {
            success++;
        }
    }

    @Override
    public void close() {
        System.out.println("success:"+success+";fail:"+fail);
    }

    @Override
    public void configure(Map<String, ?> configs) {
    }
}

Kafka和Flume

Kafka source:从Kafka中读数据

  • 对于Flume来讲,是一个Source角色,对于Kafka来讲,是一个消费者角色

Kafka Sink:把数据存入Kafka

  • 对于Flume来讲,是Sink角色,对于Kafka来讲,是生产者角色

Kafka Channel:把Kafka作为缓冲

  • The Kafka channel can be used for multiple scenarios:

    • With Flume source and sink - it provides a reliable and highly available channel for events【xxxSource->KafkaChannel->xxxSink】

    • With Flume source and interceptor but no sink - it allows writing Flume events into a Kafka topic, for use by other apps

      【xxxSource->KafkaChannel】

    • With Flume sink, but no source - it is a low-latency, fault tolerant way to send events from Kafka to Flume sinks such as HDFS, HBase or Solr

      【KafkaChannel->xxxSink】

Flume接Kafka

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5FdxgTa1-1680873643258)(.\Flume接Kafka.png)]

分析:选用netcat Source,Memory Channel,Kafka Sink

netcat-flume-kafka.conf

#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666


#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 10000
a1.channels.c1.transactionCapacity = 100

#Sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = flumetopic
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.useFlumeEventFormat=true
a1.sinks.k1.kafka.producer.acks = -1

#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1

flume-ng agent -c $FLUME_HOME/cong -f $FLUME_HOME/jobs/kafka/netcat-flume-kafka.conf -n a1 -Dflume.root.logger=INFO,console

2.包含“atguigu”的数据发给topicat主题,包含“shangguigu”的数据发送topicst主题,其他数据发给topicother主题

分析:在flume中使用拦截器,添加header

再使用多路选择器分别放入不同的Channel 和Sink

或者

在Sink中根据header中的值直接传入不同的topic【Kafka Sink uses the topic and key properties from the FlumeEvent headers to send events to Kafka.】

在这里插入图片描述

步骤

1.编写拦截器

public class DataValueInterceptor implements Interceptor {
    @Override
    public void initialize() {
    }

    @Override
    public Event intercept(Event event) {
        Map<String, String> headers = event.getHeaders();
        String body = new String(event.getBody(), StandardCharsets.UTF_8);
        if (body.contains("at")){
            headers.put("topic","topicat");
        }
        else if (body.contains("st")){
            headers.put("topic","topicst");
        }
        return event;
    }

    @Override
    public List<Event> intercept(List<Event> list) {
        for (Event event : list) {
            intercept(event);
        }
        return list;
    }

    @Override
    public void close() {
    }
    public static class MyBuilder implements Builder{
        @Override
        public Interceptor build() {
            return new DataValueInterceptor();
        }

        @Override
        public void configure(Context context) {
        }
    }
}

2.上传至flume/lib

3.编写flume config

touch netcat-flume-kafka-topic.conf

#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666

#Interceptor
a1.sources.r1.interceptors=i1
a1.sources.r1.interceptors.i1.type=com.jojo.kafka.flumeInterceptor.DataValueInterceptor$MyBuilder

#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 10000
a1.channels.c1.transactionCapacity = 100

#Sink
a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.k1.kafka.topic = topicother
a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.sinks.k1.kafka.flumeBatchSize = 20
a1.sinks.k1.useFlumeEventFormat=true
a1.sinks.k1.kafka.producer.acks = -1

#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1

4.启动topicat,topicst,topicother

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicat

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicst

kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic topicother

flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/netcat-flume-kafka-topic.conf -n a1 -Dflume.root.logger=INFO,console

kafka接flume

在这里插入图片描述

分析:把Kafka作为source

编写flume config

vim kafka-flume-logger.conf

#Named
a1.sources=r1
a1.channels=c1
a1.sinks=k1

#Source
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.batchSize=100
a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092
a1.sources.r1.kafka.topics = first
a1.sources.r1.kafka.consumer.group.id = flume


#Channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

#Sink
a1.sinks.k1.type = logger

#bind
a1.sources.r1.channels = c1
a1.sinks.k1.channel=c1

4.往topic first中生产数据

启动flume

flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/kafka-flume-logger.conf -n a1 -Dflume.root.logger=INFO,console

kafkaChannel接xxxSink

分析:没有source,用KafkaChannel接sink

编写flume config

vim kafkachannel-flume-logger.conf

#Named
a1.channels=c1
a1.sinks=k1

#Channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.channels.c1.kafka.topic = first 
a1.channels.c1.kafka.consumer.group.id = flume
a1.channels.c1.kafka.consumer.auto.offset.reset = latest
a1.channels.c1.parseAsFlumeEvent = false

#Sink
a1.sinks.k1.type = logger

#bind
a1.sinks.k1.channel=c1

4.往topic first中生产数据

启动flume

flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/kafkachannel-flume-logger.conf -n a1 -Dflume.root.logger=INFO,console

xxxSource接kafkaChannel

分析:用其他的source接KafkaChannel

编写flume config

vim netcat-flume-kafkachannel.conf

#Named
a1.sources=r1
a1.channels=c1

#Source
a1.sources.r1.type = netcat
a1.sources.r1.bind = 0.0.0.0
a1.sources.r1.port = 6666 

#Channel
a1.channels.c1.type = org.apache.flume.channel.kafka.KafkaChannel
a1.channels.c1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092
a1.channels.c1.kafka.topic = first 
a1.channels.c1.parseAsFlumeEvent = false


#bind
a1.sources.r1.channel=c1

4.往topic first中生产数据

启动flume

flume-ng agent -c $FLUME_HOME/conf -f $FLUME_HOME/job/kafka/netcat-flume-kafkachannel.conf -n a1 -Dflume.root.logger=INFO,console

kafka监控

使用kafka-eagle

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值