Apache Kafka的架构以及使用

Apache Kafka

一、概述

Apache Kafka是一个分布式的流数据平台,代表三层含义:

  • Publish/Subscribe: 消息队列系统 MQ(Message Queue)
  • Process: 流数据的实时处理(Stream Process)
  • Store: 流数据会以一种安全、容错冗余存储机制存放到分布式集群中

架构

在这里插入图片描述

应用场景

1.两大类:
  • 构建实时的流数据管道,在系统和应用之间进行可靠的流数据传输
  • 构建实时的流数据处理应用,对流数据进行转换和加工处理
2.实际场景举例:
  • 活动跟踪:
    跟踪网站用户和前端应用发生的交互,比如页面访问次数和点击,将这些信息作为消息发布到一个或者多个主题上,这样就可以根据这些数据为机器学习提供数据,更新搜素结果等等(头条、淘宝等总会推送你感兴趣的内容,其实在数据分析之前就已经做了活动跟踪)。
  • 应用解耦:
    消息队列将消息生产者和订阅者分离,实现应用解耦。如电商的订单系统与库存系统。用户下单,将消息写入消息队列,返回用户订单下单成功。库存操作根据订阅的下单消息进行,下单时库存系统不能正常使用也不影响下单。实现解耦。
  • 流量削峰:
    在应用前端以消息队列接收请求,当请求超过队列长度,直接不处理重定向至一个静态页面,来达到削峰的目的,此场景一般用于秒杀活动。
  • 流处理:
    现在非常流行的框架(如Storm,Spark Streaming、Flink)从topic中读取数据,实时对其进行处理,并将处理后的数据写入新topic中,供用户和应用程序使用。

核心概念

  • Cluster: kafka支持一到多个服务构成的分布式集群,每一个服务实例成为Broker
  • Topic: 某一个分类的消息的集合,如:订单的topic、商品的topic等
  • Partition: 一个Topic有若干个分区(Partition)构成,分区的数量在创建Topic时手动指定
  • Replication: 分区副本,是Partition的冗余备份分区,当Partition不可用时,ZooKeeper会自动将Replication(Follower)分区升级为Partition(Leader)分区
  • Offset: 分区中的Record的位置标示,每一个消费者都会记录自己的消费位置(offset)
  • Producer:主题生产者。发布消息的对象
  • Consumer:主题消费者。订阅并处理消息的对象

Topic和Log

Each partition is an ordered, immutable sequence of records that is continually appended to—a structured commit log

Kafka的每一个分区(Partition),都是一个有序、不可变的持续追加的记录序列,Kafka会以一种结构化的提交日志保存分区中的数据。

在这里插入图片描述

注意:在分区中写入数据时,会在队列的末尾进行追加,每一个消费者都维护的有一个自己的消费位置(offset)

在这里插入图片描述

二、基础使用

命令行操作

Topic使用
  • 新建Topic

    [root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic tt1 --partitions 3 --replication-factor 1 --create
    
  • 展示Topic列表

    [root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --list
    
  • 删除Topic

    [root@spark kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --delete --topic tt1
    
发布和订阅
  • 发布消息

    [root@spark kafka_2.11-2.2.0]# bin/kafka-console-producer.sh --broker-list spark:9092 --topic tt1
    >hello
    >五月
    >hello
    >hello kafka
    
  • 订阅消息

    [root@spark kafka_2.11-2.2.0]# bin/kafka-console-consumer.sh --topic tt1 --bootstrap-server spark:9092
    hello
    五月
    hello
    hello kafka
    

JAVA Driver

Maven依赖
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.2.0</version>
</dependency>
生产者
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.UUID;

/**
 * kafka 生产者的测试类
 */
public class ProducerDemo {

    public static void main(String[] args) {
        //1. 准备Kafka生产者配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"spark:9092");
        // string 序列化(Object ---> byte[])器
  properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);    properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);

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

        //3. 生产记录并将其发布
        ProducerRecord<String, String> record = new ProducerRecord<String, String>("tt1", UUID.randomUUID().toString(),"Hello Kafka");

        producer.send(record);

        //4. 释放资源
        producer.flush();
        producer.close();
    }
}

1) Kafka的消息生产者,负责生产数据(Record K\V\Timestamp),最终发布(Publish)保存到Kafka集群

2)数据的保存策略:

  • 如果 Record的 Key不为 Null,采用哈希算法: key.hashCode % numPartitions = 余数(分区序号)
  • 如果 Record的 Key为 Null, 采用轮询策略
  • 手动指定存放的分区

3) 数据会以一种分布式的方式保存在 Kafka集群中,每一个分区都会维护一个队列的数据结构,新产生的数据会追加到队列的末尾,并且分配 offset

4)数据在 Kafka集群中默认最多保留 7天(168Hours),不论是否消费,在保留周期到达后都会自动被删除。

5)数据在 Kafka中可以进行重复消费,重置消费 offset即可

消费者
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;

/**
 * kafka消费者测试类
 * 1. 订阅 subscribe
 * 2. 拉取 pull
 */
public class ConsumerDemo {
    public static void main(String[] args) {
        //1. 指定kafka消费者的配置信息
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
        // 反序列化器 byte[] ---> Object
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 消费组必须得指定
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "group1");

        //2. 创建kafka消费者对象
        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);

        //3. 订阅主体topic
        consumer.subscribe(Arrays.asList("t2"));

        //4. 拉取新产生的记录
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(record.key() + "\t" + record.value() + "\t"
                        + record.topic() + "\t" + record.offset()
                        + "\t" + record.timestamp() + "\t" + record.partition());
            }
        }
    }
}

在这里插入图片描述

1)消费者并不是独立存在,kafka中消费者会以消费组的方式进行组织和管理

2)消费组符合特征: 组外广播、组内负载均衡

  • 组外广播: 保证不同的消费组,能够独立消费新产生的数据
  • 组内负载均衡: 消息只会被消费组中的一个消费者进行处理,多个消费组提高了Kafka并行处理能力

3)消费者可以订阅一个到多个感兴趣的Topic,一旦这些Topic有新的数据产生,消费者会自动拉取新产生的数据,进行相应的业务处理

4)消费者在消费消息时,会维护一个消费的位置(offset),下一次消费时会自动从offset向后进行消费。

​ 在kafka中数据会有一个默认的保留周期(7天),在保留期内数据是可以进行重复消费的,只需要重置消费者消费的offset即可。

5)__consumer_offsets是一个特殊topic,主要记录了Kafka消费组的消费位置。

三、其他使用

偏移量控制

Kafka消费者在订阅Topic时,会自动拉取Topic中新产生的数据。首次消费时使用默认的偏移量消费策lastest

偏移量消费策略:

  • lastest(默认):如果有已提交的offset,从已提交的offset之后消费消息。如果无提交的offset,从最后的offset之后消费数据

  • earliest:如果有已提交的offset,从已提交的offset之后消费消息。如果无提交的offset,从最早的offset消费消息

    // 注意:此配置项 修改偏移量消费策略的默认行为 
    properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
    
    

Kafka消费者消费位置offset,默认采用自动提交的方式,将消费位置提交保存到特殊Topic_consumer_offsets中

自动提交策略:

// 默认自动提交消费的位置offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
// 默认每隔5秒提交一次消费位置
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,5000);

通常情况需要手动提交消费位置:

为什么需要手动提交消费位置(offset)的原因?

原因:如果自动提交消费位置,有可能在进行业务处理时出现错误,会造成数据没有被正确处理。

​ 手动提交消费位置,可以保证数据一定能够被完整的正确处理。

// 关闭消费位置offset的自动提交功能
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

// 手动提交消费位置
consumer.commitSync();

消费方式

订阅(Subscribe)

消费者订阅1到N个感兴趣的Topic,一旦Topic中有新的数据产生,会自动拉取Topic分区内的所有数据

// 订阅(消费)Topic所有的分区
consumer.subscribe(Arrays.asList("t3"));

指定消费分区

消费者在消费数据时,可以只消费某个Topic特定分区内的数据

// 指定消费Topic的特定分区
consumer.assign(Arrays.asList(new TopicPartition("t3",0)));

重置消费位置

消费者在消费数据时,可以重置消费的offset,消费已消费的数据或者跳过不感兴趣的数据

consumer.assign(Arrays.asList(new TopicPartition("t3",0)));
// 重置消费位置
consumer.seek(new TopicPartition("t3",0),1);

生产者的批量发送

kafka生产者产生的多条数据共享同一个连接,发送保存到Kafka集群,这种操作方式称为:Batch(批处理)。

批处理相比于传统的发送方式,资源利用率更为高效,是一种比较常用的生产者优化策略。

使用方法
# 生产者方 添加如下配置项即可
# 两个条件 满足其一即可
batch.size = 16384Bytes  16kb// 缓冲区大小
linger.ms = 毫秒值            // 缓冲区中数据的驻留时长 

具体使用方法
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,16384);
properties.put(ProducerConfig.LINGER_MS_CONFIG,2000);

Kafka和Spring Boot整合

依赖
 <dependencies>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.kafka</groupId>
         <artifactId>spring-kafka</artifactId>
     </dependency>

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
     </dependency>
     <dependency>
         <groupId>org.springframework.kafka</groupId>
         <artifactId>spring-kafka-test</artifactId>
         <scope>test</scope>
     </dependency>
</dependencies>

配置文件
spring.kafka.bootstrap-servers= HadoopNode01:9092,HadoopNode02:9092,HadoopNode03:9092
spring.kafka.consumer.group-id= g1
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

生产者API
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.UUID;

@Component
public class KafkaProducerDemo {

    @Autowired
    private KafkaTemplate<String,String> template;

    // 计划任务,定时发送数据
    // cron 秒 分 时 日 月 周 年(省略)
    @Scheduled(cron = "0/10 * * * * ?")
    public void send(){
        template.send("tt1", UUID.randomUUID().toString(),"Hello Kafka");
        //System.out.println(new Date());
    }
}

消费者API
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
public class KafkaConsumerDemo {

    @KafkaListener(topics = "tt1")
    public void receive(ConsumerRecord<String, String> record) {
        System.out.println(record.key() + "\t" + record.value());
    }
}

生产者幂等操作

幂等: 指的多次操作,影响结果是一致的,这种操作方式就被成为幂等操作

结论:使用Kafka生产者幂等操作原因,kafka生产者在重试发送生产数据时,多次重试操作只会在Kafka的分区队列的末尾写入一条记录

在这里插入图片描述

使用方法
properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true); // 开启幂等操作支持
// ack时机 -1或者all 所有  1 leader  0 立即应答
properties.put(ProducerConfig.ACKS_CONFIG,"all");  
properties.put(ProducerConfig.RETRIES_CONFIG,5);   // 重复次数
properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000); // 请求超时时间

Kafka事务

数据库事务: 一个连接中多个操作不可分割,是一个整体,要么同时成功,同时失败。

Kafka的事务类似于数据库事务,每一个事务操作都需要一个唯一的事务ID(Transaction-ID),并且事务默认的隔离级别为READ_UNCOMMITTEDREAD_COMMITTED

生产者事务

生产者事务: Kakfka生产者生产的多条数据是一个整体,不可分割,要么同时写入要么同时放弃

要求
  • kafka生产者提供唯一的事务ID
  • 必须开启kafka的幂等性支持
事务操作
  • 初始化事务
  • 开启事务
  • 正确操作 提交事务
  • 操作失败 回滚事务
生产者API
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;
import java.util.UUID;

/**
 * kafka 生产者的测试类
 */
public class ProducerDemo {

    public static void main(String[] args) {
        //1. 准备Kafka生产者配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"spark:9092");
        // string 序列化(Object ---> byte[])器
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class);

        // 事务ID, 唯一不可重复
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,UUID.randomUUID().toString());
        // 开启幂等操作支持
        properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
         // ack时机 -1或者all 所有  1 leader  0 立即应答
        properties.put(ProducerConfig.ACKS_CONFIG,"all"); 
        properties.put(ProducerConfig.RETRIES_CONFIG,5);   // 重复次数
        properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000); // 请求超时时间

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

        // 初始化事务
        producer.initTransactions();

        // 开启事务
        producer.beginTransaction();

        try {
            //3. 生产记录并将其发布
            for (int i = 50; i < 60; i++) {
                if(i == 56) {
                    int m = 1/0; //人为制造错误
                }
                // key不为null  第一种策略
                ProducerRecord<String, String> record = new ProducerRecord<String, String>("tt1", UUID.randomUUID().toString(),"Hello Kafka"+i);
                // key为null 轮询策略
                producer.send(record);
            }
            // 提交事务
            producer.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
            // 取消事务
            producer.abortTransaction();
        } finally {
            //4. 释放资源
            producer.flush();
            producer.close();
        }
    }
}

消费者API
// 修改消费者默认的事务隔离级别,consumer只能读取已成功提交事务的消息
properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");

消费生产并存事务

consume-transform-produce:指消费和生产处于同一个事务环境中,要么消费生产同时成功,要么同时失败

要求
  • kafka生产者提供唯一的事务ID
  • 必须开启kafka的幂等性支持
  • 关闭offset的自动提交功能
  • 不能调用手动提交的方法,如: consumer.commitSync()

创建消费Topic,以及发布的Topic

[root@HadoopNode01 kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic t6 --partitions 3 --replication-factor 1 --create
[root@HadoopNode01 kafka_2.11-2.2.0]# bin/kafka-topics.sh --bootstrap-server spark:9092 --topic t7 --partitions 3 --replication-factor 1 --create

核心代码
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

import java.time.Duration;
import java.util.*;

/**
 * 消费生产并存事务
 */
public class ConsumeTransformProduceDemo {
  public static void main(String[] args) {
  //1. 初始化生产者和消费者的配置对象
      KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(consumerConfig());
        KafkaProducer<String, String> producer = new KafkaProducer<>(producerConfig());

        //2. 消费者订阅topic
        consumer.subscribe(Arrays.asList("t6"));

        //3. 事务操作
        producer.initTransactions();

        while (true) {
            producer.beginTransaction();
            try {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
                Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
                for (ConsumerRecord<String, String> record : records) {
                    // 需要业务处理的内容
                    System.out.println(record.key() + "--->" + record.value());
                    producer.send(new ProducerRecord<String,String>("t7","t7:"+record.value()));
                    // 将消费位置记录到map集合中
                    offsets.put(new TopicPartition("t6",record.partition()),new OffsetAndMetadata(record.offset()+1));
                }
                // 维护消费位置  将事务内的消费位置信息 提交到kafka中
                producer.sendOffsetsToTransaction(offsets,"g1");

                // 正确操作 提交事务
                producer.commitTransaction();
            } catch (Exception e) {
                e.printStackTrace();
                producer.abortTransaction();
            }
        }
    }

    public static Properties producerConfig() {
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, UUID.randomUUID().toString());
        properties.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, Boolean.TRUE);
        properties.put(ProducerConfig.RETRIES_CONFIG, 5);
        properties.put(ProducerConfig.ACKS_CONFIG, "all");
        properties.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 3000);
        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        properties.put(ProducerConfig.LINGER_MS_CONFIG, 2000);
        return properties;
    }

    public static Properties consumerConfig() {
        Properties properties = new Properties();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "spark:9092");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "g1");
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        properties.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
        return properties;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值