Kafka分布式消息系统

1.简介

 

Kafka是一个分布式消息系统,由LinkedIn使用Scala语言编写的,具有高水平扩展和高吞吐量 (每秒处理任务数)

 

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

 

 

*其中ActiveMQ、RabbitMQ均支持AMQP协议,Kafka有其自己的协议 (仿AMQP,并不通用),但目前越来越多的开源分布式处理系统如Flume(日志收集系统)、Storm(实时数据处理系统)、Spark(大数据通用处理平台)、Elasticsearch(全文检索系统)都支持与Kafka的集成。

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

 

Kafka使用场景:

1.网站活动追踪:用户的活动追踪、搜索、浏览、点击率等,将操作信息发布到不同的主题中,可对数据实时监控,统计分析用户行为。

2.日志聚合:作为一种日志聚合的解决方案( 日志聚合的作用在于可以把来自不同服务器上不同应用程序产生的日志聚合起来,存放在单一的服务器上,方便进行搜索和分析)

3.数据集中管理:分布式应用产生的数据统一存放在kafka集群中,集中式管理,供其他程序使用或后续对数据统计分析。

 
*对于不影响主流程的业务,并且系统对延时性有高要求的,都可以直接使用Kafka,将不同的任务类型放入Kafka不同的Topic中,使用多个Consumer去进行消费。

 

 
 

 

2.AMQP

 

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

 

AMQP协议模型

 

 

 

生产者(Producer):往消息队列中发送消息的应用程序

消费者(Consumer): 从消息队列中获取消息的应用程序

AMQP服务器(Broker):用来接收生产者发送的消息并将这些消息路由给服务器中的队列

 

*消息队列以broker为最小的运行单元,一个broker的运行就代表着一个Kafka应用程序实例。

*Kafka客户端支持的语言:C、C++、Erlang、Java、.net、perl、PHP、Python、Ruby、Go、Javascript,可以使用任何一种语言和Kafka服务器进行通信,编写自己的生产者与消费者客户端程序。

 

 

3.Kafka的组件

 

1.核心组件Broker

 

 

*broker中可以包含多个主题,每个主题中又可以包含多个分区。

 

主题(topic)

一个主题类似新闻中的体育、娱乐、教育等分类概念,在实际工程中通常一个业务一个主题。

 

分区(partition)

一个topic中由一到多个分区组成,分区是Kafka结构的最小单元,一个分区就是一个FIFO(First In First Out)的队列,用于存放topic中的消息。

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

 

Kafka的分区模型

 

 

*可见每个分区都是一个先进先出的队列,producer往broker中的指定topic发送消息,消息将通过负载均衡策略进入到相应的partition中。

 

 

 

 

*每个消息都有一个连续的序列号叫做offset(偏移量),是消息在分区中的唯一标识。

*每个consumer都需要维护一个当前已消费消息的偏移量,类似于指针,随着consumer不断的读取消息,消费者的offset值也会不断的增加,consumer也可以以任意的顺序读取消息,只需要设置偏移量即可。

*每个consumer读取的偏移量都会同步给Kafka,在Kafka集群中同时会维护各个consumer消费的偏移量(消息在分区中的偏移量是固定的,Consumer的偏移量是动态可变的,其相当于读取的指针)

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

 

Kafka的分区备份

*每个分区在Kafka集群的若干服务中都有副本( 数量可配置 ),使Kafka具备了容错能力。

*在逻辑上相关的一组分区中,都由一个服务器作为leader,其余服务器作为follower,leader和follwer的选举是随机的,当follower接收到请求首先会发送给leader,由leader负责消息的读和写并将消息同步给各个follower,如果Leader所在节点宕机,followers中的一台则会自动成为leader。

 

 

2.Producer生产者

 

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

 

 

3.Consumer消费者

 

Kafka中提供了Consumer组的概念,一个Consumer组中包含若干个Consumer,整体对外可看成是一个消费者。

 

 

 
 

传统的消息队列模式

传统的消息队列能顺序的保存同一个生产者发送的消息,但尽管服务器保证了消息的顺序,但消息最终是通过异步的方式发送给各个消费者,当多个消费者并行消费时,并不能保证队列中消息能按顺序到达各个消费者中。
 
*Kafka采用的策略:一个topic中的各个partition只能被consumer组下的唯一一个consumer消费,用于保证消息到达的顺序,因此同一个组下的consumer的数量不能超过topic中的分区数,否则其他consumer将会处于空闲状态。
*Topic中的Partition数量决定了Consumer组下Consumer的数量。  
 

队列模式

若所有的消费者都在同一个consumer组中则成为队列模式,topic中各个分区的消息仅能被组中分区个的唯一consumer消费,组下的consumer共同竞争topic中的分区。

 

发布-订阅模式

若所有的消费者都不在同一个consumer组中则成为发布-订阅模式,topic中各个分区的消息都会广播给所有的consumer组。

 

 

4.Kafka的使用

 

1.安装

 

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

 

 

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

 

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

 

2.配置文件

 

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

 

server.proeperties(broker的配置文件)

 

 

*由于多个Kafka服务(broker)都使用同一个zk集群,因此在同一个zk集群中的Kafka也就自动成为集群的关系,因此borker.id在同一个集群中不能重复。

*Kafka中的消息是缓存到本地磁盘的(log.dirs目录下),每个topic的分区在broker的日志路径下都对应一个目录,目录下的.log文件用于存放分区中的消息,当有新消息进入分区时直接追加到文件中。

*若创建的topic其备份数大于1 (状态保存在zk) ,则Kafka集群中备份数个broker也会创建此topic,因此在其日志路径下也会存在此topic各个分区的目录。

 

consumer.properties(消费者的配置文件)

 

*在使用Kafka提供的消费者脚本文件时可以指定其使用的配置文件。

*在程序中使用时需要手动设置配置项。

 

producer.properties(生产者的配置文件)

 

*在使用Kafka提供的生产者脚本文件时可以指定其使用的配置文件。

*在程序中使用时需要手动设置配置项。

 

 

3.启动

 

1.启动zk集群

 

逐一启动zk服务

 

 

2.启动Kafka集群

 

逐一启动kafka服务并指定使用的配置文件( service.properties文件中配置使用外置的ZK集群)

 

 

4.创建主题

 

创建名为chat的topic,其备份数为3并且每个topic下存在3个分区 (由于启动了3个broker,因此topic的备份数最多只能是3)

创建topic时需指定zk服务地址,zk中保存了topic的相关属性(备份数和分区数),Kafka集群再从zk服务中获取topic属性信息并在Kafka集群中备份数个节点创建该topic。

 

 

查看各个broker中的日志目录,可见目录下都生成了chat-0、chat-1、chat2分别表示chat主题中的第一个、第二个、第三个分区,每个分区中都有.log文件存放分区中的消息。

 

 

 

查看Kafka集群中chat主题下各个分区的状态

 

 

Topic:主题名称

PartitionCount:主题包含的分区数

ReplicationFactor:topic的备份数 

Partition:分区号

Leader:充当leader的broker节点

Replicas:存在备份的broker节点(不管存活)

Isr:存在备份并且存活的broker节点

 

 

5.生产者发送消息

 

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

*消息将会根据负载均衡机制随机进入分区

 

 

6.消费者订阅消息

 

 

*由于使用脚本文件启动消费者时没有指定使用的配置文件,所以三个消费者都不是同一个消费者组中,因此三个消费者都能够消费到chat主题中各个分区的消息。

 

 

 

 

*启动了三个消费者并指定使用的配置文件,默认的group.id是test-consumer-group,因此三个消费者都属于同一个消费者组中,topic中各个分区仅能被组下的唯一一个consumer消费。

*由于启动第一个消费者时,消费者组下只有一个消费者,因此消息都会被此消费者消费,当往消费者组中添加新的消费者并且生产者往主题添加消息时,此时消费者会重新竞争消息。

 

 

5.Java中操作Kafka

 

1.导入依赖

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

 

2.创建主题

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"); 


//创建KafkaProducer生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);

//创建生产端消息实体ProducerRecord并指定消息上传的topic名称、消息的Key、消息的Value(消息由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值可以重复,使用Kafka API往topic发送消息时,默认情况下将会根据消息的Key值进行散列来决定消息到达的分区。
*生产者的缓冲区保留尚未发送到服务器的消息,后台I/O线程负责将这些消息转换成请求发送到集群,若使用后不关闭生产者则会泄露这些资源。

 

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"); //创建KafkaConsumer消费者实例 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 (若设置自动提交偏移量给Kafka,当消费消息后,后续需要进行入库或其他操作失败了,那么数据将会丢失,需要重新设置偏移量去消费)
*使用seek(TopicPartition , long)方法手动设置消费者的偏移量。
 
 
 

6.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 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, "bootKafka");
        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, IntegerDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        return props;
    }

    //Consumer工厂Bean
    @Bean
    public ConsumerFactory<Integer, String> consumerFactory() {
        return new DefaultKafkaConsumerFactory<>(consumerProps());
    }
    
    //Kafka监听器工厂Bean
    @Bean
    public ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
    }


}
 

3.创建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.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return props;
    }

    //Producer工厂Bean
    @Bean
    public ProducerFactory<Integer, String> producerFactory() {
        return new DefaultKafkaProducerFactory<>(senderProps());
    }

    //kafkaTemplate,用于向Topic发送消息
    @Bean
    public KafkaTemplate<Integer, String> kafkaTemplate() {
        KafkaTemplate template = new KafkaTemplate<Integer, String>(producerFactory());
        return template;
    }


}

 

4.Producer生产消息

@Component
public class Producer {

    @Autowired
    private KafkaTemplate kafkaTemplate;

    public void send() throws Exception {
        kafkaTemplate.send("fruit", "apple");
    }
}

 

5.Consumer消费消息

@Component
public class Consumer {

    @KafkaListener(topics = "fruit")
    public void listen(String msgData) {

    }
    
}
 
 
 

7.使用Kafka面临的问题

 

1.Consumer端消息丢失

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

2.重复消费

当consumer读取消息后,当程序处理完成后,手动提交offset,若提交offset之前程序出错,则会导致重复消费。
*此时只需要保证幂等性即可(多次执行的最终结果保持一致)
 
 

3.Broker端消息丢失

当partition中的Leader重新选举时,有可能导致消息未来得及同步到其他Follower,最终导致消息丢失。
*此时只需要设置acks=all,producer必须等待Leader将消息同步给所有Follower后再进行返回。
 
 
 
 
 
posted @ 2018-04-25 18:44 辣鸡小篮子 阅读(...) 评论(...) 编辑 收藏