分布式消息队列Kafka

概述

​ Kafka是Apache旗下,由LinkedIn公司开发,Scala语言编写的消息队列。Kafka是一种分布式的,基于发布/订阅的消息系统,能够高效并实时的吞吐数据,以及通过分布式集群及数据复制冗余机制(副本冗余机制)实现数据的安全。

特点

1 高吞吐量

​ Kafka 每秒可以生产约 25 万消息(50 MB),每秒处理 55 万消息(110 MB)

2 持久化数据存储

​ 可进行持久化操作。连续读写保证效率。将消息持久化到磁盘,因此可用于批量消费,例如 ETL,以及实时应用程序。通过将数据持久化到硬盘以及 replication 防止数据丢失。

ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(extract)、交互转换(transform)、加载(load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于数据仓库。

ETL是构建数据仓库的重要一环,用户从数据源抽取出所需的数据,经过数据清洗,最终按照预先定义好的数据仓库模型,将数据加载到数据仓库中去。

3 分布式系统易于拓展

​ 所有的 producer、broker 和consumer 都会有多个,均为分布式的。无需停机即可扩展机器。

4 客户端状态维护

​ 消息被处理的状态是在 consumer 端维护,而不是由 server 端维护。减轻服务器端的压力,为客户端会话管理提供了更好的灵活性。客户端主要维护offset,可以实现更灵活的信息。

适用场景

消息队列、网站活性跟踪、监控数据、日志收集

配置说明

伪分布式

  • 修改server.properties文件
    ​ 配置Kafka存储位置,要配置为非/tmp目录
    ​ log.dirs=非/tmp目录

  • 修改zookeeper.properties文件

    ​ Kafka内置了一个zookeeper,在伪分布式模式下,可以使用这个内置zk作为集群协调工具,但是这个内置的zookeeper只有一个节点,不能用在生产环境下。

    ​ 如果使用这个内置的zk,需要在zookeeper.properties文件中修改zk存储路径,改为非/tmp目录
    dataDir=非/tmp目录。

完全分布式

  • 修改server.properties文件

    • 配置当前broker的编号,要求唯一
      broker.id=0 #当前server编号
    • 配置当前broker使用的端口
      port=9092 #使用的端口
    • 配置Kafka存储位置,要配置为非/tmp目录
      log.dirs=非/tmp目录
    • 配置zookeeper集群的地址
      zookeeper.connect=hadoop01:2181,hadoop02:2181,hadoop03:2181
  • 复制信息:将配置完成的kafka复制到其它broker中,并修正相关配置
    scp -r kafka_2.11-1.0.0 root@hadoopxx:/home/software/


启动kafka
启动zookeeper(伪分布式):
bin/zookeeper-server-start.sh config/zookeeper.properties &
启动kafka:
bin/kafka-server-start.sh config/server.properties

操作说明

命令行操作

创建主题

bin/kafka-topics.sh --create --zookeeper hadoop01:2181,hadoop02:2181,hadoop03:2181 --partitions 1 --replication-factor 1 --topic test

查看所有主题

bin/kafka-topics.sh --list --zookeeper hadoop01:2181

查看主题信息

bin/kafka-topics.sh --describe --zookeeper hadoop01:2181 --topic test

启动生产者

bin/kafka-console-producer.sh --broker-list hadoop01:9092 --topic test

启动消费者

bin/kafka-console-consumer.sh --zookeeper hadoop01:2181 --topic test

API操作

操作说明

1 生产者写入数据的步骤

  • 序列化ProducerRecord

  • 根据分区规则决定数据去往哪一个分区

    分区规则

    • 如果ProducerRecord中指定了Partition,则直接将数据发往指定的分区

      ProducerRecord<String, String> record = new ProducerRecord<String, String>()是否有分区

    • 如果没有指定Partition但指定了Key,则kafka使用自己实现的hash方法对key进行散列,通过hash分桶算法将数据发往对应分区,保证相同的key被映射到相同的partition中

    • 如果没有指定Partition也没有指定Key,则kafka使用默认的partitioner,使用RoundRobin算法将消息均衡地分布在各个partition上。

    • 开发者也可以通过实现Partitioner接口,自己开发Partitioner,决定数据如何分发。

  • 消息被添加到相应的batch中,独立的线程将这些batch发送到Broker上

  • broker收到消息会返回一个响应。如果消息成功写入Kafka,则返回RecordMetaData对象,该对象包含了

    Topic信息、Patition信息、消息在Partition中的Offset信息;若失败,返回一个错误。

具体代码

创建Topics

@Test
	public void creatTopics(){
		ZkUtils zkUtils = ZkUtils.apply("aa:2181,bb:2181,bb:2181", 30000,
	            30000,JaasUtils.isZkSecurityEnabled());
		//检查topic是否存在
        boolean isExist = AdminUtils.topicExists(zkUtils, "student");
		System.out.println("topic student exists? "+isExist);
        //创建topic
		//AdminUtils.createTopic(zkUtils, "student", 1, 1, new Properties(), RackAwareMode.Enforced$.MODULE$);
		zkUtils.close();
	}
创建生产者
@Test
	public void producer(){
		//1.创建生产者
		Properties prop = new Properties();
        /* bootstrap.servers->指定broker的地址清单
        *key.serializer value.serializer
        *必须是一个实现org.apache.kafka.common.serialization.Serializer接口的类
        *将其序列化成字节数组*/
		prop.put("bootstrap.servers", "student01:9092,student02:9092,student03:9092");
		//key
		prop.put("key.serializer", StringSerializer.class);
		//value
		prop.put("value.serializer", StringSerializer.class);
		Producer<String, String> producer = new KafkaProducer<>(prop);
		//2.创建消息对象 分区名,value名 还能加key,partition,timestamp
		ProducerRecord<String, String> record = new ProducerRecord<String, String>("student", "hello banana");

1 异步非阻塞模式

//3.发送数据
try {
	Future<RecordMetadata> future = producer.send(record);
	RecordMetadata rmd = future.get();
	//4.写入成功,获取写入结果信息
	System.out.println("--topic:["+rmd.topic()+"]--partition:["+rmd.partition()+"]--offset:["+rmd.offset()+"]--timestamp:["+rmd.timestamp()+"]--");
	} catch (InterruptedException | ExecutionException e) {
	System.out.println("失败");
	e.printStackTrace();
}

2 通过Callback回调处理结果

//3.发送消息到kafka
producer.send(record, new Callback() {
 @Override
 public void onCompletion(RecordMetadata rmd, Exception e) {
     if(e == null){//--写入成功没有异常
        System.out.println("--topic:["+rmd.topic()+"]--partition:["+rmd.partition()+"]--offset:["+rmd.offset()+"]--timestamp:["+rmd.timestamp()+"]--");
     }else{//--写入失败有异常
        System.err.println("写入数据失败");
        e.printStackTrace();
     }
 }
});

生产者其他参数

a. acks
指定必须要有多少个partition副本收到消息,生产者才会认为消息的写入是成功的。
acks=0,生产者不需要等待服务器的响应,以网络能支持的最大速度发送消息,吞吐量高,但是如果broker没有收到消息,生产者是不知道的
acks=1,leader partition收到消息,生产者就会收到一个来自服务器的成功响应
acks=all,所有的partition都收到消息,生产者才会收到一个服务器的成功响应
b. buffer.memory
设置生产者内缓存区域的大小,生产者用它缓冲要发送到服务器的消息。
c. compression.type
默认情况下,消息发送时不会被压缩,该参数可以设置成snappy、gzip或lz4对发送给broker的消息进行压缩
d. retries
生产者从服务器收到临时性错误时,生产者重发消息的次数
e. batch.size
发送到同一个partition的消息会被先存储在batch中,该参数指定一个batch可以使用的内存大小,单位是byte。不一定需要等到batch被填满才能发送
f. linger.ms
生产者在发送消息前等待linger.ms,从而等待更多的消息加入到batch中。如果batch被填满或者linger.ms达到上限,就把batch中的消息发送出去
g. max.in.flight.requests.per.connection
生产者在收到服务器响应之前可以发送的消息个数

自定义Avro序列化器

1 编写bean

Class Custom {
    private int customID;
    private String customerName;
    
    public Custom(int customID, String customerName) {
        super();
        this.customID = customID;
        this.customerName = customerName;
    }

    public int getCustomID() {
        return customID;
    }

    public String getCustomerName() {
        return customerName;
    }
}

2 编写Schema

{  
  "namespace": "customerManagement.avro",  
   "type": "record",  
   "name": "Customer",  
   "fields":[  
       {  
          "name": "id", "type": "string"  
       },  
       {  
          "name": "name",  "type": "string"  
       },  
   ]  
}

3 发送到kafka

Properties props = new Properties();  
props.put("bootstrap", "loacalhost:9092");  
props.put("key.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");  
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");  
props.put("schema.registry.url", schemaUrl);//schema.registry.url指向序列化的存储位置
String topic = "CustomerContacts";
Producer<String, Customer> produer = new KafkaProducer<String, Customer>(props);
创建消费者
@Test
public void consumer(){
	Properties props = new Properties();
	props.put("bootstrap.servers", "aa:9092");
	//group.id 指定当前消费者所属的消费者组
	props.put("group.id", "consumer-tutorial");
    //key.deserializer value.deserializer 后面跟用来反序列化的类
    //必须是一个实现org.apache.kafka.common.serialization.Deserializer接口的类
	props.put("key.deserializer", StringDeserializer.class.getName());
	props.put("value.deserializer", StringDeserializer.class.getName());
	KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
    //指定监听的主题
	consumer.subscribe(Arrays.asList("enbook", "student"));
	try {
		while (true) {
            //消费时间长度为Long.MAX_VALUE毫秒的数据
			ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
			for (ConsumerRecord<String, String> record : records){
			      System.out.println("消费:"+record.offset() + ":" + record.value());
			}
			} catch (Exception e) {
			} finally {
			  consumer.close();
			}
	}
}

消费者的其他相关参数

a. fetch.min.bytes
指定消费者从broker获取消息的最小字节数,即等到有足够的数据时才把它返回给消费者
b. fetch.max.wait.ms
等待broker返回数据的最大时间,默认是500ms。fetch.min.bytes和fetch.max.wait.ms哪个条件先得到满足,就按照哪种方式返回数据
c. max.partition.fetch.bytes
指定broker从每个partition中返回给消费者的最大字节数,默认1MB
d. session.timeout.ms
指定消费者被认定死亡之前可以与服务器断开连接的时间,默认是3s
e. auto.offset.reset
消费者在读取一个没有偏移量或者偏移量无效的情况下(因为消费者长时间失效,包含偏移量的记录已经过时并被删除)该作何处理。默认是latest(消费者从最新的记录开始读取数据)。另一个值是earliest(消费者从起始位置读取partition的记录)
f. enable.auto.commit
指定消费者是否自动提交偏移量,默认为true
g. partition.assignment.strategy
指定partition如何分配给消费者,默认是Range。
Range:把Topic的若干个连续的partition分配给消费者。
RoundRobin:把Topic的所有partition逐个分配给消费者
h. max.poll.records
单次调用poll方法能够返回的消息数量


偏移量的提交 —commitAsync VS commitSync()

同步提交:提交的方法将会有阻塞,直到提交完成,才放开阻塞,效率低,可靠性高

异步提交:提交的方法不会有阻塞,无论是否提交完成,都继续向下执行,效率高,可靠性低

混合提交:通常情况下使用异步提交,保证效率,出问题情况下使用同步提交,保证可靠提交

//同步提交
try {
while(true){
    ConsumerRecords<String, String> rs = consumer.poll(100);
    for(ConsumerRecord<String, String> r : rs){
   System.out.println("--topic:["+r.topic()+"]"+ "--value:["+r.value()+"]--");
   }                        
	//--手动提交offset 同步提交,此方法将会阻塞,直到提交完成,如果提交失败,抛出异常
	 consumer.commitSync();
   }
} catch (Exception e) {
  e.printStackTrace();
} finally {
  //4.关闭消费者
  if(consumer!=null)consumer.close();
}
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
    if(e != null){//--提交抛出了异常
    System.err.println("提交偏移量出错!");
    e.printStackTrace();
    }
}
//混合提交
try{
while(true){
	ConsumerRecords<String, String> rs = consumer.poll(100);
	for(ConsumerRecord<String, String> r : rs){
    	System.out.println("--topic:["+r.topic()+"]"+ "--value:["+r.value()+"]--");
    }
    //--手动提交offset 混合提交提交,通常用异步提交
    consumer.commitAsync();
 }
} catch (Exception e) {
     e.printStackTrace();
} finally {
    //--出问题了,提交offset,此处为了保证能够提交,使用同步提交
    consumer.commitSync();
    //4.关闭消费者
    if(consumer!=null)
    consumer.close();
}

猜你喜欢

转载自blog.csdn.net/weixin_42712876/article/details/84864903