拉勾教育学习-笔记分享の"斡旋"消息中间件 III (Kafka)

253 阅读21分钟

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
中间件知识点up

登山则情满于山,观海则意溢于海


一、Kafka架构与实战

part 1 - 概念与基本架构

「主角登场」

最初,Kafka由Linkedin公司开发,初衷是开发一个 分布式分区生产者-订阅者模式基于zookeeper协调的分布式日志系统,后续因为其强大的持久化能力以及高吞吐率也被当做了MQ系统来使用。(2010年被贡献给Apache基金会并成为了顶级开源项目)

主要应用场景:日志收集系统/消息系统

设计目标

  1. 时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能。
  2. 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输。
  3. 支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。
  4. 支持离线数据处理和实时数据处理
  5. 支持在线水平扩展

通常来说,消息的传递有两种模式:「点对点传递」&「发布-订阅模式」,而Kafka是典型的发布-订阅模式;同时,一般消息中间件有两种模式:「推模式」&「拉模式」,而Kafka只有拉没有推(可以通过轮询实现消息的推送)

  • Kafka在一个或多个可以跨越多个数据中心的服务器上作为集群运行
  • Kafka集群按照主题分类管理,一个主题可以有多个分区,一个分区可以有多个副本分区
  • 每个记录由一个,一个和一个时间戳组成

image.png

四个核心API

  1. Producer API:允许应用程序将记录流发布到一个或多个Kafka主题
  2. Consumer API:允许应用程序订阅一个或多个主题并处理为其生成的记录流
  3. Streams API:允许应用程序充当流处理器,使用一个或多个主题的输入流,并生成一个或多个输出主题的输出流,从而有效地将输入流转换为输出流
  4. Connector API:允许构建和运行将Kafka主题连接到现有应用程序或数据系统的可重用生产者或使用者。例如,关系数据库的连接器可能会捕获对表的所有更改

「优势」

  1. 高吞吐量:单机每秒处理几十上百万的消息量。即使存储了许多TB的消息,它也保持稳定的性能。
  2. 高性能:单节点支持上千个客户端,并保证零停机和零数据丢失。
  3. 持久化数据存储:将消息持久化到磁盘。通过将数据持久化到硬盘以及replication防止数据丢失。
    1. 零拷贝
    2. 顺序读,顺序写
    3. 利用Linux的页缓存
  4. 分布式系统,易于向外扩展。所有的Producer、Broker和Consumer都会有多个,均为分布式的。无需停机即可扩展机器。多个Producer、Consumer可能是不同的应用。
  5. 可靠性 - Kafka是分布式,分区,复制和容错的。
  6. 客户端状态维护:消息被处理的状态是在Consumer端维护,而不是由server端维护。当失败时能自动平衡。
  7. 支持online和offline的场景
  8. 支持多种客户端语言。Kafka支持Java、.NET、PHP、Python等多种语言。

「应用场景」

  • 日志收集:通过Kafka以统一接口服务的方式开放给各种Consumer,从而收集各类服务Log
  • 消息系统:解耦生产者和消费者、缓存消息等
  • 用户活动跟踪:Kafka经常被用来记录Web用户或者App用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到Kafka的Topic中,然后消费者通过订阅这些Topic来做实时的监控分析,亦可保存到数据库
  • 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
  • 流式处理:如Spark Streaming和Storm

「基本架构」

消息&批次

Kafka的数据单元称为消息。可以把消息看成是数据库里的一个“数据行”或一条“记录”。消息由字节数组组成。

为了提高效率,消息被分批写入Kafka。批次就是一组消息,这些消息属于同一个主题和分区。

把消息分成批次可以减少网络开销。批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但是需要更多的计算处理。

模式

消息模式(schema)有许多可用的选项,以便于理解。如JSONXML,但是它们缺乏强类型处理能力。
Kafka的许多开发者喜欢使用Apache Avro

Avro提供了一种紧凑的序列化格式,模式和消息体分开。当模式发生变化时,不需要重新生成代码,它还支持强类型和模式进化,其版本既向前兼容,也向后兼容。

数据格式的一致性对Kafka很重要,因为它消除了消息读写操作之间的耦合性。

主题&分区

Kafka的消息通过主题(Topic)进行分类。主题可比作是数据库的表或者文件系统里的文件夹。主题可以被分为 若干分区,一个主题通过分区分布于Kafka集群中,提供了横向扩展的能力。

image.png

生产者&消费者

生产者创建消息。消费者消费消息。

一个消息会被生产者发布到一个指定的主题上,默认情况下,生产者会均衡的发布消息在该主题的所有分区中。 可归纳为以下步骤:

  1. 可指定消息的分区(不指定即为均衡发布)
  2. 根据消息的key散列取模得出分区
  3. 轮询指定分区

消费者通过偏移量来区分已经读过的消息,从而消费消息。
消费者是消费组的一部分。消费组保证每个分区只能被一个消费者使用,避免重复消费。

image.png

broker&集群

一个独立的Kafka服务器称为broker
broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘上的消息。单个broker可以轻松处理数千个分区以及每秒百万级的消息量。

image.png

每个集群都有一个broker是集群控制器"首领"(自动从集群的活跃成员中选举出来)。
"首领"负责将分区分配给broker并监控这些broker,一个分区可以分配给多个broker,此时会发生分区复制(如上图中的Broker1中的首领将自己的分区复制给了Broker2)

「核心概念」

  • Producer-生产者发布消息
    • 生产者将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的 segment 文件中。
      • 默认情况下通过轮询把消息均衡地分布到主题的所有分区上。
      • 在某些情况下,生产者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。
      • 生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。
  • Consumer-消费者读取消息
    1. 消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。
    2. 消费者通过检查消息的偏移量来区分已经读取过的消息
      偏移量是一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka 会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Zookeeper 或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。
    3. 消费者是消费组的一部分。群组保证每个分区只能被一个消费者使用。
    4. 如果一个消费者失效,消费组里的其他消费者可以接管失效消费者的工作,再平衡,分区重新分配。
  • Broker-为消费者提供服务
    • broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。
      • 如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的一个partition
      • 如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的一个partition,剩下的M个broker不存储该topic的partition数据
      • 如果某topic有N个partition,集群中broker数目少于N个,那么一个broker存储该topic的一个或多个partition。在实际生产环境中,尽量避免这种情况的发生,这种情况容易导致Kafka集群数据不均衡
  • Topic-主题用于划分消息类别
    • 主题就好比数据库的表,尤其是分库分表之后的逻辑表。
  • Partition-分区实现数据冗余和伸缩
    1. 主题可以被分为若干个分区,一个分区就是一个提交日志。
    2. 消息以追加的方式写入分区,然后以先入先出的顺序读取。
    3. 无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。
    4. Kafka 通过分区来实现数据冗余和伸缩性。
    5. 在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
  • Replicas-副本保证数据的一致性
    • Kafka 使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在broker 上,每个broker 可以保存成百上千个属于不同主题和分区的副本。
      • 首领副本:每个分区都有一个首领副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本
      • 跟随者副本:首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息,保持与首领一致的状态。如果首领发生崩溃,其中的一个跟随者会被提升为新首领。
  • Offset-确保读写秩序image.png
    • 目前ConsumerA消费到了offset=9的位置,ConsumerB消费到了offset=11的位置,当下一次他们来消费的时候,可以选择接着上一次继续消费,或者重头开始消费,也可以跳到尾端从最新消息开始消费。

part 2 - 安装与配置

因为Kafka基于Zookeeper,Zk又基于java,所以需要确保运行环境 jdk 和 zookeeper 的可用!

jdk在linux下的环境配置在此不赘述,有关zookeeper的额环境配置参考如下:

  1. ../zookeeper-x.x.x/conf/zoo.cfg 中编辑 dataDir=../zookeeper.data (zookeeper保存数据的目录)
  2. 编辑 /etc/profile
    export ZOOKEEPER_PREFIX=/root/my-software/apache-zookeeper-x.x.x
    export PATH=$PATH:$ZOOKEEPER_PREFIX/bin
    export ZOO_LOG_DIR=$PATH:$ZOOKEEPER_PREFIX/logs
    
  3. 使配置生效:
    source /etc/profile
    
  4. 验证
    zkServer.sh status
    
    image.png

Kafka的安装与配置

  1. 官网下载 解压包
  2. 执行命令 tar -zxvf kafka_x.x.x.tgz -C /opt 解压
  3. 配置环境变量 vim /etc/profile
    export KAFKA_HOME=/opt/kafka_x.x.x
    export PATH=$PATH:$KAFKA_HOME/bin
    
  4. 配置/opt/kafka_2.12-1.0.2/config中的server.properties文件
    ...
    # 指定用于存放相关日志的路径
    log.dirs=/opt/kafka_x.x.x/logs
    ...
    # 指定zookeeper的映射路径(此处默认本主机,'myKafka'->将来在zk中创建的初始节点名)
    zookeeper.connect=localhost:2181/myKafka
    
  5. 启动zk后,使用命令 ./bin/kafka-server-start.sh config/server.properties 启动kafka
    • 启动成功后:image.png
    • 当然也可以通过守护进程形式启动
      ./bin/kafka-server-start.sh -daemon config/server.properties
  6. 查看kafka后台进程 pa -ef | grep kafka
  7. 启动zookeeper客户端查看节点 ./zkCli.sh + ls /myKafka
    • /brokers: 保存了Kafk集群的所有信息,包括每台broker的注册信息,集群上所有topic的信息等。
    • /controller: 保存了Kafka controller组件(controller负责集群的领导者选举)的注册信息,同时也负责controller的动态选举.
    • /admin: 保存管理脚本的输出结果,比如删除topic,对分区进行重分配等操作。
    • /isr_change_notification: 保存isr列表发生变化的分区列表。controller会注册一个监听器实时监控该节点下子节点的变更。
    • /config: 保存了Kafka集群下各种资源的定制化配置信息,比如每个topic可能有自己专属的一组配置。那么就保存在/config/topic/<topic>下
    • /cluster: 保存了Kafka集群的简要信息,包括集群ID信息和集群版本号
    • /controller_epoch: 保存了controller组件的版本号,Kafka使用该版本号来隔离无效的controller请求。
  8. (守护线程启动情况下)关闭后台运行的kafka:./kafka-server-stop.sh
  9. 其他重要脚本
    1. kafka-topics.sh : 主题管理命令
      # 列出现有的主题
      kafka-topics.sh --list --zookeeper localhost:2181/myKafka
      # 创建主题(以下语义'TOPIC_001主题包含一个分区1(默认Leader分区)')
      kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic TOPIC_001 --partitions 1 --replication-factor 1
      # 查看分区信息
      kafka-topics.sh --zookeeper localhost:2181/myKafka --list
      # 查看指定主题的详细信息
      kafka-topics.sh --zookeeper localhost:2181/myKafka --describe 
      # 删除指定主题
      kafka-topics.sh --zookeeper localhost:2181/myKafka --delete --topic TOPIC_001
      
    2. kafka-console-producer.sh : 生产消息
      # 开启生产者
      kafka-console-producer.sh --topic TOPIC_001 --broker-list localhost:9092
      
    3. kafka-console-consumer.sh : 消费消息
      # 开启消费者
      kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_001
      # 开启消费者2(从头消费,不按照偏移量消费)
      kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_001 --from-beginning
      

part 3 - 开发实战

「消息的发送与接收」

生产者的主要对象:KafkaProducer(发送消息) / ProducerRecord(封装Kafka消息)

KafkaProducer创建时需指定的参数及含义

参数说明
bootstrap.servers配置生产者与 broker 建立连接的方式
该参数设置的是初始化参数。如果生产者需要连接的是Kafka集群,则这里配置集群中几个broker的地址,而不是全部,当生产者连接上此处指定的broker之后,在通过该连接发现集群中的其他节点。
key.serializer要发送信息的 key 数据的序列化类
设置的时候可以写类名,也可以使用该类的Class对象。
value.serializer要发送消息的 value 数据的序列化类
设置的时候可以写类名,也可以使用该类的Class对象。
acks应答消息。(默认值:all)

acks=0:生产者不等待broker对消息的确认,只要将消息放到缓冲区,就认为消息已经发送完成。
(该情形不能保证broker是否真的收到了消息,retries配置也不会生效。发送的消息的返回的消息偏移量永远是-1。)
acks=1:表示消息只需要写到主分区即可,然后就响应客户端,而不等待副本分区的确认。
(在该情形下,如果主分区收到消息确认之后就宕机了,而副本分区还没来得及同步该消息,则该消息丢失。)
acks=all:首领分区会等待所有的ISR副本分区确认记录。
(该处理保证了只要有一个ISR副本分区存活,消息就不会丢失。这是Kafka最强的可靠性保证,等效于 acks=-1)
retries重试次数

case1 ==> 当消息发送出现错误的时候,系统会重发消息。
case2 ==> 跟客户端收到错误时重发一样。
(如果设置了重试,还想保证消息的有序性,需要设置 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 ,否则在重试此失败消息的时候,其他的消息可能发送成功了)
  1. 更多参数可以直接在 org.apache.kafka.clients.producer.ProducerConfig 中查找
  2. 有关 acks 参数:消费者消费消息后,需要broker端的确认,可以同步确认但是效率低;可以异步确认效率高但是需要设置回调函数

image.png


下面通过一个java demo,简单看看消息的使用

-kafka- 服务预备

在kafka服务中创建好主题 topic_1,并且启动消费者对于该主题进行消费

# 创建主题
./bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic topic_1 --partitions 1 --replication-factor 1
# 查看主题
./bin/kafka-topics.sh --list --zookeeper localhost:2181
topic_1
-pom- 依赖引入

提前查看自己的broker是哪个版本的:

image.png

<dependencies>
    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <!-- 高版本兼容低版本,我们使用和broker一致的版本 -->
        <version>2.7.0</version>
    </dependency>
</dependencies>
-P1- 生产者A
public class MyProducerA {
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        
        Map<String, Object> configs = new HashMap<>();
        // 1. 指定初始连接用到的broker地址
        configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "117.50.40.96:9092");
        // 2. 指定key的序列化类
        configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        // 3. 指定value的序列化类
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 4. 准备生产者
        KafkaProducer<Integer, String> producer = new KafkaProducer<>(configs);
        // 5. 设置用户自定义的消息头字段
        List<Header> headers = new ArrayList<>();
        headers.add(new RecordHeader("biz.name", "producer.demo".getBytes()));
        // 6. 准备好预发送的消息(记录)
        ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic_1", 0, 0, "Hello Kafka!");
    
        /**
         * 消息的同步确认
         */
        /*Future<RecordMetadata> future = producer.send(producerRecord);
        RecordMetadata metadata = future.get();
        System.out.println("消息的主题:" + metadata.topic());
        System.out.println("消息的分区号:" + metadata.partition());
        System.out.println("消息的偏移量:" + metadata.offset());*/
    
        /**
         * 消息的确认
         */
        producer.send(producerRecord, (metadata, exception) -> {
            if (exception == null) {
                System.out.println("消息的主题:" + metadata.topic());
                System.out.println("消息的分区号:" + metadata.partition());
                System.out.println("消息的偏移量:" + metadata.offset());
            } else {
                System.out.println("异常消息:" + exception.getMessage());
            }
        });
        
        // 6. 关闭生产者
        producer.close();
    }

}

image.png

!执行过程如果遇到 :linux服务器上使用脚本可以实现收发,但是执行外部程序API调用Proeucer发送信息失败(超时),请参考解决方式

-P2- 生产者B(连发100次)
public class MyProducerB {
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        
        HashMap<String, Object> configs = new HashMap<>();
        // 1. 指定初始连接用到的broker地址
        configs.put("bootstrap.servers", "117.50.40.96:9092");
        // 2. 指定key的序列化类
        configs.put("key.serializer", IntegerSerializer.class);
        // 3. 指定value的序列化类
        configs.put("value.serializer", StringSerializer.class);
        // 4. 准备生产者
        KafkaProducer<Integer, String> producer = new KafkaProducer<>(configs);
        // 5. 设置用户自定义的消息头字段
        List<Header> headers = new ArrayList<>();
        headers.add(new RecordHeader("biz.name", "producer.demo".getBytes()));
        // 6. 准备好预发送的消息(记录)
        for (int i = 0; i < 100; i++) {
            ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic_1", 0, i, "Hello Kafka!" + i, headers);
            
            /**
             * 消息的同步确认
             */
        /*Future<RecordMetadata> future = producer.send(record);
        RecordMetadata metadata = future.get();
        System.out.println("消息的主题:" + metadata.topic());
        System.out.println("消息的分区号:" + metadata.partition());
        System.out.println("消息的偏移量:" + metadata.offset());*/
            
            /**
             * 消息的确认
             */
            producer.send(producerRecord, (metadata, exception) -> {
                if (exception == null) {
                    System.out.println("消息的主题:" + metadata.topic());
                    System.out.println("消息的分区号:" + metadata.partition());
                    System.out.println("消息的偏移量:" + metadata.offset());
                } else {
                    System.out.println("异常消息:" + exception.getMessage());
                }
            });
        }
        
        // 6. 关闭生产者
        producer.close();
    }
    
}

image.png

-C1- 消费者(从最早的消息开始拉取)
public class MyConsumer {
    
    public static void main(String[] args) {
    
        HashMap<String, Object> configs = new HashMap<>();
        // 1. 指定初始连接用到的broker地址
        configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "117.50.40.96:9092");
        // 2. 配置key的反序列化器
        configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
        // 3. 配置value的反序列化器
        configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        // 4. 配置消费组ID
        configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_demo1");
        // 5. 如果找不到当前消费者的有效偏移量,则自动重置到最开始
        // (latest:直接重置到消息偏移量的最后一个 / earliest:从头开始消费)
        configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        // 6. 准备好消费者
        KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(configs);
        // 7. 先订阅,再消费
        consumer.subscribe(Arrays.asList("topic_1"));
        /* --> 批量从主题的分区拉取消息 */
        ConsumerRecords<Integer, String> consumerRecords = consumer.poll(3_000);// 3秒拉取一次
        /* --> 遍历该次从主题拉取的批量消息 */
        consumerRecords.forEach(record -> System.out.println(record.topic() + "\t"
                + record.partition() + "\t"
                + record.offset() + "\t"
                + record.key() + "\t"
                + record.value()));
        // 8. 关闭消费者
        consumer.close();
    }
    
}

打印如下:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" "-javaagent:E:\IDE\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=1107:E:\IDE\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;C:\Archie\archieself-study\java-kafka\target\classes;C:\work\mvnRepository\org\apache\kafka\kafka-clients\1.0.2\kafka-clients-1.0.2.jar;C:\work\mvnRepository\org\lz4\lz4-java\1.4\lz4-java-1.4.jar;C:\work\mvnRepository\org\xerial\snappy\snappy-java\1.1.4\snappy-java-1.1.4.jar;C:\work\mvnRepository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" com.archie.consumer.MyConsumerA
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
topic_1	0	0	null	hello 1
topic_1	0	1	null	hello
topic_1	0	2	0	Hello Kafka!0
topic_1	0	3	0	Hello Kafka!1
topic_1	0	4	0	Hello Kafka!2
topic_1	0	5	99	Hello Kafka!3
topic_1	0	6	0	Hello Kafka!4
......
topic_1	0	103	97	Hello Kafka!97
topic_1	0	104	98	Hello Kafka!98
topic_1	0	105	99	Hello Kafka!99

Process finished with exit code 0

「SpringBoot Kafka」

-pom- 依赖声明
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springboot-kafka</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
-properties- spring初始化配置
spring.application.name=springboot-kafka
server.port=8080

# kafka的配置
spring.kafka.bootstrap-servers=117.50.40.96:9092

# --- producer配置 ---
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.IntegerSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 生产者每个批次最多放多少条记录
spring.kafka.producer.batch-size=16384
# 生产者端总可用发送缓冲区大小,此处设置为32MB
spring.kafka.producer.buffer-memory=33554432

# --- consumer配置 ---
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.group-id=springboot-consumer
# 如果在kafka中找不到当前消费者的偏移量,则直接将偏移量重置为最早的
spring.kafka.consumer.auto-offset-reset=earliest
# 消费者的偏移量是自动提交还是手动提交,此处自动提交偏移量
spring.kafka.consumer.enable-auto-commit=true
# 消费者偏移量自动提交的时间间隔
spring.kafka.consumer.auto-commit-interval=1000
-SyncController- 同步控制器
@RestController
public class KafkaSyncProducerController {
    
    @Autowired
    private KafkaTemplate<Integer, String> template;
    
    @RequestMapping("send/sync/{message}")
    public String send(@PathVariable String message) {
        
        final ListenableFuture<SendResult<Integer, String>> future = template.send("topic-spring", 0, 0, message);
        // 同步发送消息
        try {
            final SendResult<Integer, String> sendResult = future.get();
            final RecordMetadata metadata = sendResult.getRecordMetadata();
            
            System.out.println(metadata.topic() + "\t" + metadata.partition() + "\t" + metadata.offset());
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        
        return "success";
    }
    
}
-AsyncController- 异步控制器
@RestController
public class KafkaAsyncProducerController {
    
    @Autowired
    private KafkaTemplate<Integer, String> template;
    
    
    @RequestMapping("send/async/{message}")
    public String send(@PathVariable String message) {
        
        final ListenableFuture<SendResult<Integer, String>> future = this.template.send("topic-spring", 0, 1, message);
        
        // 设置回调函数,异步等待broker端的返回结果
        future.addCallback(new ListenableFutureCallback<SendResult<Integer, String>>() {
            @Override
            public void onFailure(Throwable throwable) {
                System.out.println("发送消息失败:" + throwable.getMessage());
            }
            
            @Override
            public void onSuccess(SendResult<Integer, String> result) {
                final RecordMetadata metadata = result.getRecordMetadata();
                
                System.out.println("发送消息成功:" + metadata.topic() + "\t" + metadata.partition() + "\t" + metadata.offset());
            }
        });
        
        return "success";
    }
    
}
-Listener- 客户端消费者(监听器)
@Component
public class MyConsumer {
    
    @KafkaListener(topics = "topic-spring")
    public void onMessage(ConsumerRecord<Integer, String> record) {
        System.out.println("消费者收到的消息:"
                + record.topic() + "\t"
                + record.partition() + "\t"
                + record.offset() + "\t"
                + record.key() + "\t"
                + record.value());
    }

}
-App- 启动入口
@SpringBootApplication
public class SpringbootKafkaApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringbootKafkaApplication.class, args);
    }
    
}

现在就可以启动主入口试试了!
启动前无须担心自己的Kafka服务上没有topic-spring而导致服务启动失败,因为Spring内部整合了 KafkaAdmin,启动时它如果识别到不存在对应主题,会主动创建出来,如下图:

image.png

同步访问的测试结果:↓

image.png

异步访问的测试结果:↓

image.png


当然,作为SpringBoot项目整合的任何技术,都允许我们自主扩展相关配置功能,并在最大的程度上提供了相关API,如下所述:

  1. 想要自定义式的创建主题(自定义名称和分区大小)
    @Configuration
    public class KafkaConfig {
        @Bean
        public NewTopic topic1() {
            return new NewTopic("my-topic-01", 3, (short) 1);
        }
    
        @Bean
        public NewTopic topic2() {
            return new NewTopic("my-topic-02", 5, (short) 1);
        }
    }
    
    启动后image.png
  2. 想要覆盖各类初始化配置image.png

part 4 - 服务端参数配置

「zookeeper.connect」

该参数用于配置Kafka要连接的Zookeeper/集群的地址。

它的值是一个字符串,使用逗号分隔Zookeeper的多个地址。
Zookeeper的单个地址是 host:port形式的,可以在最后添加Kafka在Zookeeper中的根节点路径。

举例

zookeeper.connect=node2:2181,node3:2181,node4:2181/myKafka...

「listeners」

用于指定当前Broker向外发布服务的地址和端口。
advertised.listeners 配合,用于做内外网隔离。

举例

# 监听名称和安全协议的映射配置(每个监听器的名称只能在map中出现一次)
listener.security.protocol.map=INTEGER:PLAINTEXT,EXTERNAL:PLAINTEXT
# 用于配置broker监听的URI以及监听器名称列表,使用逗号隔开多个URI及监听器名称
listeners=INTERNAL://192.168.100.101:9092,EXTERNAL://192.168.100.130:9093
# 用于配置broker之间通信使用的监听器名称,该名称必须在advertised.listeners列表中
inter.broker.listener.name=EXTERNAL
# 需要将该地址发布到zookeeper供客户端使用,如果客户端使用的地址与listeners配置不同
#(必须是listeners中配置的或配置的一部分)
advertised.listeners=EXTERNAL://192.168.100.130:9092

「broker.id」

用于唯一标记一个Kafka的Broker,它的值是一个任意integer值

当Kafka以分布式集群运行的时候,尤为重要。

最好该值跟该Broker所在的物理主机有关的
如主机名为 host1.lagou.com ,则 broker.id=1 ,如果主机名为 192.168.100.101 ,则 broker.id=101 等等。

「log.dir」

通过该属性的值,指定Kafka在磁盘上保存消息的日志片段的目录。

它是一组用逗号分隔的本地文件系统路径。如果指定了多个路径,那么broker 会根据“最少使用”原则,把同一个分区的日志片段保存到同一个路径下。

broker 会往拥有最少数目分区的路径新增分区,而不是往拥有最小磁盘空间的路径新增分区。

举例

log.dirs=/var/kafka-logs

有关Kafka还有很多高级特性需要结合源码进行分析说明,后续笔者会持续跟进;以下列出大致重点,小伙伴们可以自行深入:

  1. 生产者
    • 数据发送流程
    • 序列化器(自定义)
    • 分区器
    • 拦截器
  2. 消费者
    • 概念:消费者 + 消费组 + 心跳机制
    • 消息接收的订阅、反序列化(自定义)
    • 消息的位移提交、位移管理
    • 再均衡
    • 消费者拦截器
  3. 主题
    • 创建、查看、修改、删除
    • 增加分区
    • KafkaAdminClient应用
    • 偏移量管理
  4. 分区
    • 副本机制
    • Leader选举
    • 分区重新分配
    • 自动再均衡
    • 分区分配策略
  5. 物理量存储
    • 概念:LogSegment、切分文件、索引文件切分过程
    • 索引的偏移量、时间戳
    • 日志删除、压缩策略
    • 磁盘的零拷贝、页缓存、顺序写入
  6. 稳定性
    • 事务的幂等性、事务操作
    • broker选举
    • 可靠性保证——失效副本、副本复制
    • 消息重复的场景及解决方案
    • __consumer_offsets
  7. 延时队列
  8. 重试队列
  9. Kafka集群与运维