• Kafka分布式消息系统


    1.简介

    Kafka是一个分布式消息系统,使用Scala语言进行编写,具有高水平扩展以及高吞吐量特性。

    目前流行的消息队列主要有三种:ActiveMQ、RabbitMQ、Kafka

    ActiveMQ、RabbitMQ均支持AMQP协议,Kafka使用仿AMQP协议,目前Flume、Storm、Spark、Elasticsearch都支持与Kafka进行集成。

    动态扩容:在不需停止服务的前提下动态的增加或减少节点,Kafka的动态扩容是通过zookeeper实现的,zookeeper上保存着kafka的相关状态信息(topic、partition等)

    2.关于AMQP

    AMQP全称 高级消息队列协议,是一个统一消息服务的应用层协议,为面向消息的中间件所设计,基于此协议的客户端与消息中间件可相互传递消息,并不受客户端和中间件的产品以及开发语言不同所限制。

    Producer负责把消息发送给Broker。

    Broker负责接收并存储Producer发送的消息。

    Consumer负责从Broker中消费消息。

    *Broker是消息队列中最小的运行单元,一个Broker的运行就代表着一个Kafka实例。

    3.Kafka的模型

    1.Broker

    Broker中可以包含多个Topic,每个Topic下又包含多个Partition。

    一个Topic(主题)类似于新闻中的体育、娱乐、等分类概念,在实际开发中通常一个业务对应一个Topic。

    一个Topic下由多个Partition组成,每个Partition都是一个First In First Out的队列,用于存放Topic中的消息。

    每个消息在Partition中都有一个offset(偏移量),是消息在分区中的唯一标识。

    每个Consumer都需要维护一份自己的offset,用于记录当前消费的进度,然后保存到Kafka当中(Consumer可以以任意的位置开始进行读取,只需要设置offset即可)

    在一个可配置的时间段内,Kafka集群将保留所有发送的消息,不管这些消息是否被被消费。

    Kafka的分区是提高Kafka性能的关键手段,当Kafka集群的性能不高时,可以试着往topic中添加分区。

    Kafka的分区备份

    Topic下的每个Partition在Kafka集群中都有备份,在逻辑相关的一组Partition中,都有一个作为Leader,其余作为Follower,Leader和Follwer的选举都是随机的,当Follower接收到请求时首先会发送给Leader,由Leader负责消息的读和写并把消息同步给各个Follower,如果Leader所在的节点宕机,Follower中的一台则会自动成为Leader。

     

    比如搭建一个Kafka集群,存在3个节点,同时设置Topic的分区数以及分区的备份数是3,现往Broker1中创建一个New Topic,那么在每个Broker实例中都会存在一个New Topic,同时每个New Topic下都会包含3个Partition,在逻辑相关的一组Partition中,都有一个作为Leader,其余作为Follower。

    2.Producer

    Producer向Broker中指定的Topic发送消息,消息将会根据负载均衡策略进入相应的Partition。

    *Producer向Broker发送消息时,除了指定Topic以及Message以外,还可以指定一个Key,用于Partition的散列,Key相同的消息将会保存到同一个Partition当中。

    3.Consumer组

    Kafka提供了Consumer组的概念,一个Consumer组下可以包含多个Consumer。

    Kafka规定,Topic下的每一个Partition都只能被Consumer组下的唯一一个Consumer进行消费,以确保消费的顺序性,因此Consumer组下的Consumer数量不能超过Partition的数量,否则将会处于空闲状态。


    队列模式

    若所有的Consumer都在同一个Consumer组中下则成为队列模式,Topic中的各个Partition都只能被组中的唯一一个Consumer进行消费,组下的Consumer共同竞争Topic中的Partition。

    广播模式

    若所有的Consumer都不在同一个Consumer组中则成为广播模式,Topic中各个Partition的消息都会广播给所有的Consumer组。

    4.Kafka的应用场景

    1.解耦

    比如存在一个应用A,它需要接收请求并且对请求进行处理,那么此时可以利用Kafka进行解耦,应用A只负责接收请求,同时将请求中的数据封装为Message,然后保存在Kafka的Topic当中,后续由应用B来进行消费,以达到解耦的目的。

    2.削流

    如果有大量并发的写请求直接去到数据库,那么将会导致数据库的奔溃(618/11.11),此时可以利用Kafka进行削流,将所有的写请求封装成Message,然后保存到Kafka的Topic当中,后续再通过Consumer以一定的速率进行消费(队列模式)

    3.通知

    首先各个被通知者都消费Kafka中指定的一个Topic,当需要进行通知时,往Kafka中指定的Topic发送消息,那么此时所有的被通知者就能够收到通知(广播模式)

    5.关于Kafka中消息的顺序性

    Kafka只能保证在同一个Partition中的消息是有序的,因为Kafka规定了Topic下的每个Partation都只能被Consumer组中的唯一一个Consumer进行消费,同时Kafka也没有实现在一个Consumer中使用多线程进行消费,Partition之间的消息是不能够保证有序的

    Kafka是通过指定消息的Key来保证消息的顺序性的,因为Kafka会对Key进行散列,Key相同的消息将会保存在同一个Partation当中,因此当消息需要有序时可以通过指定相同的Key放入到同一个Partation当中。

    如果需要在一个Consumer中使用多线程去消费Partation中的消息,那么需要自己实现,可以把Consumer拉取过来的消息全部提交到线程池中进行处理,那么消费的速度完全取决于线程池中线程的个数。

    6.Kafka的使用

    1.安装

    由于Kafka使用scala语言编写,scale语言运行在JVM中,因此需要先安装JDK并且配置好环境变量。

    由于Kafka中的状态信息都保存在zk上,虽然Kafka自带zk,但一般是使用外置的zk集群,因此需要先安装zk服务并且配置好zk集群关系。

    从Kafka官网中下载安装包并进行解压。

    2.配置文件

    config目录是Kafka配置文件的存放目录

    Broker端配置

    Kafka在启动时需要连接ZK,共同连接同一个ZK集群的Kafka自动构成集群关系(broker.id在集群中不能重复)

    Kafka中的消息是保存到磁盘的(log.dirs目录下),每个Topic下的Partition都对应log.dirs中的一个目录(topic-num),每个Partition目录下都有log文件用于存放消息,当Partition有新的消息时会往该log文件后进行追加。

    如果创建的Topic其备份数大于1 ,那么在Kafka集群备份数个Broker中也会创建此Topic,因此在其log.dirs目录下也会存在该topic的目录。

    Producer端配置

    Consumer端配置

    3.启动Kafka

    1.启动zk集群

    #启动zk节点
    zkServer.sh start
    #查看节点角色
    zkServer.sh status 

    2.启动Kafka集群

    ./kafka-server-start.sh -daemon ../config/server.properties

    3.创建Topic

    1.创建名为chat的Topic,Topic的分区数以及备份数都为3。

    ./kafka-topic.sh --create --zookeeper 192.168.1.80:2181,192.168.1.80:2182,192.168.1.80:2183 --partitions 3 --replication-factor 3 --topic "chat"

    创建Topic时需指定ZK服务地址,ZK中保存了Topic的分区数以及备份数(元数据),Kafka集群中的其他节点再从ZK服务中获取Topic的元数据来创建Topic。

    2.查看各个broker中的log.dirs,可见在该目录下都生成了chat-0、chat-1、chat2分别表示chat Topic的第一个、第二个、第三个partation,每个partation中都有.log文件用于存放Partation中的消息。

    3.查看Kafka集群中chat Topic下各个Partation的状态

    ./kafka-topic.sh --describe --zookeeper 192.168.1.80:2181 --topic chat

    Leader:充当Leader的Broker节点(broker.id)

    Replicas:存在备份的Broker节点(broker.id,不管节点是否存活)

    Isr:存在备份的同时存活的Broker节点。

    4.Producer发送消息

    往Kafka集群中的chat主题发送消息

    ./kafka-console-producer.sh --broker-list 192.168.1.80:9092,192.168.1.81:9092,192.168.1.82:9092 --topic chat

    如果不指定Key,那么消息将会根据负载均衡策略进入相应的Partation。

    5.Consumer消费消息

    启动Consumer

    ./kafka-console-consumer.sh --bootstrap-server 192.168.1.80:9092,192.168.1.81:9092,192.168.1.82:9092 --topic chat --from-beginning

    由于使用脚本文件启动Consumer并没有指定使用的配置文件,所以三个Consumer都不在同一个Consumer组中,因此三个Consumer都能够消费chat主题下各个Partation中的消息。


    启动Consumer并指定配置文件

    ./kafka-console-consumer.sh --bootstrap-server 192.168.1.80:9092,192.168.1.81:9092,192.168.1.82:9092 --topic chat --from-beginning --consumer.config ../config/consumer.properties

    启动了3个Consumer并指定使用的配置文件,默认的group.id为test-consumer-group,因此这3个Consumer都在同一个Consumer组下,Topic中各个Partation仅能被组下的唯一一个Consumer进行消费。

    在启动第一个Consumer时,Consumer组下只有一个Consumer,因此消息都会被此Consumer进行消费,当往Consumer组中添加新的Consumer时,将会重新分配拥有Partation的权利。

    7.JAVA中使用Kafka

    1.导入依赖

    <dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka_2.12</artifactId>
      <version>0.11.0.1</version>
    </dependency>

    2.创建Topic

    ZkUtils zkUtils = ZkUtils.apply("192.168.1.80:2181,192.168.1.81:2181,192.168.1.82:2181", 30000, 30000, JaasUtils.isZkSecurityEnabled());
    // 创建一个名为chat的主题其包含2个分区,备份数是3
    AdminUtils.createTopic(zkUtils, "chat", 2, 3, new Properties(), RackAwareMode.Enforced$.MODULE$);
    zkUtils.close();

    3.Producer发送消息

    //创建Properties对象用于封装配置项
    Properties props = new Properties();
    props.put("bootstrap.servers", "192.168.1.80:9092,192.168.1.81:9092,192.168.1.82:9092"); 
    props.put("acks", "all"); 
    props.put("retries", 0); 
    props.put("batch.size", 16384); 
    props.put("linger.ms", 1); 
    props.put("buffer.memory", 33554432); 
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); 
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    
    Producer<String, String> producer = new KafkaProducer<>(props);
    //创建消息实体ProducerRecord,并指定消息上传的topic、消息的Key、消息的Value
    ProducerRecord<String,String> record = new ProducerRecord<String,String>("topic","key","value");
    //发送消息
    producer.send(record);
    //关闭连接
    producer.close();

    KafkaProducer是线程安全的,在线程之间可以共享单个生产者实例。

    send()方法是异步的,一旦消息被保存在待发送缓冲区时此方法就立即返回,其返回Future<RecordMetadata>实例,当调用该实例的get()方法时将会阻塞直到服务器对请求进行应答(阻塞时长跟acks配置项有关),当服务器处理异常时将抛出异常。

    消息由Key和Value组成,Key相同的Message将会保存在同一个Partation当中(根据Key进行散列)

    4.Consumer消费消息

    //创建Properties对象用于封装配置项
    Properties props = new Properties();
    props.put("bootstrap.servers", "192.168.1.80:9092,192.168.1.81:9092,192.168.1.82:9092");
    props.put("group.id", "consumerA");
    //自动提交Consumer的偏移量给Kafka服务
    props.put("enable.auto.commit", "true");
    props.put("auto.commit.interval.ms", "1000");
    props.put("auto.offset.reset", "earliest");
    props.put("session.timeout.ms", "30000");
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
    
    Consumer<String, String> consumer = new KafkaConsumer<>(props);
    //订阅主题,一个消费者实例可以订阅多个主题
    consumer.subscribe(Arrays.asList("chat", "hello"));
    //接收数据,消息存放在ConsumerRecords消息集合中
    ConsumerRecords<String, String> records = consumer.poll(1000*5);
    //遍历消费端消息集合获取ConsumerRecord消费端消息实体,一个消费端消息实体包含偏移量、消息Key值、消息Value值
    for (ConsumerRecord<String, String> record : records){
    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
    }

    poll(long blockTime)方法用于接收topic中的消息,当没有消息时将会等待blockTime的时间 (单位:毫秒),执行结果需结合auto.offset.reset配置项。

    使用commitSync()方法可以手动同步消费者的偏移量给Kafka 。

    使用seek(TopicPartition , long)方法手动设置消费者的偏移量。

    8.Spring Kafka

    1.导入依赖

    <dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka-clients</artifactId>
      <version>0.10.2.0</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.kafka</groupId>
      <artifactId>spring-kafka</artifactId>
      <version>1.2.0.RELEASE</version>
    </dependency>

    2.创建Kafka Producer配置类

    @Configuration
    @EnableKafka
    public class KafkaProducerConfiguration {
    
        /**
         * Producer配置
         */
        private Map<String, Object> senderProps() {
            Map<String, Object> props = new HashMap<>();
            props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
            props.put(ProducerConfig.RETRIES_CONFIG, 1);
            props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
            props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
            props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 1024000);
            props.put(ProducerConfig.ACKS_CONFIG, "1");
            props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
            props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
            return props;
        }
    
        /**
         * Producer工厂
         */
        @Bean
        public ProducerFactory<String, String> producerFactory() {
            return new DefaultKafkaProducerFactory<>(senderProps());
        }
    
        /**
         * KafkaTemplate
         */
        @Bean
        public KafkaTemplate<String, String> kafkaTemplate() {
            return new KafkaTemplate<>(producerFactory());
        }
    
    }

    3.创建Kafka Consumer配置类

    @Configuration
    @EnableKafka
    public class KafkaConsumerConfiguration {
    
        /**
         * Consumer配置
         */
        private Map<String, Object> consumerProps() {
            Map<String, Object> props = new HashMap<>();
            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
            props.put(ConsumerConfig.GROUP_ID_CONFIG, "GroupA");
            props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
            props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100");
            props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000");
            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
            return props;
        }
    
        /**
         * Consumer工厂
         */
        @Bean
        public ConsumerFactory<String, String> consumerFactory() {
            return new DefaultKafkaConsumerFactory<>(consumerProps());
        }
    
        /**
         * Kafka监听器
         */
        @Bean
        public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
            ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
            factory.setConsumerFactory(consumerFactory());
            return factory;
        }
    
    }

    4.Producer发送消息 

    @Component
    public class Producer {
    
        @Autowired
        private KafkaTemplate<String,String> kafkaTemplate;
    
        public void send() throws Exception {
            kafkaTemplate.send("topic", "key","value");
        }
    }

    5.Consumer消费消息

    @Component
    public class Consumer {
    
        @KafkaListener(groupId="GroupA",topics = "topic",concurrency="3")
        public void consumer(String value) {
                 System.out.println(String.format("消费消息,value:%s", value));
        }
    }

    @KafkaListener注解中的concurrency数量为开启的Consumer数量,也就是在Consumer组下存在多少个Consumer。

    9.使用Kafka要解决的问题

    1.Consumer端消息丢失

    当Consumer消费消息后,自动提交了offset,如果后续程序处理出错,那么消息将会丢失,此时可以通过手动提交offset的方式进行解决。

    2.重复消费

    当Consumer读取消息后,程序也成功进行处理,如果手动提交offset时出错,则会导致重复消费,同时如果Producer重复发送消息也会导致重复消费,当发生重复消费时只需要保证幂等性即可(多次执行的结果保持一致)

    如何保证幂等性

    1.每次保存时,都先从数据库查一次,如果数据已存在则表示重复消费(针对并发不大同时实时性不高的场景)

    2.数据库表添加唯一约束,当重复消费时将会插入失败(针对没有分库分表的场景)

    3.添加消息表并在字段上加上唯一约束,每当消费完一条消息就往表里插入一条记录,当重复消息时将会插入失败(针对有分库分表的场景)

    4.每次保存时,都先从Redis中查一次,如果Redis中已存在则表示重复消费(不太靠谱,除非过期时间设置很久)

    *如果并发很高,需要借助Redis或者Zookeeper通过分布式锁来进行控制(比如Producer发送了2条相同的消息,如果没有指定Key,假设这两条消息分别坐落在不同的Partation当中,然后刚好被两个Consumer线程同时消费,此时就存在同步问题,需要通过分布式锁来进行控制)

    3.Broker端消息丢失

    当Partition中的Leader重选举时,也就是说Leader挂了,那么有可能导致消息未来得及同步给其他的Follower,最终导致消息丢失,此时只需要在Producer中设置acks等于all,那么Producer必须等待Leader将消息同步给所有的Follower后再进行返回。

    4.消息堆积

    消息堆积主要是因为Consumer消费的速度太慢了,可以通过为Topic新增Partation同时新增Consumer来进行消费从而提高消费的速度,或者将Consumer拉取的数据放入到线程池中进行处理,那么消费的速度就取决于线程池中线程的数量(要注意内存溢出),但是就不能通过Kafka监控工具中来判断是否存在消息堆积的现象了

     

    5.消费失败

    当消费失败时,可以将消息放入到一个队列当中,比如使用Redis的list结构,后续专门有一个线程来处理消费失败的消息(定时任务)

  • 相关阅读:
    mime.types:强制下载 application/force-download
    使用FWTools来导入shp数据到mysql
    测试反应能力的小代码!(娱乐)
    用Vue.js模仿一个百度的页面!(后台写死)
    在Vue.js使用配置(SSH框架的附带使用下){其余代码同上}
    Vue.js理论!
    动态从数据库获取数据(Vue.js)【数据可变】
    发布网站
    Vue.js 的开始!
    二叉树
  • 原文地址:https://www.cnblogs.com/funyoung/p/8855714.html
Copyright © 2020-2023  润新知