kafka入门学习

kafka概述

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

使用消息队列的好处

  解耦、可恢复性、缓冲、灵活性&峰值处理能力、异步通信

消息队列的两种模式

  1.点对点模式

  2. 发布/订阅模式

Kafka架构

Kafka下载安装

  下载地址:https://www.apache.org/dyn/closer.cgi?path=/kafka/2.5.0/kafka_2.13-2.5.0.tgz

  进入config目录对server.properties进行配置

  修改配置

// broker 的全局唯一编号,不能重复
broker.id=0
// kafka运行日志存放的路径 其实也是数据 
log.dirs=/home/software/kafka/kafka-logs 
// 配置zookeeper集群地址
zookeeper.connect=hadoop01:2181,hadoop02:2181,hadoop03:2181
// 删除topic功能
delete.topic.enable=true 
// hostname、port都会广播给producer、consumer。如果你没有配置了这个属性的话,则使用listeners的值,如果listeners的值也没有配置的话,则使用
// java.net.InetAddress.getCanonicalHostName()返回值(这里也就是返回localhost了)。 advertised.listeners=PLAINTEXT://192.168.2.144:9092

  编辑kafka集群启动/关闭脚本 在bin目录下创建一个文件kafka.sh并授权

  chmod 777 kafka.sh

#!/bin/bash
case $1 in
"start"){
    for i in hadoop01 hadoop02 hadoop03
    do
        echo "********$i********"
        ssh $i "/home/software/kafka/bin/kafka-server-start.sh -daemon /home/software/kafka/config/server.properties"
    done
};;

"stop"){
    for i in hadoop01 hadoop02 hadoop03
    do
        echo "********$i********"
        ssh $i "/home/software/kafka/bin/kafka-server-stop.sh"
    done
};;
esac

  kafka集群启动

    启动前先启动zookeeper集群

    在kafka bin目录下执行 kafka.sh

  在bin目录下执行:

  创建自定义的topic

./kafka-topics.sh --create --zookeeper hadoop01:2181 --replication-factor 1 --partitions 1 --topic mayun

  查看所有的topic  

./kafka-topics.sh --list --zookeeper hadoop01:2181

  启动producer

./kafka-console-producer.sh --broker-list hadoop01:9092,hadoop02:9092,hadoop03:9092 --topic mayun

  启动consumer

./kafka-console-consumer.sh --bootstrap-server hadoop01 --topic mayun --from-beginning

kafka分区策略

1)分区原因

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

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

2)分区原则

   我们需要将producer发送的数据封装成一个ProducerRecord对象

public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers)
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value)
public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers)
// 指定分区
public ProducerRecord(String topic, Integer partition, K key, V value)
// key的hash值与topic的partiton数进行取余得到partition值
public ProducerRecord(String topic, K key, V value)
// 第一次调用随机整数,将这个值与topic可用的partition总数取余得到partition值,之后轮询,也就是round-robin算法
public ProducerRecord(String topic, V value)

kafka文件存储机制

kafka生产者

数据可靠性保证

  为保证producer发送的数据,即可靠的发送指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),

如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。

  选用第二种方案后做出优化

  ISR:Leader维护了一个动态的in-sync replica set(ISR),意为和leader保存同步的follower集合,当ISR中的follower完成数据同步之后,follower就会给leader发送ack,

如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阀值由replica.lag.time.max.ms参数设定,leader发生故障之后,就会从ISR中选举新的leader。

  ack参数配置

  ack:

    0: producer不等待broker的ack,这一操作提供了最低延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。

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

    -1(all):producer等待broker的ack,partition的leader和follower(ISR)全部落盘成功之后才返回ack,但如果在follower同步完成后,

    broker发送ack之前,leader发生故障,那么会造成数据重复。

  幂等性:

    要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可(ack 默认 -1),kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据的上游。

  开启幂等性的producer在初始化的时候会被分配一个PID,发往同一partition的消息会附带Sequence Number。而Broker端会对<PID,Partition,SeqNumber>做缓存,

  当具有相同主键的消息提交时,Broker只会持久化一条。

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

  kafka事务:

    Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 id,即使故障恢复后也不会改变,这个 id 就是 TransactionnalId(也叫   txn.id),txn.id 可以跟内部的 PID 1:1 分配,它们不同的是 txn.id 是用户提供的,而 PID 是 Producer 内部自动生成的(并且故障恢复后这个 PID 会变化),有了 txn.id 这个机制,就可以实现多 partition、跨会话的 EOS 语义。

  当用户使用 Kafka 的事务性时,Kafka 可以做到的保证:

  1. 跨会话的幂等性写入:即使中间故障,恢复后依然可以保持幂等性;
  2. 跨会话的事务恢复:如果一个应用实例挂了,启动的下一个实例依然可以保证上一个事务完成(commit 或者 abort);
  3. 跨多个 Topic-Partition 的幂等性写入,Kafka 可以保证跨多个 Topic-Partition 的数据要么全部写入成功,要么全部失败,不会出现中间状态。

  上面是从 Producer 的角度来看,那么如果从 Consumer 角度呢?Consumer 端很难保证一个已经 commit 的事务的所有 msg 都会被消费,有以下几个原因:

  1. 对于 compacted topic,在一个事务中写入的数据可能会被新的值覆盖;
  2. 一个事务内的数据,可能会跨多个 log segment,如果旧的 segmeng 数据由于过期而被清除,那么这个事务的一部分数据就无法被消费到了;
  3. Consumer 在消费时可以通过 seek 机制,随机从一个位置开始消费,这也会导致一个事务内的部分数据无法消费;
  4. Consumer 可能没有订阅这个事务涉及的全部 Partition。

  事务性示例:

  Kafka 事务性的使用方法也非常简单,用户只需要在 Producer 的配置中配置 transactional.id,通过 initTransactions() 初始化事务状态信息,再通过 beginTransaction() 标识一个事务的开始,然后通过 commitTransaction() 或 abortTransaction() 对事务进行 commit 或 abort,示例如下所示:

Properties props = new Properties();
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "ProducerTranscationnalExample");
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "test-transactional");
props.put("acks", "all");
KafkaProducer producer = new KafkaProducer(props);
producer.initTransactions();

try {
    String msg = "matt test";
    producer.beginTransaction();
    producer.send(new ProducerRecord(topic, "0", msg.toString()));
    producer.send(new ProducerRecord(topic, "1", msg.toString()));
    producer.send(new ProducerRecord(topic, "2", msg.toString()));
    producer.commitTransaction();
} catch (ProducerFencedException e1) {
    e1.printStackTrace();
    producer.close();
} catch (KafkaException e2) {
    e2.printStackTrace();
    producer.abortTransaction();
}
producer.close();

  故障处理:

    

  HW之前的数据才对consumer可见

  1) follower故障

    follower发生故障后会被临时提出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次HW,并将log文件高于HW的部分截取掉,

  从HW开始向leader进行同步,等该follwer的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

  2)leader故障

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

  然后从新的leader同步数据。

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

kafka消费者

  kafka消费方式

    consumer采用pull(拉)模式从broker中读取数据

    push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的

    pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环汇总,一直返回空数据。

  分区分配策略

    一个consumer group 中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定那个partition由那个consumer来消费

  kafka有两种分区策略,RoundRobin、Range 

kafka offset机制

  Consumer消费者的offset存储机制(key:消费者组+主题+分区 value:offset)

  Consumer在从broker读取消息后,可以选择commit,该操作会在Kakfa中保存该Consumer在该 Partition中读取的消息的offset。

该Consumer下一次再读该Partition时会从下一条开始读取。 通过这一特性可以保证同一消费者从Kafka中不会重复消费数据。

旧版本的Kafka是把消费者的offset存到Zookeeper,这种机制缺点是对zookeeper造成较大的负载 新版本的Kafka的offset是由kafka自己来管理

进入kafka-logs目录查看,会发现多个很多目录,这是因为kafka默认会生成50个 __consumer_offsets 的目录,用于存储消费者消费的offset位置。

Kafka会使用下面公式计算该消费者group位移保存在__consumer_offsets的哪个目录上: Math.abs(groupID.hashCode()) % 50

 

kafka 高效读写数据 

1)顺序读写磁盘

  为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对 应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。

因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比 随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。

2)零拷贝复制技术(来源 https://www.jianshu.com/p/835ec2d4c170

  kafka中的消费者在读取服务端的数据时,需要将服务端的磁盘文件通过网络发送到消费者进程,网络发送需要经过几种网络节点。如下图所示:

传统的读取文件数据并发送到网络的步骤如下:
(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。

通常情况下,Kafka的消息会有多个订阅者,生产者发布的消息会被不同的消费者多次消费,为了优化这个流程,Kafka使用了“零拷贝技术”,如下图所示:

“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。

如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

Kafka API

  消息发送流程

    Kafka的Producer发送消息采用的是异步发送的方式,在消息发送的过程中,涉及到了两个线程--main线程和Sender线程,以及一个线程共享变量--RecordAccumulator。

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

  API普通生产者

/**
 * 普通生产者
 */
public class MyProducer {
    public static void main(String[] args) {
        // 创建kafka生产者配置信息
        Properties properties = new Properties();
        // 指定kafka连接集群
        properties.put("bootstrap.servers", "192.168.2.144:9092");
        // ack应答级别
        properties.put("acks", "all");
        // 重试次数
        properties.put("retries", 3);
        // 批次大小
        properties.put("batch.size", 16384);
        // 等待时间
        properties.put("linger.ms", 1);
        // RecordAccumulator缓冲区大小
        properties.put("buffer.memory", 33554432);
        // key,value 序列化类
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        // 发送数据
        for (int i = 0; i < 20; i++) {
            producer.send(new ProducerRecord<String, String>("mayun", "money" + i));
        }
        // 关闭资源
       producer.close();
    }
}

  回调生产者

/**
 * 回调生产者
 */
public class CallBackProducer {
    public static void main(String[] args) {
        // 创建配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.144:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

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

        // 发送数据
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<>("mayun", "money" + i), (recordMetadata, e) -> {
                if (e == null) {
                    System.out.println(recordMetadata.partition() + "---" + recordMetadata.offset());
                } else {
                    e.printStackTrace();
                }
            });
        }
        producer.close();
    }
}

  API普通消费者

public class MyConsumer {
    public static void main(String[] args) {
        // 创建配置信息
        Properties properties = new Properties();
        // 配置集群
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.144:9092");
        // 开启自动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
        // 自动提交延时
        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        // 消费组
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "abt");
        
        // 创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
        
        // 订阅主题
        consumer.subscribe(Arrays.asList("maYun","maHuaTen"));
        
        // 获取数据
        ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
        
        // 解析并打印consumerRecords
        for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
            System.out.println(consumerRecord.key() + "---" + consumerRecord.value());
        }
        
        // 关闭连接
        consumer.close();
    }
posted @ 2020-06-21 16:23  南鸽  阅读(172)  评论(0编辑  收藏  举报