kafka从入门到关门

kafka简介

Kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。

场景比较

传统同步方式就是按流程一步一步进行,因为发送短信这个操作没有那么高的实时性,如果都在发送短信后才提示页面注册成功,如果人数很多的情况下,就可能有人需要一直等待,那么这种是不合理的。采用异步的方式,写入数据库成功就提示用户成功,短信通知由后续程序通知,不会造成用户等待。
在这里插入图片描述

Kafka基础架构

在这里插入图片描述
kafka集群是依赖于zookeeper的,如对于zk不熟悉的同学可以参考,

此图中的术语查看 术语介绍 章节。

环境搭建以及目录结构

本人是在windows下搭建的三个kafka节点,具体linux下安装,后续文章将会更新,本次以介绍为主。
在这里插入图片描述

  • bin目录存放可执行脚本,包含windows的bat和linux的shell
  • config 配置文件,包含生产者和消费者以及一些其他配置
  • libs 一些java的库
  • log kafka真正的数据存储
  • logs 系统运行的一些日志
  • site-docs html格式的一些文档

log目录详解
在这里插入图片描述

  • 红色部分是系统自己维护的offset,在低版本之前这些是存储在zookeeper(以下简称zk)中,但是zk不适合做频繁的读写功能,因此在高版本的kafka中将维护的offset存储在kafka自身目录下。
  • 黑色部分是用户自定义的topic,test-0 代表的是test的第一个分区,同理test-1,…test-n ,分区个数可以自定义。
  • 紫色部分是系统中一些checkpoint的日志文件,便于恢复数据使用。

术语

  • broker(代理)也称作节点,例如我们搭建的伪分布式的一个文件夹就是一个节点
    在这里插入图片描述
  • topic 主题
    发送到kafka的消息有千万条,如果没有类型标识这些消息,那么我们将无从快速的查找到需要消费的消息,因此这里的topic也就是消息对应的类别。
  • 分区(Partition)
    物理上的一个概念,近似的理解为一个文件夹,为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。提高了并发性。
    • leader
      每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。
    • follower
      每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个follower会成为新的follower。
  • Producer
    生成者,生产消息到kafka队列中。
  • 消费者
    消息消费者,向Kafka broker读取消息的客户端
  • Consumer Group
    每个Consumer属于一个特定的Consumer Group,每一个分区最多有同一个组里面的一个消费者对应,可能比较绕嘴,后面会详细介绍。
  • 副本(replica)
    每个分区的副本,分布在不同的 Broker 上, 为保证集群中的某个节点发生故障时,该节点上的partition数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower。
    对应目录就是 我们在第一个节点下存在分区文件,
    在这里插入图片描述
    那么第二台上也存在分区文件,
    在这里插入图片描述

分区原则

(1)指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
(2)没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;
(3)既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition 值,也就是常说的 round-robin 算法。

Kafka命令行操作

以下操作都是基于window操作的,linux版本的只需要把执行的bat换成sh即可。

  • 启动zk集群
D:\study\kafka\apache-zookeeper-3.5.7-bin\bin\zkServer
D:\study\kafka\apache-zookeeper-3.5.7-bin-2\bin\zkServer
D:\study\kafka\apache-zookeeper-3.5.7-bin-3\bin\zkServer
  • 启动kafka 集群
@echo start kafka
start /D "D:\study\kafka\kafka_2.11-2.2.0\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0\config\server.properties
start /D "D:\study\kafka\kafka_2.11-2.2.0-2\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0-2\config\server.properties
start /D "D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0-3\config\server.properties
  • cmd下输入 jps查看是否启动成功
    在这里插入图片描述
  • 创建topic
 kafka-topics.bat --create --zookeeper 127.0.0.1:2181 --replication-factor 1 --partitions 1 --topic test
  • 查看topic
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-topics.bat --list --zookeeper localhost:2181
  • 生产消息
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-console-producer.bat --broker-list localhost:9092 --topic test
  • 消费消息
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test2 --from-beginning
  • 获取某个主题最新的offset
kafka-run-class.bat kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test2
  • 查看kafka日志文件内容即生产者生产的消息(初学者常用)
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-run-class.bat kafka.tools.DumpLogSegments --files D:\study\kafka\kafka_2.11-2.2.0\log\test2-0\00000000000000000000.log  --print-data-log

kafka文件存储机制

在这里插入图片描述
由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition分为多个segment。每个segment对应两个文件——“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号 ,与上面目录详情中可以对应上了。
两者之间存储关系如下图:
在这里插入图片描述
index 顾名思义存储的就是索引位置,log存储的具体内容。
索引中的元数据指向日志中物理偏移量。

传输中可靠性的保证

为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
在这里插入图片描述
那么问题来了,啥时候发送ack,或者说达到了什么条件才会发送ack?

  • 副本复制策略

前文中讲到了分区是由多个副本的,也就是副本之间也需要同步数据,那么副本之间是如何同步数据的呢?又是如何保证数据一致性的呢?
其实想想一个复制多个无法就两种情况:

  • 有一个老大复制完了,在复制给其他人。
  • 等所有的小弟全部复制 完成。

那很明显了kafka中有以下两种方案

方案 优点 缺点
半数以上完成同步,就发送ack 延迟低 选举新的leader时,容忍n台节点的故障,需要2n+1个副本
全部完成同步,才发送ack 选举新的leader时,容忍n台节点的故障,需要n+1个副本 延迟高

Kafka选择了第二种方案,原因如下:
1.同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。
2.虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
但是采取第二种方式也是有很大弊端的,试想以下场景:

leader收到生产者生产的消息,所有的follower开始同步数据了,但是有一个follower中间因为某种故障,断开连接了,迟迟同步不了,那么leader就要一直等待。
那ack怎么发出来呢?
Leader维护了一个in-sync-replica set 简称 ISR,意思为和leader保持同步的follower同步的集合。当ISR中的follower同步完成数据之后,
leader将会给follower发送ack,如果出现这种迟迟不响应的情况,则该follower将会被提出ISR,改时间由replica.lag.time.max.ms参数设定。同理当Leader故障时则会产生新的Leader,这个选举过程在Zk中完成的。选区Leader的过程将在下章 zk与kafka的关系中详细讲解。
  • ack策略
    上面讲到了,要等所有的ISR中的副本全部相应成功才会相应给生产者此次生产已经完整的写入队列。
    那这里的ack说的是leader和follower之间的相应,并不是leader和生产者之间的ack。
    对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功
    ack的三种取值对应关系:
    0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据;
    1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;
    -1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才 返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复。

消费者的消费方式

生活中的例子:
你去参加高考,查成绩的时候,两种方式,一种是 你主动打电话或者登陆网站去查询 另外一种是被动模式,比较牛逼,很有信息,我非等老师查完成绩然后打电话给我说,你考的这么烂,你家里人知道么。
这里的主动就是 pull,英文翻译拖拽,由人自动发起
被动就是push,英文翻译就是推送,被动接受,下图无关,放松心情
在这里插入图片描述

Counsumer采取的消费模式就是主动模式,Consumer说我不喜欢被动,男人嘛就要主动一点。那为啥不选择push的方式被动呢主要是因为难适应消费速率不同的消费者,被动模式没有主动权,可能一个消费者正在消费突然间大把的消息进来,虽然有分区的概念,但是会出现网络拥堵或者拒绝连接的情况。
那采取pull模式就能解决这个问题,随之而来的问题就是,pull就是一直不断的亲求,假如队列中没有数据的话,有很多空请求会一直轮询,因此Kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。

kafka为啥吞吐量这么高

  • 顺序写磁盘
    我们生活都遇到过这种情况,当我们拷贝一个大文件的时候,会比较快,哪怕文件有几个G,每秒钟可能复制几十M甚至百M。但是我们复制大量的小文件的时候就会特别慢。这就要从磁盘的硬件结构来说了,当我们拷贝大文件的时候,是磁头寻址一次,一块区域一块区域的顺序读写,因此会很快。但是当我们多文件拷贝的时候我们的磁盘就需要不停的查找磁头的位置,时间都浪费在寻址上了。因此kafka在设计的时候就避开了硬件的短板,采用顺序读写的方式,这是保证高速写的一个重要因素。
    Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到到600M/s,而随机写只有100k/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。

在这里插入图片描述

  • 零拷贝
    学过Linux的都知道,要想从磁盘中拷贝一个文件,大致要经历这么几个简单的过程。图示:
    在这里插入图片描述

基本操作就是循环的从磁盘读入文件内容到缓冲区,再将缓冲区的内容发送到socket。但是由于Linux的I/O操作默认是缓冲I/O。这里面主要使用的也就是read和write两个系统调用,我们并不知道操作系统在其中做了什么。实际上在以上I/O操作中,发生了多次的数据拷贝。

当应用程序访问某块数据时,操作系统首先会检查,是不是最近访问过此文件,文件内容是否缓存在内核缓冲区,如果是,操作系统则直接根据read系统调用提供的buf地址,将内核缓冲区的内容拷贝到buf所指定的用户空间缓冲区中去。如果不是,操作系统则首先将磁盘上的数据拷贝的内核缓冲区,这一步目前主要依靠DMA来传输,然后再把内核缓冲区上的内容拷贝到用户缓冲区中。如此繁琐的过程 ,大大降低了传输效率。

零拷贝一句话总结 :让数据传输不需要经过user space。
在这里插入图片描述
只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。

Zookeeper与Kafka中的关系

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

Leader选取的过程:
在这里插入图片描述
我们可以亲自去看下zk目录下和kafka相关的目录文件
我们启动zk的客户端,Windows下直接在cmd中切换到zk的bin目录执行zkcli.cmd

查看/brokers/ids
在这里插入图片描述
刚好是三个节点,0,1,2
当有一个节点掉线之后,zk /brokers/ids 这个目录下的存储的断开的节点就会被移除,这时候,节点总数发生了变化,kafkaController发现节点发生变化后,便会获取ISR里面的节点,并从中选取一个作为新的leader,ISR的获取方式
在这里插入图片描述
图上我们可以很清晰的看到当前的leader就是第0个节点。

以下是其他目录对应的kafka的存储信息,这里就不一一展开,需要看的可以自己使用zk命令自己去查看研究。
在这里插入图片描述
此图是转自csdn博客上的一个大神 幽灵之使。

offset的维护

由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消费,所以consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费。
可以参考目录章节的__consumer__offset 图片,ps:是在0.9版本之后,之前的版本存储在zk中。

消息发送流程

生产者发送消息采取的是异步 发送,发送过程中涉及到了两个线程,main-Sender,以及一个线程共享变量-RecordAccumulator
关于线程共享变量可以简单的理解为Java中的volitate。

主要流程:
main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。

奉上丑照:
在这里插入图片描述

Java版API

添加maven依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
//核心代码 不带回调函数的API 
     Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka集群,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator缓冲区大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
    
    
            producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)));
        }
        producer.close();
    }

//带回调函数的 就是在ProducerRecord 第三个参数加一个回调参数
//回调函数会在producer收到ack时调用,为异步调用,该方法有两个参数,分别是RecordMetadata和Exception,如果Exception为null,说明消息发送成功,如果Exception不为null,说明消息发送失败。

producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
    
    
                //回调函数,该方法会在Producer收到ack时调用,为异步调用
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
    
    
                    if (exception == null) {
    
    
                        System.out.println("success->" + metadata.offset());
                    } else {
    
    
                        exception.printStackTrace();
                    }
                }
            });

猜你喜欢

转载自blog.csdn.net/abc8125/article/details/109351098