一、消息中间件介绍

消息中间件的产生,个人认为是解决端对端通信问题,基于tcp/ip协议的长连接的工具,例如websocket已经做到了端对端通信,那么消息中间件的出现要解决哪些端对端问题呢?

  • 消息量积压问题,大数据量高并发下,数据量太大
  • 解决多端对多端问题,同个业务中消息源和消费源现实中有很多个,除了端不同,其它没有差别,所以需要无差别通信,急需要一个中间组件让多个端共享
  • 解决消息安全问题,实际生产中要保证消息有效消费,需要一个组件来管理消息,让消息的得与失稳定可控。
  • 简化上下游应用开发成本,即插即用的模块化思想,做到了上下游应用的松耦合
  • 区别于通信的消息处理能力,可以魔法式的完成消息处理,还想花点心思赚点外快。

具体可以读读JMS规范,大差不差基于此规范开发的消息中间件。

二、消息中间件现在常用的场景

  • 异步处理
  • 应用解耦
  • 流量削峰
  • 日志处理
  • 分布式消息处理
  • 流式计算

三、常用消息中间件对比

特性

activeMQ

rabbitMQ

rocketMQ

kafka

单机吞吐量

万级,比rocketMQ和kafka低一个数量级

同activeMQ

10万级,支撑高吞吐

10万级,高吞吐量,常用于大数据生态

topic数量对吞吐量的影响

可以达到几百/几千的级别,吞吐量会有较小幅度下降,在同等机器下,可以支撑大量的topic

topic从几十到几百时,吞吐量会大幅度下降,在同等机器下,kafka尽量保证topic数量不要过多,如果要支持大规模的topic,需要增加机器数

时效性

ms级

微秒级,延迟最低

ms级

延迟在ms级以内

可用性

高,主从架构实现高可用

同activeMQ

非常高,基于分布式架构

非常高,分布式,一个partition多个副本,少数机器宕机不会丢失数据,高可用

消息可靠性

有较低的概率丢失数据

基本不丢

经过参数优化配置,做到0丢失

同rocketMQ

功能支持

MQ领域功能完备

基于erlang开发,并发能力强,性能极好,延迟很低

MQ功能较为完善,分布式,扩展性好

功能较为简单,支持简单MQ功能,在大数据领域的实时计算和日志采集被大规模使用

社区活跃度





四、kafka介绍

kafka是一个分布式,支持分区(partition),多副本(replica),基于类似zk协调的分布式消息系统。常用于实时处理大数据量场景,比如日志收集、用户活动追踪、运营指标分析报告和报警。是大数据生态中的常客。

1. kafka架构图

kafka集群一般需要一个分布式协调框架,常用zookeeper。kafka分布式体现在partition上,可以配置多个分区副本,但只有leader分区参与读写。controller在kafka集群中参与分区管理,包括分区故障恢复以及集群元数据同步分发。

如何使用kafka插件消费 kafka简单使用_学习

2. 消费流程

生产者指定分区或key来发送消息,发送完会受到ack,但不表示发送成功,具体需要参考ack配置策略。

kafka集群,会将消息提交到leader中的partition,partition维护segement文件,支持消息的顺序读写,并将消息同步到其它副本分区。在读写消息时,支持零拷贝,大大提高读写性能。kafka基于磁盘文件存储,顺序读写和零拷贝保证读写性能大大提高,可以比肩内存存储的读写。

消费者通过消费者组来订阅topic,topic对消费组是多播,对消费组中的各个消费者是单播。单播得益于rebalance机制,每个分区只能绑定消费组内的一个消费者,topic下分区和组内消费者只存在多对一的关系。消费者需要自己维护分区的offset,消费消息后需要提交offset到kafka,保存最后一个消费消息的偏移量。

3. HW和LEO

HW为高水位,取一个partition对应的ISR中最小的LEO(log-end-offset)作为HW,consumer最多只能消费到HW所在的位置。每个副本都有HW,leader和follower各自负责更新自己的HW状态。对于leader新写入的消息,consumer不能立即消费,leader会等消息被ISR中所有replicas同步更新后更新HW,此时消息才能被消费者消费。这样做保证leader所在broker宕机后,该消息仍然可以从新选出来的leader中获取到。

五、kafka集群搭建

1. 环境配置

1.搭建三台虚拟机,节点分别为192.168.47.128、192.168.47.129、192.168.47.130

2.安装好jdk环境

3.下载apache-zookeeper-3.8.3-bin.tar.gz并解压,搭建zk集群并启动

4.下载kafka_2.12-3.6.0版本到三台虚拟机,并添加到系统环境中

2. 修改配置

修改 config/server.properties文件,在三台虚拟机中都需要修改。

如何使用kafka插件消费 kafka简单使用_kafka_02

如何使用kafka插件消费 kafka简单使用_kafka_03

如何使用kafka插件消费 kafka简单使用_学习_04

3. 启动kafka集群

在每台虚拟机上执行 kafka-server-start.sh -daemon ../config/server.properties,启动kafka集群

查看kafka集群是否启动成功,可以连接zk集群 zkCli.sh -server master1:2181,worker1:2182,worker2:2183 查看如下,表示成功:

如何使用kafka插件消费 kafka简单使用_如何使用kafka插件消费_05

六、kafka 客户端配置

1. java客户端配置
1.1 引入依赖

如何使用kafka插件消费 kafka简单使用_kafka_06

1.2 配置生产者和消费者
package com.spring.zkkafka.conf;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

@Configuration
public class ConfigKafka {

    @Bean
    Producer<String, String> kafkaProducer() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.47.128:9092,192.168.47.129:9093,192.168.47.130:9094");
        props.put(ProducerConfig.ACKS_CONFIG, "1"); // ack 确认机制
        props.put(ProducerConfig.RETRIES_CONFIG, 3); // 重试次数
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 3000); // 每次重试之间间隔时间
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 生产者本地缓冲区大小,可以提高发送性能,默认32MB
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 批量发送消息量大小,默认16384,即16KB
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 发送消息的延迟时间,默认为0,有消息就发送,设置10ms发送,但如果batch够了也发送
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置key序列化工具
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置value序列化工具
        return new KafkaProducer<>(props);
    }

    @Bean
    KafkaConsumer<String, String> kafkaConsumer() {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.47.128:9092,192.168.47.129:9093,192.168.47.130:9094");
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "testGroup_2"); // 消费者组
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 改为手动提交,默认自动提交(poll之后就提交offset了)
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // key值反序列化(二进制转成字符串)
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // value值反序列化(二进制转成字符串)
        /*
         当消费主题的是一个新的消费组,或者offset不存在,怎么消费
         latest(默认):只消费自己启动之后发送到主题的消息
         earliest:第一次从头开始消费,之后按照offset记录继续消费,这个区别于consumer.seekToBeginning(每次都从头消费)
         */
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000); // consumer给broker发送的心跳间隔时间毫秒
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000); // kafka如果10s没有收到消费者心跳,则会把消费者剔除消费组,进行rebalance.
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10); // 一次poll能拉的最大消息条数,根据消费能力设定. 默认值500
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000); // 两次poll的时间超过30s,kafka认为消费能力低,剔除消费组,触发rebalance
        return new KafkaConsumer<>(props);
    }
}
1.3 测试用例
package com.spring.zkkafka;

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.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.TopicPartition;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class KafkaTest {
    @Autowired
    private Producer<String, String> kafkaProducer;
    @Autowired
    private KafkaConsumer<String, String> kafkaConsumer;
    private String topic = "testt";

    @Test
    public void sendMessage() throws ExecutionException, InterruptedException {
        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, "my",
                "client-test11");
        RecordMetadata metadata = kafkaProducer.send(producerRecord).get();
        System.out.printf("发送消息成功:message-%s, topic-%s, partition-%s, offset-%d%n", "client-test11", metadata.topic(),
                metadata.partition(), metadata.offset());
        kafkaProducer.close();
    }

    @Test
    public void sendAsyncMessage() throws IOException {
        for (int i = 0; i < 10; i++) {
            String mess = "kkkk" + i;
            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, 1, "myzff",
                    mess);
            kafkaProducer.send(producerRecord, (recordMetadata, e) -> {
                if (e != null) {
                    System.out.printf("消息 %s 发送失败,报异常 %s%n", mess, e.getMessage());
                } else {
                    System.out.printf("发送消息成功:message-%s, topic-%s, partition-%s, offset-%d%n", mess,
                            recordMetadata.topic(), recordMetadata.partition(), recordMetadata.offset());
                }

            });
        }

        System.out.println("主线程 -------------------");

        System.in.read();

    }

    @Test
    public void testConsumer() throws InterruptedException {
        kafkaConsumer.subscribe(Collections.singletonList(topic)); // 内部算法,自动分给分区给消费组中的消费者(range、轮询、sticky)

        // 手动指定分区消费,手动指定分区后,消费者自动分区就会失效(如果不提交offset,不影响subscribe的消费者)
//        kafkaConsumer.assign(Collections.singletonList(new TopicPartition(topic, 1)));
//        kafkaConsumer.seekToBeginning(Collections.singletonList(new TopicPartition(topic, 1))); // 指定分区消费后,可以从头0开始消费,消费者启动后每次都从0开始消费

        // 手动指定分区偏移量消费,消费者自动分区就会失效(如果不提交offset,不影响subscribe的消费offset)
//        kafkaConsumer.assign(Collections.singletonList(new TopicPartition(topic, 1)));
//        kafkaConsumer.seek(new TopicPartition(topic, 1),75); // 指定分区开始消费的offset

        // 手动指定时间开始消费,这里消费topic中所偶分区,消费者自动分区就会失效(如果不提交offset,不影响subscribe的消费offset)
//        List<PartitionInfo> partitions = kafkaConsumer.partitionsFor(topic); // 获取所有分区
//        long millis = System.currentTimeMillis() - 1000 * 60 * 20;
//        Map<TopicPartition, Long> tPTime = new HashMap<>(partitions.size());
//        for (PartitionInfo p : partitions) {
//            tPTime.put(new TopicPartition(p.topic(), p.partition()), millis);
//        }
//        Map<TopicPartition, OffsetAndTimestamp> tPOffsetMap = kafkaConsumer.offsetsForTimes(tPTime);
//        // 这种方式有问题,assign方法使用一次,后面的会覆盖前面指派的分区,所以要指派分区后消费完,再指派
//        for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : tPOffsetMap.entrySet()) {
//            TopicPartition topicPartition = entry.getKey();
//            OffsetAndTimestamp offsetAndTimestamp = entry.getValue();
//            if (topicPartition == null || offsetAndTimestamp == null) continue;
//            kafkaConsumer.assign(Collections.singletonList(topicPartition)); // 手动指定分区偏移量,只能指定一个分区后消费完,再指定另外的分区消费
//            kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
//            // 消费逻辑,一个分区一个分区消费
//        }

        // 长轮训消费
        while (true) {
            ConsumerRecords<String, String> records = kafkaConsumer.poll(Duration.ofMillis(1000)); // poll在1s内是长轮询poll
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("收到消息:topic-%s, partition-%s, offset-%d, key-%s, value-%s%n", record.topic(),
                        record.partition(), record.offset(), record.key(), record.value());
            }
            if (!records.isEmpty()) {
//                TimeUnit.SECONDS.sleep(40); // 这里是模拟 两次poll间隔超过30s,kafka会把该消费者剔除消费者组
                kafkaConsumer.commitSync(); // 阻塞 每批相同分区的offset只需要提交一次(顺序消费,每个分区提交最后消费的消息,即为最大的offset)
                kafkaConsumer.commitAsync((map, e) -> { // 异步 每批相同分区的offset只需要提交一次
                    if (e != null) {
                        System.out.println(e.getMessage());
                    } else {
                        for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : map.entrySet()) {
                            System.out.printf("已提交offset : partition-%s,offset-%s.%n", entry.getKey().partition(),
                                    entry.getValue().offset());
                        }
                    }
                });
            }
            if (records.isEmpty()) {
                TimeUnit.SECONDS.sleep(2);
            }

        }

    }
}
2. springboot 配置
2.1 引入依赖

如何使用kafka插件消费 kafka简单使用_如何使用kafka插件消费_07

2.2 参数配置
server.port=8080
#### ZK ####
curator.retryCount=5
curator.elapsedTimeMs=5000
curator.server=192.168.47.128:2181,192.168.47.129:2182,192.168.47.130:2183
curator.sessionTimeoutMs=600000
curator.connectTimeoutMs=5000
#### kafka ####
### producer ####
spring.kafka.bootstrapServers=192.168.47.128:9092,192.168.47.129:9093,192.168.47.130:9094
spring.kafka.producer.acks=1
spring.kafka.producer.batchSize=16384
spring.kafka.producer.bufferMemory=33554432
spring.kafka.producer.retries=3
spring.kafka.producer.keySerializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.valueSerializer=org.apache.kafka.common.serialization.StringSerializer
### consumer ####
spring.kafka.consumer.groupId=testGroup_2
spring.kafka.consumer.enableAutoCommit=false
spring.kafka.consumer.autoOffsetReset=earliest
spring.kafka.consumer.heartbeatInterval=1000
spring.kafka.consumer.maxPollRecords=5
spring.kafka.consumer.keyDeserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.valueDeserializer=org.apache.kafka.common.serialization.StringDeserializer
### listener ####
## MANUAL_IMMEDIATE(调用ack..方法就提交了,MANUAL(批量提交,消费端本地暂存offset(每个分区最大Offset),poll的数据消费完后,在下次poll时提交(惰性提交))) ##
spring.kafka.listener.ackMode=MANUAL_IMMEDIATE
2.3 客户端消费者配置
package com.spring.zkkafka.conf;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;

@Configuration
@ConditionalOnClass(ConfigKafka.MyConsumer.class)
public class ConfigKafka {

    @Bean
    @ConditionalOnMissingBean(MyConsumer.class)
    MyConsumer myConsumer() {
        return new MyConsumer();
    }

    static class MyConsumer {

        @KafkaListener(topics = "testt", groupId = "testGroup_2")
        void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
            String value = record.value();
            System.out.printf("消费成功:%s.%n", value);
            ack.acknowledge(); // 手动提交
        }
    }
}
2.4 生产者调用
package com.spring.zkkafka;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;

import java.io.IOException;
import java.util.concurrent.ExecutionException;

@SpringBootTest
public class KafkaTest {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    private String topic = "testt";


    @Test
    public void sendMessageSpringBoot() throws ExecutionException, InterruptedException, IOException {
        SendResult<String, String> res = kafkaTemplate.send(topic, 1, "key", "springboot发送消息了").get();
        System.out.printf("发送成功:%s%n", res.getProducerRecord().value());
        System.in.read();
    }
}

七、kafka监听kafka-eagle的搭建

1. 环境准备

下载efak-web-3.0.1-bin.tar.gz到本地任意一台虚拟机,解压缩,添加系统环境配置

2. 修改配置文件

vi conf/system-config.properties

如何使用kafka插件消费 kafka简单使用_如何使用kafka插件消费_08

如何使用kafka插件消费 kafka简单使用_如何使用kafka插件消费_09

3. 启动并访问监控页面

ke.sh start

如何使用kafka插件消费 kafka简单使用_apache_10