bingmous

欢迎交流,不吝赐教~

导航

Kafka学习笔记(整理)

Kafka概述

  • 定义:分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。
  • 消息队列的好处:
    • 解耦:允许独立的扩展或修改数据生产者消费者,只需要确保同样的接口约束。
    • 可恢复性:一部分组件失效,不会影响整个系统。消息队列降低进程间的耦合度,即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后处理。
    • 缓冲:有助于控制和优化数据流经过系统的速度,解决生产消息和处理消息速度不一致的情况。
    • 灵活性、峰值处理能力
    • 异步通信:允许把消息放入队列,并不立即处理,需要的时候再去处理。
  • 消息队列的两种模式:
    • 点对点模式:一对一,消费者拉数据;
    • 发布订阅模式:一对多,(队列推 - 推的速度可能不匹配消费者接收速度,消费者崩溃或者资源浪费、消费者拉 - 需要不断的轮询浪费资源)
  • Kafka基础架构,依赖于ZK,Kafka是用scala写的
    • producer,消息生产者,向kafka broker发消息的客户端
    • consumer,消息消费者,从kafka broker取消息的客户端
    • consumer group,消费者组,由多个消费者组成,订阅同一个topic,每个消费者负责消费不同分区的数据,一个分区只能由一个消费者消费;消费者组之间互不影响,消费者组是逻辑上的一个订阅者
    • broker,一台服务器就是一个broker,一个集群有多个broker组成,一个broker可以容纳多个topic
    • topic,可以理解为一个队列,(消费者组是逻辑上的一个topic的订阅者)
    • partition,为了实现扩展性,一个topic可以分为多个partition,每个partition是一个有序队列,提高读写的并发
    • replica,副本,每个partition有多个副本
    • leader,每个分区的leader,生产者发送数据和消费者消费数据的对象都是leader
    • follower,每个分区的follower,与leader同步数据,leader发生故障时,某个follower会成为新的leader

Kafka快速入门

安装部署

  • 修改conf/server.properties:分发,其他节点需要配置broker.id不重复
################需要配置内容#######################
# broker的全局唯一编号 不能重复
broker.id=102
# 删除topic功能使能
delete.topic.enable=true
# kafka运行数据存放的路径 存放的是数据 不是日志日志在logs里
log.dirs=/opt/module/kafka/data
# 配置连接Zookeeper集群地址 kafka依赖于ZK
zookeeper.connect=localhost102:2181,localhost103:2181,localhost104:2181
#######################################
# broker的全局唯一编号 不能重复
broker.id=0
# 删除topic功能使能
delete.topic.enable=true
# 处理网络请求 的 线程数量
num.network.threads=3
# 用来处理磁盘IO的现成数量
num.io.threads=8
# 发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
# 接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
# 请求套接字的缓冲区大小
socket.request.max.bytes=104857600
# kafka运行日志存放的路径
log.dirs=/opt/module/kafka/logs 
# topic 在当前broker上的分区个数
num.partitions=1
# 用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# segment文件保留的最长时间,超时将被删除
log.retention.hours=168
# 配置连接Zookeeper集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181
  • 启动、关闭
# 启动:每一台都要启动,实际使用最好配置绝对路径
bin/kafka-server-start.sh -daemon config/server.properties # 使用配置的信息启动
# 关闭:
bin/kafka-server-stop.sh
  • 群起脚本
#!/bin/bash
case $1 in
"start"){
        for i in localhost102 localhost103 localhost104
        do
                echo "------------$i-------------------"
                ssh $i "/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties"
        done
};;
"stop"){
        for i in localhost102 localhost103 localhost104
        do
                echo "------------$i-------------------"
                ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh"
        done
};;
esac
  • kafka命令行操作
# 查看所有命令
bin/kafka-topics.sh --zookeeper localhost102:2181

# 列出所有Topics
bin/kafka-topics.sh --zookeeper localhost102:2181 --list

# 创建Topic
bin/kafka-topics.sh --zookeeper localhost102:2181 --create --topic first --partitions 2 --replication-factor 2

# 删除Topic # delete.topic.enable=true否则只是标记删除
bin/kafka-topics.sh --zookeeper localhost102:2181 --delete --topic first

# 发送消息
bin/kafka-console-producer.sh --broker-list localhost102:9092 --topic first

# 消费消息,0.9版本之后offset存放在kafka里而不是zookeeper,使用bootstrap-server,会在本地生成50个内部topoc,存储offset
bin/kafka-console-consumer.sh --bootstrap-server localhost102:9092 --topic first
bin/kafka-console-consumer.sh --zookeeper localhost102:2181 --topic second --from-beginning

# 查看Topic
bin/kafka-topics.sh --zookeeper localhost102:2181 --describe --topic first

# 修改分区数 只能增加
bin/kafka-topics.sh --zookeeper localhost102:2181 --alter --topic first --partitions 3

Kafka架构深入

Kafka 工作流程及文件存储机制

- 消息追加到.log文件,为防止.log文件过大导致数据定位效率低下,采取分片和索引机制。一个Topic分为多个partition,每个partition分为多个segment(最多1G),每个partition在物理上以topic-partition命名,存储.log和.index文件,通过.index找到每条消息在.log中的索引位置,.index每个索引大小相同,查找速度非常快。

Kafka 生产者

  • 分区策略

    • 分区原因:1> 方便扩展:Partition可以通过调整以适应它所在的机器,而一个topic有多个partition,因此整个集群可以适应任意大小数据。2> 提高并发:可以以partition为单位读写
    • 分区原则:会将数据封装为ProducerRecord对象,1> 指明分区,2> 没有指明分区,但是有key,根据key的hash值对分区数取余得到分区值,3> 没有指明分区,没有key,第一次随机选一个分区,以后都是轮询
  • 数据可靠性保证

    • 为保证producer向partition发送数据能可靠的到达指定topic,producer收到ack之后发送下一轮,否则重发。(异步发送,会一直发送新消息,另外会有一个线程接收ack,ack没收到会重启,)
    • 副本同步策略:一个partition的所有副本全部同步才会发送ack,(虽然延迟高,但对kafka影响较小)
    • ISR:为避免有个别follower迟迟不能同步造成leader一直等下去,leader维护一个动态的in-sync replica set (ISR),意为与leader同步的follower,replica.lag.time.max.ms设定阈值时间,阈值时间内未与leader同步被踢出ISR,leader发生故障后,从ISR中选举新的leader,(0.9版本以前还有一个条件,是follower和leader同步的条数小于某个阈值被移出ISR,这样容易造成flowever频繁的被移入移出ISR,频繁读写ZK,消息的读写是按批次的,比如一次批量写入leader12条,阈值为10条,那么一开始所有ISR都会被移出)
    • ack应答机制:acks参数可以配置0、1、-1,
      • 0表示producer不等待ack,leader一接收(写入socket buffer)还没有写入磁盘就返回,故障时有可能丢失数据
      • 1表示leader落盘后返回ack,如果follower同步之前发生故障,有可能丢失数据
      • -1表示所有follower(ISR)落盘后返回ack,如果follower同步之后,broker返回ack之前发生故障,有可能数据重复,如果ISR只要leader自己,也有可能丢数据
    • 故障处理细节:log文件中有HW(消费者能见的最大的offset,队列中最小的offset)和LEO(每个副本最大的offset)两个offset,来保证所有的follower和leader数据是同步的,follower故障后,恢复时读取HW,高于HW的截取掉,然后向leader同步,重新返回ISR。leader故障后,从ISR中选取一个follower为leader,为保证数据的一致性,其他follower截取掉各自高于HW的log文件,然后从新的leader同步。(多退少补)
  • Exactly Once语义

    • 0.11版本之后引入幂等性,保证producer向partition写数据时不重复也不丢失,所谓的幂等性就是指 Producer不论向 Server发送多少次重复数据, Server端都只会持久化一条。
    • 要启用幂等性,只需要将Producer的参数中 enable.idompotence设置为 true,ack自定为-1
    • Producer在初始化的时候会被分配一个 PID,发往同一 Partition的消息会附带 Sequence Number。而Broker端会对 做缓存,当具有相同主键的消息提交时, Broker只会持久化一条。PID重启就会有变化,同时不同的partition也具有不同的主键,所以幂等性无法保证跨会话的exactly once

Kafka消费者

  • 消费方式:pull(消费者la),kafka在消费者拉取数据时引入timeout,如果没有数据可供消费会等待一段时间避免一直轮询
  • 分区策略:即如何将多个分区分给同一个消费者组下的多个消费者,当消费者的个数变化时都会触发
    • 1>RoundRobin 轮询(最大差1个):按组,同一组内所有消费者订阅的topic当成一个整体,所有topic的分区排序后轮询分给各个消费者,若有消费者订阅了其他topic,则有可能组内其他消费者会分到其他topic的分区【只有当消费者组内的消费者消费的topic一样时才有意义,可以更均匀的将不同的分区分配到不同的消费者】(按组分,看所有topic一共有多少分区,排序,划分给消费者,不在同一个组是不同组订阅同一topic,数据复制),(问题:没订阅的消费者可能受到其他订阅者订阅的消息)
    • 2> Range(默认):按topic,每个topic内的分区分别划分给订阅的消费者,(问题:消费者消费的分区有可能会越来越不对等)
  • offset 的维护:0.9版本之前消费者的offset存在ZK,之后存在kafka一个内置的topic中__consumer_offsets,存储的是group+topic+partition的offset。存在kafka是按kv存入内部的topic的,共50个【这个topic默认的ack为-1】

消费者组案例

一个分区只能由一个消费者消费,当消费者个数变化是,分区重新分配给消费者组个各个消费者

Kafka高效读数据

  • 顺序写磁盘:写的过程一直追加到文件末端
  • 零复制技术:使用内核控制,不经过上层user space

ZK在Kafka中的作用

Kafka 集群中有一个broker 会被选举为Controller,负责管理集群broker 的上下线,所有topic 的分区副本分配和leader 选举等工作。Controller 的管理工作都是依赖于Zookeeper 的

Kafka 事务

Kafka 从0.11 版本开始引入了事务支持。事务可以保证Kafka 在Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。

  • Producer事务:为了实现跨分区跨会话的事务,需要引入一个全局唯一的Transaction ID,并将 Producer获得的 PID和 Transaction ID绑定。这样当 Producer重启后就可以通过正在进行的 Transaction ID获得原来的 PID。
  • Consumer事务:对于 Consumer而言,事务的保证就会相对较弱,尤其时无法保证Commit的信息被精确消费。这是由于 Consumer可以通过offset访问任意信息,而且不同的 Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。(默认segment存储7天,如果跨segment,有可能被删除)

Kafka API

Producer API

  • Producer发送消息使用异步发送,发送过程中涉及到两个线程——main线程和sender线程,以及线程共享变量RecordAccumulator,main线程发送给RecordAccumulator,sender线程不断从RecordAccumulator拉数据发送到broker
  • main线程:Producer -- Interceptor -- Serializer -- Partitioner,然后写入RecordAccumulator
  • 异步发送API(见代码)
  • 同步发送API,main线程和sender线程同步,使用send().get()方法,send方法返回future对象,调用get会阻塞main线程

Consumer API

  • 自动提交offset:1 开启自动提交 2 自动提交时间间隔
  • 手动提交offset:commitSync(同步提交),commitAsync(异步提交),都有可能造成数据漏消费或者重复消费
  • 自定义存储offset:借助ConsumerRebalanceListene,可以结合数据处理和offset提交组成一个事务,将自定义的offset存储在数据库中

自定义拦截器

实现ProducerInterceptor接口

Kafka监控

Kafka Eagle

Flume对接Kafka

进行数据分类时,可以使用拦截器在source中添加key为topic的value在同一个channel中,sink为kafka,那么在kafka中会自动根据topic进行分类

面试题

代码

  • 生产者
/**
 * 生产者
 * created by Bingmous on 2021/6/28 19:49
 */
public class ProducerTest {
    public static void main(String[] args) {

        //1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
        Properties props = new Properties();
//        props.put("bootstrap.servers","10.194.227.212:9092"); //连接的kafaka集群
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092"); //连接的kafaka集群
        props.put("acks","all"); //应答级别
        props.put("retries",3); //重试次数
        props.put("batch.size",16384); //默认批次大小,达到这个大小才提交给kafka
        props.put("linger.ms",1); //默认等待时间,不管批次大小多少,达到这个等待时间就提交,避免数据太小没有达到批次大小而没有提交
        props.put("buffer.memory",33554432); //默认RecordAccumulator缓冲区大小,数据先提交到这里,main进程和send进程通过这个对象联系
        props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); //RecordAccumulator缓冲区大小,数据先提交到这里
        props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer"); //RecordAccumulator缓冲区大小,数据先提交到这里

        //2 创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);

        //3 发送数据(异步发送)
        for (int i = 0; i < 10; i++) {
            var producerRecord = new ProducerRecord<String, String>("mytest",String.valueOf(i),"data-"+i); //不同的数据发送方式:有无key
            producer.send(producerRecord); //发送数据
//            //同步发送(很少用)
//            Future<RecordMetadata> future = producer.send(producerRecord);
//            try {
//                RecordMetadata recordMetadata = future.get(); //future对象调用get方法阻塞当前进程并返回进程的返回值
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            } catch (ExecutionException e) {
//                e.printStackTrace();
//            }
        }

        //4 关闭资源
        producer.close(); //清空内存,即使没有达到1ms也会将数据提交
    }
}
  • 生产者,带回调函数
/**
 * 生产者,带回调函数
 * created by Bingmous on 2021/6/29 10:09
 */
public class CallBackProducerTest {
    public static void main(String[] args) {
        //1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");

        //2 创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        //3 发送数据
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "data--" + i);
            producer.send(producerRecord, (metadata, e) -> {
                if (e == null){
                    System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
                }
            });
        }

        //4 关闭资源
        producer.close();
    }
}
  • 生产者,使用自定义分区器
/**
 * 生产者,使用自定义分区器
 * created by Bingmous on 2021/6/29 11:08
 */
public class PartitionProducerTest {
    public static void main(String[] args){
        //1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.bingmous.partitioner.MyPartitioner");

        //2 创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        //3 发送数据
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "mypartitionerdata--" + i);
            producer.send(producerRecord, (metadata, e) -> {
                if (e == null){
                    System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
                }
            });
        }

        //4 关闭资源
        producer.close();
    }
}
- 生产者,使用拦截器
```java
/**
 * 生产者,使用拦截器
 * created by Bingmous on 2021/6/29 14:53
 */
public class IntercetorProducerTest {
    public static void main(String[] args) {
        //1 创建kafka生产者的配置信息,所有配置都在这个类中ProducerConfig
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");

        ArrayList<String> list = new ArrayList<>();
        list.add("com.bingmous.interceptor.TimeInterceptorTest");
        list.add("com.bingmous.interceptor.CountInterceptorTest");

        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,list);

        //2 创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);

        //3 发送数据
        for (int i = 0; i < 10; i++) {
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>("mytest", "interceptorProducerdata--" + i);
            producer.send(producerRecord, (metadata, e) -> {
                if (e == null){
                    System.out.println(metadata.topic()+"-" + metadata.partition()+"-"+metadata.offset());
                }
            });
        }

        //4 关闭资源
        producer.close();
    }
}

- 分区器,发往kafka的数据如何进行分区
```java
/**
 * 分区器,发往kafka的数据如何进行分区
 * created by Bingmous on 2021/6/29 10:56
 */
public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        //自定义分区逻辑
        List<PartitionInfo> partitionInfos = cluster.availablePartitionsForTopic(topic); //通过Cluster拿到元信息
        int size = partitionInfos.size(); //总的分区数
        return 1; //返回分区号
    }

    @Override
    public void close() {
    }

    @Override
    public void configure(Map<String, ?> map) {
    }
}
  • 拦截器
/**
 * 拦截器
 * created by Bingmous on 2021/6/29 14:39
 */
public class TimeInterceptorTest implements ProducerInterceptor {
    //被拦截数据
    @Override
    public ProducerRecord onSend(ProducerRecord record) {
        ProducerRecord producerRecord = new ProducerRecord<>(record.topic(),record.partition(),record.key(),System.currentTimeMillis() + ","+record.value());
        return producerRecord;
    }

    //在数据从RecordAccumulator成功发送到kafka broker或者发送过程失败时调用,通常在producer回调逻辑触发前
    //运行在producer的IO线程中,不要在该逻辑中放入很重的逻辑,否则拖慢producer的消息发送效率
    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
    }
    //关闭interceptor,主要用于执行一些清理工作
    //interceptor可能运行在多个线程中,在具体实现时需要自行确保线程安全,若指定了多个拦截器,则producer按顺序调用他们,
    //并仅仅捕获每个interceptor可能发生的异常记录到错误日志而非向上传递
    @Override
    public void close() {
    }
    //获取配置信息和初始化数据,拦截前调用
    @Override
    public void configure(Map<String, ?> configs) {
    }
}
  • 消费者
/**
 * 消费者
 * created by Bingmous on 2021/6/29 12:25
 */
public class ConsumerTest {
    public static void main(String[] args) {
        //1 创建消费者配置信息
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.194.227.212:9092"); //连接的集群
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true); //自动提交offset,存储到kafka,只在启动时去内部的topic读取,关闭时才提交,其他的时间是存在内存中的
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000); //自动提交的间隔时间
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); //key,value的反序列化
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer"); //key,value的反序列化
        props.put(ConsumerConfig.GROUP_ID_CONFIG,"testgroup"); //消费者组
//        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest"); //重置消费者的offset,只有当组第一次消费或上次消费的数据不存在offset失效后才生效

        //2 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("mytest","mytest2")); //订阅topic
        //自定义offset,分区有可能变化,需要自定义存储offset
//        consumer.subscribe(Arrays.asList("mytest","mytest2"), new ConsumerRebalanceListener() {
//            //该方法会在 Rebalance 之前调用
//            @Override
//            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
//
//            }
//
//            //该方法会在 Rebalance 之后调用
//            @Override
//            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
//
//            }
//        });

        while (true) { //持续拉取数据
            //3 获取数据
            ConsumerRecords<String, String> records = consumer.poll(100); //拉到空数据的超时时间
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(record.topic()+"-"+record.partition() + "-" + record.key() + "-" + record.value());
            }
            //手动提交offset,需要关闭自动提交(ENABLE_AUTO_COMMIT_CONFIG,false)
//            consumer.commitSync(); //同步提交,当前线程会阻塞,直到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);
//                    }
//                }
//            });
        }
//        consumer.close();
    }
}

posted on 2020-07-25 23:51  Bingmous  阅读(57)  评论(0编辑  收藏  举报