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结构,后续专门有一个线程来处理消费失败的消息(定时任务)

 

posted @ 2018-04-25 18:44  辣鸡小篮子  阅读(5743)  评论(2编辑  收藏  举报