Kafka

一、简介

1、 是什么

是一个分布式、支持分区的(partition)、多副本的(replication),基于 zookeeper 协调的分布式消息系统,可以实时的处理大量数据.

2. 有哪些主流的消息队列 (中间件)

  • RabbitMQ: 由 Erlang(二郎) 语言编写。吞吐量比较低,不容易进一步开发扩展。
  • RocketMQ: 由 java 编写,阿里开发,社区活跃度低,万一不维护,需要自己公司研发。
  • Redis: 用作消息队列时,数据量大小在 10k 以内速度快,数据量大时会非常慢
  • Kafka:Apache 开发,由 Scala 和 java 编写,适合大数据实时计算以及日志收集

3. 为什么要使用消息队列

主要用来缓冲任务削峰:上游数据有时会突发流量,下游可能扛不住,或者下游没有足够多的机器来保证处理,此时 kafka 在中间可以起到一个缓冲作用,把消息暂存在 kafka 集群中,下游服务就可以按照自己的节奏慢慢处理任务。

4. 消息队列的应用场景

用户管理中,当成功写入数据库后的回调信息中要作两件事:

  • 发送注册邮件
  • 发送注册短信

如果使用消息队列,则在收到异步通知后就直接响应用户,把后两件事放在队列中慢慢处理。
串行处理任务
并行处理任务(多线程问题)
kafka 消息队列处理

5. kafka 的分布式实现

NameServer 在 kafka 中使用的是 zookeeper

6.Kafka 特性

解耦、高吞吐量、低延迟、高并发、容错性、可扩展性、持久性和可靠性

  • 解耦:使用 kafka 后,任务的处理者 (consumer) 与任务的发布者 (producer) 之间没有依赖关系。
  • 高吞吐量、低延迟:kafka 每秒可处理几十万条消息,延迟可低到几毫秒。每个 topic 可以分多个 partition,consumer group 对分区可并行读取
  • 可扩展性:kafka 集群支持热扩展
  • 持久性和可靠性:
    消息被持久化到本地磁盘, 并支持数据备份防止数据丢失。
  • 容错性:允许集群中节点失败,只要还剩下一个就能正常工作
  • 高并发:支持数千个客户端的读写

7. 消息的分类

  • 点对点:一个队列可以有多个消费者一起消费,但一个消息只能被一个消费者消费。
  • 发布与订阅
    消息被持久化到一个 topic 中,消费者可消费该 topic 中所有数据,同一条数据可被多个消费者消费,数据被消费后不会删除。

二、kafka 整体架构

1.kafka 由哪些组件构成

topic:

  • 是什么?就是一堆消息,由多个分区构成。
  • 分区 partition, 为什么要分区:
    • 从 producer 角度看,分区分布在不同 broker 上,方便容量扩展,同时也提高吞吐量负载均衡
    • 从 consumer 角度看,一个组内的某个消费者只能消费一个分区,分区后可以提高并发量,效率大提高。但要求组内消费者数量不能大于 topic 的分区数
  • 副本 replication

副本是什么?
leader 副本:用来提供读写功能,一个分区只有一个 leader 副本
follower 副本:只是被动地备份leader 中数据,不提供读写功能,作用是为了提高分区的可用性

producer:

向 topic 中发布消息

consumer:

订阅 topic 中消息,并从 topic 中读取和处理消息

broker:

管理 topic 中消息存储的服务器

2 . ISR 和 AR 是什么,ISR 的伸缩又指什么?

  • ISR:In-sync Replicas(副本同步队列),一个分区中,包含了 leader 和所有与 leader 保持同步的 follower 的 id, 该队列由控制器和 leader 维护,如果 follower 从 leader 中同步时间超过阈值,就会被从 ISR 中踢出,并把该 follower 的 id 存入 OSR(Outof-sync replicas) 列表中
  • AR 是什么?
    AR=ISR+OSR
  • ISR 伸缩:follower 副本跟 leader 同步超时会 ISR 中移除,当 follower 同步了所有 leader 数据后又加入 ISR 中

3. 控制器是什么?如何选举的

就是一个 Broker, 集群中第一个启动的 broker 会通过在 zookeeper 中创建临时节点 / controller 来试图让自己成为控制器,其他 broker 会在该节点消失时收到通知,然后分别再向 zk 中写 / controller 节点,只有一个能成功,其他节点只能监听该节点

作用:监听 ids 变化从而实现下面两个功能:

  • topic 的分区副本分配:一个 topic 为了实现负载均衡,会被分成多个分区,这些分区信息及与 broker 的对应关系由 controller 维护
  • 分区的 leader 选举

4. 分区 leader 选举?

某个作为 leader 的 broker 挂了,则 controller 会把其从 ISR 中移除,再从 ISR 列表中找跟当前 leader 保持最高同步的副本作为 leader, 如果都保持了完全同步,则按顺序从前向后选。

三、环境搭建

1.kafka 安装

  • 网址下载软件:http://kafka.apache.org/downloads.html
  • 解压:tar -zxvf kafka/kafka.tar.gz -C /opt/module/
  • 在 kafka 目录中创建目录:mkdir logs,用来存放日志和消息数据
  • 编辑配置文件:vim config/server.properties
    • broker.id=0
    • delete.topic.enable=true 删除注释,以便可以删除 topic
    • log.dirs=/opt/module/kafka/logs
    • zookeeper.connect=192.168.184.100:2181
    • listeners = PLAINTEXT://192.168.184.100:9092
      host.name=192.168.184.100, 这里的 ip 是当前虚拟机的 ip
  • 克隆两台虚拟机并修改 ip 为 101 和 102
    https://blog.csdn.net/wlxs32/article/details/105232268 注意要删除每台虚拟机 kafka 内部的 logs 目录

2. 启动

启动 zookeeper

/opt/module/zookeeper-3.4.10/bin/zkServer.sh start

启动与关闭 kafka 服务

在每台虚拟机上都执行下面命令

  • 前台运行:/opt/module/kafka/bin/kafka-server-start.sh /opt/module/kafka/config/server.properties
  • 后台运行:/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties
  • 通过 jps 命令查看 java 进程,如果看到 kafka 表示启动成功
  • 关闭服务:
    • netstat -alnp | grep 9092,kill -9 进程号
    • bin/kafka-server-stop.sh,如果权限不足,只可以 chmod 777 kafka-server-stop.sh

四、shell 命令使用

关闭:/opt/module/kafka/bin/kafka-server-stop.sh
启动:/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties

1. 创建 topic 命令

/opt/module/kafka/bin/kafka-topics.sh —create —zookeeper 192.168.184.100:2181 —partitions 2 —replication-factor 2 —topic first
会在 logs 目录中创建 topic,分别为 first-0 和 first-1

2. 查看集群中有哪些 topic

/opt/module/kafka/bin/kafka-topics.sh —list —zookeeper 192.168.184.100:2181

3. 查看指定 topic 的详细信息

/opt/module/kafka/bin/kafka-topics.sh —describe —zookeeper 192.168.184.100:2181 —topic first

  • 没有 zookeeper 可以使用 kafka 吗?zk 有什么用
    • 管理集群 broker 的上下线,如 broker 上线,会把 brokerId 写到 zk 的 / brokers/ids 节点下,下线就从 zk 中删除
    • controller 选举:kafka 中某个 broker 会被 zk 选举为 controller
      • controller 有什么用?
        监听 ids 变化从而实现下面两个功能:
        • topic 的分区副本分配:一个 topic 为了实现负载均衡,会被分成多个分区,这些分区信息及与 broker 的对应关系由 controller 维护
        • 分区的 leader 选举

4. 删除 topic

/opt/module/kafka/bin/kafka-topics.sh —delete —zookeeper 192.168.184.100:2181 —topic first
注意:必须所有分区所在的主机 kafka 服务处于开启状态才能正常完全删除。

5. 修改一个 topic 的分区数

bin/kafka-topics.sh —zookeeper 192.168.184.100:2181 —alter —partitions 3 —topic first
注意:分区数只能增加不能减少

6. producer 客户端连接

bin/kafka-console-producer.sh —broker-list 192.168.184.101:9092 —topic first

7.consumer 客户端连接

  • 旧版本用法

bin/kafka-console-consumer.sh —zookeeper 192.168.184.100:2181 —from-beginning —topic first

问题:offset 的作用?
每个消息都有一个顺序 ID 号,叫偏移量 offset, 用来唯一地识别分区中的每条消息

  • 新版本用法
    bin/kafka-console-consumer.sh —bootstrap-server 192.168.184.102:9092 —from-beginning —topic first
    offset 的值不再保存到 zk 中,而是保存在一个叫作**__consumer_offsets 的 topic 中**,可通过命令 bin/kafka-topics.sh —list —zookeeper 192.168.184.100:2181 查看

五、数据流程

1. 生产数据流程

  • producer 从 broker-list 中获取某一 partition 的 leader
    如 zookeeper 上获取 0 号分区的副本信息:get /brokers/topics/first/partitions/0/state
  • producer 把消息发送给某一个分区的 leader
  • leader 将消息写入本地 logs 目录中
  • 该分区的 follwer 从 leader 中 pull 消息,并写入 follower 本地 logs,然后再向 leader 发送 ack(acknowledge character)
  • leader 收到所有 follower 的 ack 后再向 producer 发送 ack

2. 消费数据流程

  • 采用 pull 模式消费消息,由消费者自己记录消费状态 (自已把 offset 写入 zk 或 kafka 中),每个消费者互相独立的顺序读取每个分区的消息。如果 broker 没数据,则会有一个超时等待时间,过了这段时间再返回。
  • 分区消费
    组内某个消费者只能同时消费一个分区,如果分区数大于组内消费者,则默认采用轮询方式依次消费数据。

3. 数据持久化

存储方式:

topic 存储在一个或多个 partition 中,每个分区对应一个文件夹,用来放消息和索引文件。

数据删除策略

无论消息是否被消费,kafka 都会保留所有消息,有两种策略可以删除旧数据

  • 基于时间:log.retention.hours=168, 默认采用这种方式(7 天)
  • 基于大小:log.retention.bytes=1073741824,1G, 如果分区大小超过 1G 就会在适当时机把一最老的 segment 数据段删除

数据的可靠性如何保证?

ack 为 all 的前提下采用幂等机制
通过控制丢失率、重复率和副本数据的一致性来实现。

  • ack 的不同级别(3 个)保证控制数据的丢失率和重复率
    • 0:生产者将数据发送出去后就不管了,不去等待任何回应,好处是数据不会重复,效率高,但不能保证数据一定能成功写入
    • 1: 数据发送到 leader,并成功写入 leader 的 logs,但不会等待成功写入 follower 就会响应acks. 数据可能重复
      如果 leader 在响应后,同步到 follower 前 leader 挂了,则会丢失数据。
    • all: 会等待 leader 和所有副本成功写入,才响应acks, 否则就重发。
      好处是数据绝对不丢失,缺点是会重复。
      • 为什么会重复?
        如果数据已写入全部副本,但在响应 producer 前,leader 挂了,那么 producer 就认为写入失败,就要重写。
      • 如何在 kafka 内去除重复?
        • 用 acks=all 和幂等机制,也就是将 enable.indempotence 设置为 true 就能实现。
        • 实现原理:kafka 会额外为 producer 分配一唯一的 id, 叫作 pid,producer 在发送消息时,生成消息的 id(pid + 消息的序列号),leader 在接收消息时会把每条消息的 id 存入缓存中,并对新接收的消息判断是否是重复的 id, 如果重复就丢弃
  • 如何保证副本数据的一致性
    通过 LEO 和 HW 保证副本数据的一致性。
    • LEO:标识当前分区中下一条待写入消息的 offset
    • HW: 高水位,实际上就是 ISR 中所有 broker 的 LEO 最小值,消费者只能获取 HW 这个 offset 之前的消息。
      hw 有什么用?
      • follower 挂掉后,被移出 isr, 恢复后获取挂掉前的 hw, 把副本中所有大于等于这个 hw 的数据清除,从 leader 同步数据。
        • 为什么要清除 hw 之后数据?因为 follower 恢复过程中,leader 可能挂掉,并重新选举了新 leader,而这个 leader 的 LEO 可能比较低,且随后又接收到一些新数据,该 follower 数据如果不删除一部分,就无法确定从哪个位置同步数据。
      • leader 挂掉,会从 ISR 中选出一个新的 leader 之后,为保证多个副本之间的数据一致性,其余(不包括 leader)的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader 同步数据。

4. 写数据的高效性?

效率很高,因为采用顺序写,也就是在一个连续空间写数据,所以比写内存效率要高

5. 消费者获取消息的高效性如何保证

  • 分区:在消费者组中有多个消费者并发读取不同的分区数据
  • 分段:将数据文件分段,比如 100 条消息,它们的 offset 是从 0 到 99,假设将数据文件分成 5 段,第一段为 0-19 号消息,其文件名为 0.log,第二段 20-39,其文件名 20.log, 以此类推。
  • 稀疏索引
    为每个小文件中的部分消息建立索引文件,名字跟分段文件名相同,后缀为. index,其内容有两部分:
    (1) 部分消息的相对 offset
    (2) 部分消息的 poistion: 是相当于文件首地址的物理偏移量。

6.producer 发送消息到哪个分区?分区选择的原则是什么?

  • 指定分区器:发送到分区器指定的分区中,p.put(“partitioner.class”,”xxx”)
  • 没指定分区器,但指定了 key, 就会根据 key 的 hash 值跟分区数取模得到结果,就是要发送的分区 new ProducerRecord<String, String>(“first”,”abc”,Hello” + i), 这里 abc 就是 key
  • key 和分区器都没指定,则默认采用轮询(旧版本)决定发送到哪个分区。
  • 配置文件
<dependencies>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka_2.12</artifactId>
        <version>0.11.0.3</version>
    </dependency>
</dependencies>
#cfg.properties
bootstrap.servers=192.168.184.100:9092
#一批数据的大小
batch.size=16384
#accumulator缓冲区大小
buffer.memory=33554432
#提交延时,当达到指定时间,即便是accumulator缓存不满也会提交数据
linger.ms=1
#生产者需要用序列化器(Serializer)将key和value序列化成字节数组才可以将消息传入Kafka。
#消费者需要用反序列化器(Deserializer)把从Kafka中收到的字节数组转化成相应的对象
#指定kv的序列化器的类型,值为其class路径。
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer
partitioner.class=hy.MyPartition
retries=0

7. 生产者拦截器

用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求
如何实现?

  • 定义类并实现接口:ProducerInterceptor
public class MyInterCeptor implements ProducerInterceptor<String,String>{
    long sucNum;
    long errNum;
    //producer发送前会先调用该方法
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> r) {
        return new ProducerRecord<String,String>(r.topic(),r.partition(),r.timestamp(),r.key(),System.currentTimeMillis()+r.value(),r.headers());
    }
    //在kafka响应ack时会调用
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        if(e==null){
            sucNum++;
        }else{
            errNum++;
        }
    }
    //kfka的Producer关闭时会调用
    public void close() {
        System.out.println("本次共成功发送:"+sucNum+",失败:"+errNum);
    }
    //获取配置信息时调用
    public void configure(Map<String, ?> map) {
        System.out.println(map);
    }
}
  • 添加拦截器到配置文件中。
p.put("interceptor.classes","hzn.MyInterCeptor");

8. 消费者

public class MyConsumer {
    public static void main(String[] args) {
        Properties p=new Properties();
        p.put("bootstrap.servers","192.168.184.100:9092");
        p.put("group.id","gr_01");
        p.put("enable.auto.commit","false");//关闭自动提交
        p.put("key.deserializer", StringDeserializer.class.getName());
        p.put("value.deserializer", StringDeserializer.class.getName());
        p.put("auto.offset.reset","earliest");//从头开始消费
        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(p);
        while(true){
            consumer.subscribe(Arrays.asList("first"));
            ConsumerRecords<String,String> cr=consumer.poll(100);
            for(ConsumerRecord<String,String> r:cr){
                System.out.println("主题:"+r.topic()+",所属分区:"+r.partition()+",内容:"+r.value());
            }
            //手动提交
            consumer.commitAsync();
        }
        /*
         *异步:commitAsync,提交后就结束,不阻塞
         *同步:commitSync,提交后会等待kafka确认,在收到确认前会阻塞
         */
    }
}

9. 分区选择

public class MyPartition implements Partitioner{
    Random random=new Random();
    int p=-1;
    @Override
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        System.out.println("s="+s);
        int part=random.nextInt(2);
        //return part;
        p++;
        if(p==2){
            p=0;
        }
        return p;
    }
    @Override
    public void close() {
    }
    @Override
    public void configure(Map<String, ?> map) {
    }
}

10. 生产者

public class MyProducer {
    public static void main(String[] args) throws Exception{
        Properties p = new Properties();
        //p.put("bootstrap.servers","192.168.184.100:9092");
        p.put("acks","all");
        p.put("enable.indempotence",true);//幂等机制
        p.load(MyProducer.class.getClassLoader().getResourceAsStream("cfg.properties"));
        Producer<String, String> producer = new KafkaProducer<String, String>(p);
        for (int i = 0; i < 10; i++) {
            //producer.send(new ProducerRecord<String, String>("first","hello_"+i));
            //producer.send(new ProducerRecord<String, String>("first","abc","hello_"+i));
            producer.send(new ProducerRecord<String, String>("first", "abc", "hello_" + i), (m, e) -> {
                //收到ack时调用,发送失败且不再重发也会调用该方法
                if(e==null){
                    //发送成功
                    System.out.println("分区:"+m.partition()+",offset:"+m.offset());
                }else {
                    e.printStackTrace();
                    System.out.println("发送失败");
                }
            }).get();//注意:调用get的后果是让发送方法阻塞;当收到ack并回调就解除阻塞
        }
        producer.close();
    }
}

六、面试题

1.kafka 是怎么体现消息顺序性的?

在一个分区内能过 offset 来维护顺序性,不同分区无法保证。

2.kafka 新建的分区会在哪个目录中创建?

在配置文件指定的 logs 目录下创建: first-0,first-1

3.kafka 中的分区器、序列化器、拦截器是否了解?之间的顺序是什么?

  • kafka 在发送消息前先经过拦截器处理
  • 序列化器转换为字节数组
  • 分区器指定分区

4. 什么情况会造成重复消费?什么情况会造成漏消费?

  • 重复消费?
    • 生产者:ack=all时生产者会重复发消息会重复消费,可通过幂等机制解决
    • 消费者:消费者采用先消费后提交 offset的方式,如果在消费结束后提交 offset 时,失败了,则会重复消费同一条记录。可通过同步提交 offset 的方式解决。
  • 漏消费?
    • 生产者:ack=0 或 1 时,kafka 在生产者发送了数据后可能没存储成功,从而漏消费。
    • 消费者:消费者采用先提交 offset, 后消费,可能会漏消费。可先消费后提交解决

5.kafka 内部有 topic 吗?有什么用?

__consumer_offsets, 用来记录所有 topic 的所有分区被消费者消费的 offset

6. 如何保证消息只能被消费一次?

  • producer 端
    • ack=all
    • p.put(“enable.indempotence”,true);//幂等机制
  • consumer 端
    • 关闭自动提交功能
    • 每消费一次数据后手动提交

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值