Kafka学习记录

基础环境搭建(参照官网)好了之后直接搞,kafka的配置文件server.properties详细说明戳我。下面的Demo都是基于win10搭建出来的基础环境。

1. 基本概念

 Kafka是一个分布式流处理平台,注意它是使用流来处理数据的(核心特征之一),最终是为了能够进行实时的流处理。它是一个集群,通过 topic 对其中存储的数据进行分类,每条记录包含一个 key、一个value、一个时间戳。官网上总结Kafka有4个核心API:

  1. The Producer API:允许一个应用程序发布一串流式的数据到一个或者多个Kafka topic。
  2. The Consumer API:允许一个应用程序订阅一个或多个 topic ,并且对发布给他们的流式数据进行处理。
  3. The Streams API:允许一个应用程序作为一个流处理器,消费一个或者多个topic产生的输入流,然后生产一个输出流到一个或多个topic中去,在输入输出流中进行有效的转换。
  4. The Connector API 允许构建并运行可重用的生产者或者消费者,将Kafka topics连接到已存在的应用程序或者数据系统。比如,连接到一个关系型数据库,捕捉表(table)的所有变更内容。

 kafka中topic是一个非常重要的概念,本身的目的也是提供遗传流式的记录(即topic)。一些重要的基本概念如下:

  • Broker:可以理解为各个kafka的节点标识,通常用broker.id来标识;
  • Topic:数据主题,是数据记录发布的地方,可以用来区分业务系统,通常一个Topic可以拥有多个消费者来订阅;
  • Producer:生产者,将数据发布到指定的topic的某个 partition 中,这个行为可以使用循环或某些语义函数实现负载均衡;
  • Consumer:消费者,由消费者组名称进行标识,发布到topic中的记录将会分配给订阅该topic的消费者;
  • Consumer group:消费者组,由多个消费者实例组成(可以分布在多个进程或机器上),对应逻辑上的一个订阅者;
  • Partition:kafka 对每个 topic 都会维护一个分区,每个分区都是有序的记录集(顺序不可变),且日志会不断追加到结构化的 commit log 文件中,分区中每个记录都会分配一个id(即 offset)来标识上述的顺序,topic的分区可以是若干个副本,当节点出现问题时,能自动进行故障转移,保证数据的可用性;
  • Replication:这里的副本即指partition的副本,创建副本的单位是topic的partition,通常每个分区都有一个leader和0或多个followers,副本数目=leader数+follower数,所有的follower节点都会同步leader节点的日志(故障转移的一种机制)。

 日志中的partition有一下2个作用:

  • 当日志大小超过了单台服务器的限制,允许日志进行扩展。每个单独的分区都必须受限于主机的文件限制,不过一个主题可能有多个分区,因此可以处理无限量的数据;
  • 可以作为并行的单元集;

 kafka 有一些质量保证:生产者发送到某个topic的消息会按照发送的顺序处理,即先发的消息记录的偏移(offset)必定比后发的消息的偏移小,且在日志中较早出现;消费者实例按照日志中的顺序查看;如果一个主题有N个副本,那kafka最多容忍N-1个服务故障,保证数据不会丢失。在kafka中,各个节点是否“存活”,主要取决于下面2个标准:

  1. 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接;
  2. 如果节点是个 follower ,它必须能及时的同步 leader 的写操作,并且延时不能太久;

只有满足上述2个标准的节点才是kafka认可的in sync状态。Leader会追踪所有 “in sync” 的节点。如果有节点挂掉了, 或是写超时, 或是心跳超时, leader 就会把它从同步副本列表中移除。 同步超时和写超时的时间由replica.lag.time.max.ms配置确定

2. 基本命令

 Talk is cheap. Show me the code,实操才是最好的熟悉新事物的方式,下面是一些简单的操作:

# 1. --create 指定当前对于topic的操作行为
# --topic 指定名为 test 的 topic
# --zookeeper 指定 zookeeper 地址为 localhost:2181
# --replication-factor 指定 topic 的副本数量为3
# --partitions 指定分区 必选
.\kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

# 2. 查看topic列表
.\kafka-topics.bat --list --zookeeper localhost:2181

# 3. 查看某个topic(这里是my-replicated-topic)的详细信息
.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic my-replicated-topic
# 输出如下, 其中第一行标识所有分区的摘要,后续的第二行第三行..标识每个分区的信息,my-replicated-topic 只有一个分区,故只有一行
Topic:my-replicated-topic       PartitionCount:1        ReplicationFactor:3     Configs:
        Topic: my-replicated-topic      Partition: 0    Leader: 0       Replicas: 0,1,2 Isr: 0,1,2

# 4. 修改 topic 的 partitions 为2
.\kafka-topics.bat --zookeeper localhost:2181 --alter --topic test --partitions 2
# 再次查看 test 的详情:.\kafka-topics.bat --describe --zookeeper localhost:2181 --topic test 输出如下
Topic:test      PartitionCount:2        ReplicationFactor:1     Configs:
        Topic: test     Partition: 0    Leader: 0       Replicas: 0     Isr: 0
        Topic: test     Partition: 1    Leader: 1       Replicas: 1     Isr: 1
# 4.1 增加配置项:对 topic test 添加某个配置项 x=y (key-value的形式)
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --add-config x=y
# 4.2 删除配置项
.\kafka-configs.bat --zookeeper localhost:2181 --entity-type topics --entity-name test --alter --delete-config x=y

# 5. 向集群发送消息,运行生产者,选择某个主题,这里选择 test 的topic
.\kafka-console-producer.bat --broker-list localhost:9092 --topic test

# 6. 从集群消费消息,运行消费者,选择和生产者一致的topic进行连接,当然可以是集群中的任意kafka节点,比如我这里是 9092~9094 3个节点
.\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning

# 7. 删除topic,如果配置文件中没有配置 delete.topic.enable=true 删除操作时并不会真正删除,只是打上一个标记
.\kafka-topics.bat --delete --zookeeper localhost:2181 --topic my-replicated-topic

上述 --describe 行为输出结果详细参数说明如下:

  • “leader”是负责给定分区所有读写操作的节点。每个节点都是随机选择的部分分区的领导者。
  • “replicas”是复制分区日志的节点列表,不管这些节点是leader还是仅仅活着。
  • “isr”是一组“同步”replicas,是replicas列表的子集,它活着并被指到leader。

通过在生产者balabala一些,他就会将你说的这些废话(默认一行废话为一个message)发送到kafka的集群,集群内的所有消费者都会接受你的废话,如下:

生产者发送消息

 上述的基本命令都是topic级别的,很多参数都可以通过 --config 的方式指定,比如上述的--create--zookeeper--replication-factor,当然还有很多参数可以配置,比如:--config max.message.bytes=64000--config flush.messages=1指定最大消息的大小和刷新频率。topic的创建行为可以像上述那样手动创建,也可以在数据首次发布到某个不存在的topic时自动创建,对于一些参数详细说明如下:

  • --replication-factor:控制有多少服务器将复制每个写入的消息。如果设置了3个复制因子,那么只能最多2个相关的服务器能出问题,否则将无法访问数据(N-1)。建议您使用2或3个复制因子,以便在不中断数据消费的情况下透明的调整集群;
  • --partitions:控制 topic 将被分片到多少个日志里。partitions 会产生几个影响-首先,每个分区只属于一个台服务器,所以如果有20个分区,那么全部数据(包含读写负载)将由不超过20个服务器(不包含副本)处理,最后 partitions 还会影响 consumer 的最大并行度。

每个分区日志都放在自己的Kafka日志目录下的文件夹中。这些文件夹的名称的形式为topic名-分区ID,注意topic的名称不能超过249个字符(为了留下了足够的空间以显示短划线和可能的5位长的分区ID)。

3. SpringBoot集成kafka

 基于上述进行简单的集成,基本环境如下:

  • SpringBoot:2.2.1.RELEASE;
  • spring-kafka:2.3.4.RELEASE;
  • kafka:本地 win10 搭建的 kafka-2.3.1 3节点集群;
  • zookeeper:本地 win 10 搭建的 zookeeper-3.5.6;

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.glodon</groupId>
    <artifactId>spring_kafka</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring_kafka</name>
    <packaging>jar</packaging>
    <description>Spring project for Kafka</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-kafka.version>2.3.4.RELEASE</spring-kafka.version>
    </properties>

    <dependencies>
        <!--spring-boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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>

        <!--fastJson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

        <!-- kafka-->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>${spring-kafka.version}</version>
        </dependency>
    </dependencies>
</project>

相关参数配置:

# kafka
spring.kafka.bootstrap-servers=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094
spring.kafka.listener.concurrency=3
# 生产者相关配置
spring.kafka.producer.retries=1
spring.kafka.producer.batch-size=16384
spring.kafka.producer.buffer-memory=33554432
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.group-id=consumer-default
spring.kafka.consumer.auto-commit-interval=100
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer

生产者和消费者实现类:

/**
 * @author liuwg-a
 * @date 2019/1/4 15:49
 * @description 实例化配置,主要用于创建2个测试的topic
 */
@Configuration
public class Config {
    /**
     * 创建一个测试的 topic
     *
     * @return NewTopic
     */
    @Bean("noBlockingSpringKafkaTopic")
    public NewTopic createNoBlockingSpringKafkaTopic() {
        return TopicBuilder.name("spring-kafka-no-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
                                                                                                    "zstd").build();
    }

    @Bean("blockingSpringKafkaTopic")
    public NewTopic createBlockingSpringKafkaTopic() {
        return TopicBuilder.name("spring-kafka-blocking-test").partitions(10).replicas(3).config(TopicConfig.COMPRESSION_TYPE_CONFIG,
                                                                                                 "zstd").build();
    }

}

/**
 * @author liuwg-a
 * @date 2019/12/10 19:01
 * @description kafka 消息类
 */
public class Message {

    private Long          id;
    private String        msg;
    private LocalDateTime time;

    public Message() {
    }

    public Message(Long id, String msg, LocalDateTime time) {
        this.id = id;
        this.msg = msg;
        this.time = time;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public LocalDateTime getTime() {
        return time;
    }

    public void setTime(LocalDateTime time) {
        this.time = time;
    }
}

/**
 * 生产者
 * @author liuwg-a
 * @date 2019/12/10 19:04
 * @description
 */
@Service("producer")
public class KafkaProducer {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    @Autowired
    private NewTopic                      noBlockingSpringKafkaTopic;
    @Autowired
    private NewTopic                      blockingSpringKafkaTopic;

    private static final Logger           logger = LoggerFactory.getLogger(KafkaProducer.class);

    /**
     * 1. 异步非阻塞发送消息
     *
     * @param message message
     */
    public void noBlockingSend(Message message) {
        if (message == null) {
            return;
        }

        ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(noBlockingSpringKafkaTopic.name(),
                                                                                 JSON.toJSONString(message));
        /*
         * 消息发送成功进行回调
         */
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

            @Override
            public void onFailure(Throwable throwable) {
                // 处理失败的逻辑
                logger.error("kafka producer:send message failed");
            }

            @Override
            public void onSuccess(SendResult<String, String> stringStringSendResult) {
                // 处理成功的逻辑
                logger.info("kafka producer: send message success");
            }
        });
    }

    /**
     * 2. 阻塞式发送消息
     *
     * @param message message
     */
    public void blockingSend(Message message) {
        try {
            kafkaTemplate.send(blockingSpringKafkaTopic.name(), JSON.toJSONString(message)).get(5, TimeUnit.SECONDS);
            // 发送成功立即处理
            logger.info("kafka producer: send message success");
        } catch (Exception e) {
            // 处理失败的逻辑
            logger.error("kafka producer:send message failed");
        }
    }

}

/**
 * 消费者
 * @author liuwg-a
 * @date 2019/12/10 19:06
 * @description
 */
@Service("consumer")
public class KafkaConsumer {

    private static final String NO_BLOCKING_SPRING_KAFKA_TOPIC_STR = "spring-kafka-no-blocking-test";
    private static final String BLOCKING_SPRING_KAFKA_TOPIC_STR    = "spring-kafka-blocking-test";

    private static final Logger logger                             = LoggerFactory.getLogger(KafkaConsumer.class);

    /**
     * 注解 @KafkaListener 会促使 Spring Kafka 自动创建一个消息容器
     */
    @KafkaListener(topics = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = NO_BLOCKING_SPRING_KAFKA_TOPIC_STR)
    public void receiveNoBlocking(ConsumerRecord<String, String> record) {
        Optional<String> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            logger.info("kafka consumer: receive message -> record = {}", record);
            logger.info("kafka consumer: receive message -> message = {}", message);
        }
    }

    @KafkaListener(topics = BLOCKING_SPRING_KAFKA_TOPIC_STR, groupId = BLOCKING_SPRING_KAFKA_TOPIC_STR)
    public void receiveBlocking(ConsumerRecord<String, String> record) {
        Optional<String> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            logger.info("kafka consumer: receive message -> record = {}", record);
            logger.info("kafka consumer: receive message -> message = {}", message);
        }
    }
}

测试类:

/**
 * @author liuwg-a
 * @date 2019/12/10 19:10
 * @description
 */
@SpringBootTest
@ExtendWith(SpringExtension.class)
class KafkaProducerTest {

    @Autowired
    private KafkaProducer kafkaProducer;

    @Test
    void noBlockingSend() {
        for (int i = 0; i < 3; i++) {
            kafkaProducer.noBlockingSend(produceMessage());
        }
    }

    @Test
    void blockingSend() {
        for (int i = 0; i < 3; i++) {
            kafkaProducer.blockingSend(produceMessage());
        }
    }

    /**
    * 模拟生产消息
    */
    private Message produceMessage() {
        return new Message(new Random().nextLong(), UUID.randomUUID().toString(), LocalDateTime.now());
    }
}

启动测试即可,注意这里如果不进行初始化创建的2个topic的java配置,则需要提前手动创建好测试的 topic,否则启动会出现找不到该 topic 的异常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章