kafka快速入门

消息系统

  • 串行
  • 并行

消息模式

  • 点到点:

    • 主要采用的队列的方式
    • 对点消息系统中,消息被保留在队列中,一个或者多个消费者可以消费队列中的消息,但是特定的消息只能有最多的一个消费者消费。
    • 该系统的典型应用就是订单处理系统
  • 发布-订阅消息系统

    • 主题

    • 发布者

    • 订阅者

    • 应用场景:

      • 应用解耦

      • 流量控制

      • 日志处理

      • 消息通讯

kafka应用

  • 搜索日志,
  • 监控日志,
  • 访问日志等。
  • 指标分析
  • 日志聚合解决方法
  • 流式处理

kafka优点

可靠性:分布式的,分区,复制和容错的。
可扩展性:kafka消息传递系统轻松缩放,无需停机。
耐用性:kafka使用分布式提交日志,这意味着消息会尽可能快速的保存在磁盘上,因此它是持久的。
性能:kafka对于发布和定于消息都具有高吞吐量。即使存储了许多TB的消息,他也爆出稳定的性能。
kafka非常快:保证零停机和零数据丢失。

KafKa架构四大核心

  • 生产者API:允许应用程序发布记录流至一个或者多个kafka的主题(topics)
  • 消费者API:允许应用程序订阅一个或者多个主题,并处理这些主题接收到的记录流。
  • StreamsAPI:允许应用程序充当流处理器(stream processor),从一个或者多个主题获取输入流,并生产一个输出流到一个或者多个主题,能够有效的变化输入流为输出流。
  • ConnectorAPI:允许构建和运行可重用的生产者或者消费者,能够把kafka主题连接到现有的应用程序或数据系统。

kafka支持消息持久化,消费端为拉模型来拉取数据,消费状态和订阅关系有客户端负责维护,消息消费完 后,不会立即删除,会保留历史消息。因此支持多订阅时,消息只会存储一份就可以了

kafka整体架构

在这里插入图片描述

  • 典型的kafka集群中包含若干个Producer,若干个Broker,若干个Consumer,以及一个zookeeper集群
  • kafka通过zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行Rebalance(负载均 衡);Producer使用push模式将消息发布到Broker;Consumer使用pull模式从Broker中订阅并消费消息。

Kafka术语

  • broker,消息队列中常用的概念,在Kafka中指【部署了Kafka实例的服务器节点】。

  • topic,用来区分不同类型信息的主题。比如应用程序A订阅了主题t1,应用程序B订阅了主题t2而没有订阅t1,那么发送到主题t1中的数据将只能被应用程序A读到,而不会被应用程序B读到。

  • partiton,每个topic可以有一个或多个partition(分区)。分区是在物理层面上的,不同的分区对应着不同的数据文件。Kafka使用分区支持物理上的并发写入和读取,从而大大提高了吞吐量。

  • replica,Kafka在建立topic時,可以設定replica(副本)的数量,(控制消息保存在几个broker(服务器)上,一般情况下等于broker的个数)

    • 副本因子过程图?

    • 在这里插入图片描述

    • replica操作以partition为单位,每个partition都有各自的主副本和从副本,主副本为leader,从副本叫做follower(在有多个副本的情况下,kafka会为同一个分区下的分区,设定角色关系:一个leader和N个 follower),处于同步状态的副本叫做in-sync-replicas(ISR,表示当前可用的副本);follower通过拉的方式从leader同步数据。消费 者和生产者都是从leader读写数据,不与follower交互。

    • 作用:让kafka读取数据和写入数据时的可靠性。

    • 副本因子是包含本身,同一个副本因子不能放在同一个Broker中。

    • 如果某一个分区有三个副本因子,就算其中一个挂掉,那么只会剩下的两个钟,选择一个leader,但不会在其 他的broker中,另启动一个副本(因为在另一台启动的话,存在数据传递,只要在机器之间有数据传递,就 会长时间占用网络IO,kafka是一个高吞吐量的消息系统,这个情况不允许发生)所以不会在零个broker中启 动。

  • segement,kafka中实际用来存储消息的文件

    • 在这里插入图片描述

    • producer将数据存入到kafka集群broker上,consumer才能消费。消息是存储在segment段上

    • 消息是按照什么形式或怎么方式存储到segment段呢?

      • 实际中有config/server.properties文件的配置如下:

      • ##日志滚动的周期时间,到达指定周期时间时,强制生成一个新的segment log.roll.hours=72 
        ##segment的索引文件最大尺寸限制,即时log.segment.bytes没达到,也会生成一个新的segment log.index.size.max.bytes=10*1024*1024 
        ##控制日志segment文件的大小,超出该大小则追加到一个新的日志segment文件中(-1表示没有限制) 、
        log.segment.bytes=1014*1024*1024
        
  • offset,消费位置,它唯一标识了一条消息,消费者通过(offset,partition,topic)跟踪记录。

    • 任何发布到此partition的消息都会被直接追加到log文件的尾部,每条消息在文件中的位置称为offset
    • 在这里插入图片描述
  • LogEndOffset,简称LEO, 代表Partition的最高日志位移,其值对消费者不可见。

  • LogSize,已经存入该partition的数据量。已经写到该分区的消息数

  • 消费组:由一个或者多个消费者组成,同一个组中的消费者对于同一条消息只消费一次。

    • 某一个topic下的partition数,对消费组而言,应小于等于该主题下的分区数。
    • 某一topic有4个partition,那么消费组中消费者应该小于4,且最好与分区数成整数倍。同一个partition下的数据,在同一时刻,不能同一个消费组的不同消费者消费
    • 总结:分区数越多,同一时间可以有越多的消费者来进行消费,消费数据的速度就会越快,提高消费的性能

Partition 接收消息的时候是有顺序的吗?需要进行怎么的处理?不处理的话是怎样接收的

一个broker服务下,可以创建多个partition,在kafka中,每一个分区会有一个编号:编号从0开始,某一个分区的数据是有序的

在这里插入图片描述

  • 说明-数据是有序,生产是什么样的顺序,那么消费的时候也是什么样的顺序

如何保证一个主题下的数据是有序的?

  • 方案1:

    • 一个主题(topic)下面有一个分区(partition)即可(单线程)
    • 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个
  • 方案2:(多个线程来并发处理消息

    • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性

    在这里插入图片描述

产生消息

熟悉offset的提交方式(自动提交以及手动提交)

  • 自动提交

    props.put("enable.auto.commit", "true"); 
    props.put("auto.commit.interval.ms",  "1000");
    
  • 手动提交

     //关闭自动提交确认选项
     props.put("enable.auto.commit", "false");
     
     // 手动提交offset值  kafkaConsumer.commitSync();
     consumer.commitSync();
     buffer.clear();
    
  • 了解生产消息时的几种消息格式(string,arvo,json)

 * Json
     * 当我们的数据格式完全不固定,而且大量的文字性的消息,例如从网上爬取一篇文章,然后分词,做摘要,
     * 最后存储到ElasticSearch里面做检索,这么一个流程,使用JSON其实是免去了很多转换工作
     * 优势
     *      所以采用JSON在数据转换方面的工作量肯定是比较小的
     * 劣势
     *      1.由于它每条记录都需要Key,所以在大数据时,其实它有大量的数据冗余
     *      2.它不够明白,需要额外的文档来说明每一个字段。
     *
 * string
     * org.apache.kafka.common.serialization.StringSerializer	String 序列化
     * org.apache.kafka.common.serialization.StringDeserializer	String 反序列化
 * arvo
     * 解决了JSON的一些问题,但增加了一道转换的工序。
     * 当我们的数据格式固定时,应该避免直接JSON,可以考虑使用Avro。

消费消息

  • 按partition消费以及指定partition和offset进行消费

    • 指定partition进行消息消费

      //手动指定消费指定分区的数据---start
      String topic = "foo";
      TopicPartition partition0 = new TopicPartition(topic, 0);
      TopicPartition partition1 = new TopicPartition(topic, 1);
      consumer.assign(Arrays.asList(partition0,  partition1));
      //手动指定消费指定分区的数据---end
      
    • 指定partition的offset进行消费

      //从指定的partition的指定的offset开始消费数据:
      KafkaConsummer consummer.subscribe(Arrays.asList(topic));
      ConsummerRecords<String,User> records =  consummer.poll(1000);
      consummer.seek(new TopicPartition(topic,partitionNum),offset);
      consummer.commitSync(); // 向ZK提交offset
      

熟悉topic数据过期策略以及实现过程

  • 过期策略
    • 基于时间或者日志大小,具体底层怎么实现的, 就是有个定时任务一样的线程,检测日志大小或者检测时间,记录在一个文件里面,数据超过一定的大小 生成新的文件
  • 压缩合并日志的操作
    • https://medium.com/@sunny_81705/kafka-log-retention-and-cleanup-policies-c8d9cb7e09f8

kafka partition和replica分配策略(可选)

partition分配策略

  • 一个topic里面有好多个分区,数据究竟写入到哪一个分区里面去???

  • kafka的partition默认规则

    /**
     * The default partitioning strategy:
     * <ul>
     * <li>If a partition is specified in the record, use it
     * <li>If no partition is specified but a key is present choose a partition based on a hash of the key
     * <li>If no partition or key is present choose a partition in a round-robin fashion
     */
    

    总结:如果指定了数据的分区号,那么数据直接生产到对应的分区里面去

    如果没有指定分区好,出现了数据key,通过key取hashCode来计算数据究竟该落在哪一个分区里面

    如果既没有指定分区号,也没有指定数据的key,使用round-robin轮询 的这种机制来是实现

  • kafka当中四种分区策略

     //分区策略第一种,如果既没有指定分区号,也没有指定数据key,那么就会使用轮询的方式将数据均匀的发送到不同的分区里面去
                //ProducerRecord<String, String> producerRecord1 = new ProducerRecord<>("mypartition", "mymessage" + i);
                //kafkaProducer.send(producerRecord1);
    
    //第二种分区策略 如果没有指定分区号,指定了数据key,通过key.hashCode  % numPartitions来计算数据究竟会保存在哪一个分区里面
                //注意:如果数据key,没有变化   key.hashCode % numPartitions  =  固定值  所有的数据都会写入到某一个分区里面去
                //ProducerRecord<String, String> producerRecord2 = new ProducerRecord<>("mypartition", "mykey", "mymessage" + i);
                //kafkaProducer.send(producerRecord2);
    
    //第三种分区策略:如果指定了分区号,那么就会将数据直接写入到对应的分区里面去
              //  ProducerRecord<String, String> producerRecord3 = new ProducerRecord<>("mypartition", 0, "mykey", "mymessage" + i);
               // kafkaProducer.send(producerRecord3);
    
    //第四种分区策略:自动以分区策略.如果不自定义分区规则,那么会将数据使用轮询的方式均匀的发送到各个分区里面去
                  //配置我们自定义分区类
            props.put("partitioner.class","cn.itcast.kafka.partition.MyPartitioner");
                kafkaProducer.send(new ProducerRecord<String, String>("mypartition","mymessage"+i));
    

replica 分配策略

  • 1.数据同步

    • kafka 0.8后提供了Replication机制来保证Broker的failover。
      • 引入Replication之后,同一个Partition可能会有多个Replica,而这时需要在这些Replication之间选出一个Leader,Producer和Consumer只与这个Leader交互,其它Replica作为Follower从Leader中复制数据。
  • 副本放置策略

    • Kafka分配Replica的算法如下,下面的broker、partition副本数这些编号都是从0开始编号的

      • 将所有存活的N个Brokers和待分配的Partition排序

      • 将第i个Partition分配到第(i mod n)个Broker上,这个Partition的第一个Replica存在于这个分配的Broker上,并且会作为partition的优先副本( 这里就基本说明了一个topic的partition在集群上的大致分布情况 )

      • 将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上

      • 假设集群一共有4个brokers,一个topic有4个partition,每个Partition有3个副本。下图是每个Broker上的副本分配情况。

      在这里插入图片描述

  • 对于Kafka而言,定义一个Broker是否“活着”包含两个条件:

    • 一是它必须维护与ZooKeeper的session(这个通过ZooKeeper的Heartbeat机制来实现)。
    • 二是Follower必须能够及时将Leader的消息复制过来,不能“落后太多”。

kafka producer的幂等性(可选)

  • 引入目的

    • 生产者重复生产消息。生产者进行retry会产生重试时,会重复产生消息。有了幂等性之后,在进行retry重试时,只会生成一个消息。
  • 幂等性实现

    • PID和Sequence Number
      • PID。每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。
      • Sequence Numbler。(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number。
  • Kafka 中,Producer 默认不是幂等性的

  • 在 0.11 之后,指定 Producer 幂等性的方法很简单,仅需要设置一个参数即可,即 props.put(“enable.idempotence”,ture)

  • 开启条件

    • enable.idempotence=true
    • ack=all
    • retries>1
  • 解幂等性 Producer 的作用范围。

    • 它只能保证单分区上的幂等性,即一个幂等性Producer 能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性
    • 只能实现单会话上的幂等性,不能实现跨会话的幂等性。
    • Producer 进程的一次运行。当你重启了Producer 进程之后,这种幂等性保证就丧失了。
    • 如果想实现多分区以及多会话上的消息无重复,应该怎么做呢?
      • 答案就是事务(transaction)或者依赖事务型 Producer。这也是幂等性 Producer 和事务型 Producer 的最大区别。

使用多线程增加kafka消费能力

  • 某些业务的执行速度实在是太慢,这个时候我们就要用到多线程去消费,提高应用机器的利用率,而不是一味的给kafka增加压力。
  • 使用Spring创建一个kafka消费者是非常简单的。我们选择的方式是继承kafka的ShutdownableThread,然后实现它的doWork方法即可
  • 多线程消费某个分区的数据
    • 创建了一个最大容量为20的线程池,其中有两个参数需要注意一下
      • 使用了了零容量的SynchronousQueue,一进一出,避免队列里缓冲数据,这样在系统异常关闭时,就能排除因为阻塞队列丢消息的可能。
      • 使用了CallerRunsPolicy饱和策略,使得多线程处理不过来的时候,能够阻塞在kafka的消费线程上。
    • 将真正处理业务的逻辑放在任务中多线程执行,每次执行完毕,我们都手工的commit一次ack,表明这条消息我已经处理了。由于是线程池认领了这些任务,顺序性是无法保证的,可能有些任务没有执行完毕,后面的任务就已经把它的offset给提交了
    • Error due to java.util.ConturrentModificationException: kafkaConsumer is not safe for multi-thread access
    • kafka的消费端不是线程安全的,它拒绝你这么调用它的api。kafka的初衷是好的,想要避免一些并发环境的问题,但我确实需要使用多线程处理。
    • kafka消费者通过比较调用者的线程id来判断是否是由外部线程发起请求。
    • commitSync函数放在线程外面了,先提交ack、再执行任务。
  • 加入管道
  • 参数配置
  • 消息保证
  • 使用关闭钩子
  • 使用日志处理
  • 借助redis处理

kafka topic 创建

创建topic的时候,kafka如何分配partition以及replica所在的位置.

要是一个broker down了,那它的replica该怎么重新分配.

  • KafkaApis.handleCreateTopicsRequest()

  • adminManager.createTopics()

  • AdminUtils.assignReplicasToBrokers() assignReplicas就在assignReplicasToBrokers这个函数中完成

    • AdminUtils这个类中,作者在源码上的注释

      • 从broker-list中选定一个随机的位置,从这个位置开始,将每一个partition的第一个replica依次赋予brokerList中的broker.

        • 比如现在有broker0~4,同时该topic有10个partition,随机选定的起始位置是broker0,那么就从broker0开始依次赋予partition,当partition超过了broker的数目时,再回到一开始选定的broker开始分配

        • * broker-0  broker-1  broker-2  broker-3  broker-4
          
          * p0        p1        p2        p3        p4       (1st replica)
          
          * p5        p6        p7        p8        p9       (1st replica)
          
      • 当分配好了第一个replica之后,剩下的replica以第一个replica所在的broker为基准,依次赋予之后的broker

        • 比如partition0的replica0给了broker2,那么partion0的replica1与replica2依次赋予broker3和broker4

        • * broker-0  broker-1  broker-2  broker-3  broker-4
          
          * p0        p1        p2        p3        p4       (1st replica)
          
          * p5        p6        p7        p8        p9       (1st replica)
          
          * p4        p0        p1        p2        p3       (2nd replica)
          
          * p8        p9        p5        p6        p7       (2nd replica)
          
          * p3        p4        p0        p1        p2       (3nd replica)
          
          * p7        p8        p9        p5        p6       (3nd replica)
          
      • kafka集群会通过zookeeper选出一个集群中的leader,由这个leader与zookeeper交互.选举或者加入集群成为follower在一个broker初始化的时候完成.

    private def assignReplicasToBrokersRackUnaware(nPartitions: Int,
    
                                                   replicationFactor: Int,
    
                                                   brokerList: Seq[Int],
    
                                                   fixedStartIndex: Int,
    
                                                   startPartitionId: Int): Map[Int, Seq[Int]] = {
    
      val ret = mutable.Map[Int, Seq[Int]]()
    
      val brokerArray = brokerList.toArray
    
      val startIndex = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length)
    
      var currentPartitionId = math.max(0, startPartitionId)
    
      var nextReplicaShift = if (fixedStartIndex >= 0) fixedStartIndex else rand.nextInt(brokerArray.length)
    
      for (_ <- 0 until nPartitions) {
    
        if (currentPartitionId > 0 && (currentPartitionId % brokerArray.length == 0))
    
          nextReplicaShift += 1
    
        val firstReplicaIndex = (currentPartitionId + startIndex) % brokerArray.length
    
        val replicaBuffer = mutable.ArrayBuffer(brokerArray(firstReplicaIndex))
    
        for (j <- 0 until replicationFactor - 1)
    
          replicaBuffer += brokerArray(replicaIndex(firstReplicaIndex, nextReplicaShift, j, brokerArray.length))
    
        ret.put(currentPartitionId, replicaBuffer)
    
        currentPartitionId += 1
    
      }
    
      ret
    
    }
    
  • 总之经过了某些的运算,replica并不是在broker之间依次分配下去的.而是间隔了nextReplicaShift个broker分配的.

    private def replicaIndex(firstReplicaIndex: Int, secondReplicaShift: Int, replicaIndex: Int, nBrokers: Int): Int = {
    
     val shift = 1 + (secondReplicaShift + replicaIndex) % (nBrokers - 1)
    
     (firstReplicaIndex + shift) % nBrokers
    
    }
    
  • 每两个replica之间所间隔的broker数目取决于

    • 1.nextReplicaShit的大小
    • 2.该replica是该partition的第几个replica
  • 最后的shif在不考虑shift超出了broker数目的情况下t为1+nextReplicaShift+replicaindex

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值