分布式-MQ-06 kafka基本使用

一、kafak架构中相关术语

RabbitMQ-01章节中,讲述了消息队列的基础知识,包括优劣势、常见产品、应用场景等。

最近在生产中又使用到了aws的队列产品Simple Queue Sservice。从其特性看:超高吞吐量(支持批量消息Max10条/次,20s自动长轮询等待)、局部有序性的标准队列;FIFO队列等特性都和kafka的功能极其相似(暴论:猜测sqs也是kafka的衍生产品)。并且支持文件存储到S3桶、分布式消息队列全球拉取的能力,确实值得国内大厂们学习对标
在这里插入图片描述
kafka的高吞吐量、消息生产消费的高可用性、分布式系统的稳定性,是很多衍生产品争相学习的热点。其相关术语则是大家语言环境中的基础,今天再把相关概念翻出来念念。

1.1 kafka所遵守JMS规范

JMS(Java Message Service)指Java平台中关于消息中间件的API规范。用于多应用进程或分布式系统间发送消息,异步通信。JMS规范模型如下:
在这里插入图片描述
JMS规范中有2种消息传播模式:

  • Point-to-Point点对点队列模式
    类似上文中提到的亚马逊的sqs即是点对点模式,通过轮询到某个既定的队列地址拉取消息,实现消费。
    在这里插入图片描述
  • pub/sub发布订阅模式
    kafka最广泛的使用场景,监听Broker中的某个topic,由服务器像所有消费者组推送新消息。
    在这里插入图片描述
1.2 相关术语
术语名称解释
Producer、Consumer消息生产者,向Broker发送消息的客户端;消息消费者,从Broker读取消息的客户端。
Topic主题。kafka根据topic对消息进行归类
Brokerkafka服务的实例(节点),一个或多个Broker组成kafka cluster
Group.id消费者分组。每个ConsumerGroup(Group.id)中只有1个Consumer可以唯一的消费某条消息
Partition物理分区,一个Topic可以分成多个partition,每个partition内的消息是局部有序的
Replicas副本数,一般追求集群高可用时,会设置不少于2的副本数,当Leader宕机时,选择其他broker收发消息
Leaderkafka集群中,partition一般都会有副本(Replicas),在各Broker间会由kafka推举出Leader用来收发消息
Followers多副本中,Followers将从Leader拉取消息,当Leader所在Broker宕机时,将从Follower中重选Leader
AR、IsrAssigned Repllicas(所有的副本,包括宕机的副本);In-Sync Replicas(与Leader保持一定同步的副本)
Offset偏移量,在partition中的消息是顺序写入的,当需要快速定位某条消息时,需要为其加上索引序号Offset
HWHeighWaterMark高水位,当所有副本中的活跃实例ISR中消费记录的偏移量达到统一,follower追上Leader时,HW值更新为最新offset值,并使最新记录对所有消费者可见。
LEOLog-end-offset,每个分区节点中日志偏移量末尾值。
1.3 术语详解及使用
  • Producer、Consumer
    生产消费模式大家熟知,在高并发章节中也时常提起,为了提高服务的吞吐能力,好像大家思路一致,三两步思路便都考虑到解藕。kafka服务端和线程池的BlockingQueue队列设计思路相似,都是有生产者将消息或Task推送到队列或kafka的服务器,再由消费者从队列中拉取消息,消费消息。进而实现了服务端解藕,提高并发能力。
    在这里插入图片描述
  • Topic
    典型订阅/发布模式中主题信息,当用户订阅了某个频道或大V的产品,则当该频道发送消息时,所有fans将接收到该消息。
  • Broker
    每个kafka实例都是一个Broker,受限于单机句柄数、IO瓶颈,会将多个Broker实例组成集群,提高并发能力。
    在这里插入图片描述
  • group.id
    ConsumerGroup消费者组需要为该组指定唯一的标识,即group.id。Producer在将消息发送都partition时,只会将消息写入partition中的某一个,这样当消费者消费消息时,同样group组内也只有唯一的consumer可以消费该消息。
    验证过程:新建topic:test_msg_partition,副本数为3,分区为2。向该topic发送消息:
    [root@worker1 kafka_2.11-2.4.1]# bin/kafka-topics.sh --create --zookeeper 192.168.149.128:2181 --replication-factor 3 --partitions 2 --topic test_mes_partition
    [root@worker1 kafka_2.11-2.4.1]# bin/kafka-console-producer.sh --broker-list 192.168.149.128:9092 --topic test_mes_partition
    >how many partition will receive this message?
    

在这里插入图片描述
从管理台检查,当所有副本都来到ISR后(数据同步完毕),检查哪个分区有消息:发现只有分区1数据写入
在这里插入图片描述

  • Partition

    kafka集群在启动时会在多个broker中选择一个controller,由controller负责对分区的leader进行选举,每个分区选择1个broker作为当前分区的leader,分区的其他副本作为follower。

    • controller选举机制:
      逐台Broker启动,谁先启动谁是controller,脚本批量启动,谁先注册到zk(create broker),谁是controller。
    • leader选举机制:
      一般只有ISR结合中的副本才有资格在现有Leader宕机时被选举为信Leader(默认会选择ISR列表中第一个broker作为leader,又问:为啥选第一个broker?被放入ISR列表的顺序靠前,同步副本的水位也应相对较高)
      但也能控制 unclean.leader.election.enable=true时,当ISR列表为空时,也可以在非ISR列表中而存在于AR中的broker选择leader
      在这里插入图片描述
  • Leader、Followers
    default.replication.factor=1自动创建topic时默认副本数量,当副本数设置为1时,当前broker实例即是Leader,所有消息的发送和读取都发生在当前Broker。但为了提高集群可用性,考虑到该Broker实例挂了呢?一般建议将副本数设置为集群中broker个数。
    那什么又是Leader、Followers呢?

    • Leader:收发消息的broker,实际应用中主要工作者。
    • Follower:复制Leader的消息,当Leader挂了时,伺机顶上。
  • Replicas
    副本数,名为副本数,实际指集群中所有broker的数量。

  • Isr
    In-Sync Replicas,同步副本。所有副本中与Leader中offset偏移量同步值保持一致的副本。

  • offset偏移量
    日志文件中的偏移量,相当于数组所在下标索引。

  • 动态看生产过程中上述术语的含义
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

二、相关API

  <dependencies>
    <dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka-clients</artifactId>
      <version>2.4.1</version>
    </dependency>
  </dependencies>
2.1 生产者API
public class ProducerDemo {
    private final static String TOPIC_NAME = "my‐local‐topic";

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Properties props = new Properties();
        //broker服务端主机列表(kafka去zk化的标志,在历史版本中,此处客户端直接连接zk的端口)
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.128:9092,192.168.149.129:9092,192.168.149.130:9092");
        /*
        发出消息持久化机制参数,acks默认值为1
        1、acks=0:producer不会等待Broker确认结果即认为消息发送成功;性能最高,但是最容易丢消息.
        2、acks=1:producer等待leader将数据写入本地的log日志,不等待follower写入即可认为服务端写入数据成功.
            这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失.
        3、acks=‐1或all:producer需要等待 最少同步副本数 min.insync.replicas(默认为1,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,才认为服务端写入成功.
            这种策略会保证,只要有一个备份存活就不会丢失数据.
         */
        props.put(ProducerConfig.ACKS_CONFIG, "1");
         /*
        发送失败会重试的次数,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在
        接收者那边做好消息接收的幂等性处理
        */
        props.put(ProducerConfig.RETRIES_CONFIG, 3);
        //默认重试间隔100ms,重试间隔设置300ms
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);

        //设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,即32MB
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        /*
        kafka本地线程会从缓冲区取数据,批量发送到broker,
        设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去
        */
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
        /*
        定义1个时间周期,本地缓冲区batch消息即使不满16ks,满足这个时间间隔也要发送到服务端
        默认值是0,意思就是消息必须立即被发送,但这样会影响性能
        */
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

        //把发送的key从字符串序列化为字节数组
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        //把发送消息value从字符串序列化为字节数组
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        Producer<String, String> producer = new KafkaProducer<String, String>(props);

        int msgNum = 5;
        final CountDownLatch countDownLatch = new CountDownLatch(msgNum);
        for (int i = 1; i <= msgNum; i++) {
            //指定发送分区
           /* ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME
                    , 0, "order"+i, "value"+i);*/
            //未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum
            ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME
                    , /*key*/"order"+i, "value"+i);

            //等待消息发送成功的同步阻塞方法(当acks为1或-1时生效)
            RecordMetadata metadata = producer.send(producerRecord).get();
            System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
                    + metadata.partition() + "|offset-" + metadata.offset());

            //异步回调方式发送消息
            producer.send(producerRecord, new Callback() {
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception != null) {
                        System.err.println("发送消息失败:" + exception.getStackTrace());

                    }
                    if (metadata != null) {
                        System.out.println("异步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
                                + metadata.partition() + "|offset-" + metadata.offset());
                    }
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await(5, TimeUnit.SECONDS);
        producer.close();
    }
}
2.2 消费者API
public class ConsumerDemo {
    private final static String TOPIC_NAME = "my-local-topic";
    private final static String CONSUMER_GROUP_NAME = "groupLocal0";

    public static void main(String[] args) {
        Properties props = new Properties();
        // 指定broker服务器端口
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.149.128:9092,192.168.149.129:9092,192.168.149.130:9092");
        // 消费者所在组 group.id
        props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
        // 是否自动提交offset,默认就是true
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        // 自动提交offset的间隔时间
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        //props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

        /*
        当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
        latest(默认) :只消费自己启动之后发送到主题的消息
        earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
        */
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

        /*
         consumer 控制心跳
         consumer给broker发送心跳的间隔时间,broker接收到心跳如果此时有rebalance发生会通过心跳响应将
         rebalance方案下发给consumer,这个时间可以稍微短一点
         */
        props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);

        /*
        服务端管理心跳
        服务端broker多久感知不到一个consumer心跳就认为他故障了,会将其踢出消费组,
        对应的Partition也会被重新分配给其他consumer,默认是10秒
        */
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);

        //一次poll最大拉取消息的条数,如果消费者处理速度很快,可以设置大点,如果处理速度一般,可以设置小点
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);

        /*
        如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,
        会将其踢出消费组,将分区分配给别的consumer消费
        */
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
        //序列化与反序列化的代理类
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);

        consumer.subscribe(Arrays.asList(TOPIC_NAME));
        // 消费指定分区
        //consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));

        //消息回溯消费
        /*consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));*/

        //指定offset消费
        /*consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
        consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);*/

        //从指定时间点开始消费
        /*List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
        //从1小时前开始消费
        long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
        Map<TopicPartition, Long> map = new HashMap<>();
        for (PartitionInfo par : topicPartitions) {
            map.put(new TopicPartition(topicName, par.partition()), fetchDataTime);
        }
        Map<TopicPartition, OffsetAndTimestamp> parMap = consumer.offsetsForTimes(map);
        for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet()) {
            TopicPartition key = entry.getKey();
            OffsetAndTimestamp value = entry.getValue();
            if (key == null || value == null) continue;
            Long offset = value.offset();
            System.out.println("partition-" + key.partition() + "|offset-" + offset);
            System.out.println();
            //根据消费里的timestamp确定offset
            if (value != null) {
                consumer.assign(Arrays.asList(key));
                consumer.seek(key, offset);
            }
        }*/

        while (true) {
            /*
             * poll() API 是拉取消息的长轮询
             */
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("收到消息:partition = %d,offset = %d, key = %s, value = %s%n", record.partition(),
                        record.offset(), record.key(), record.value());
            }

            /*if (records.count() > 0) {
                // 手动同步提交offset,当前线程会阻塞直到offset提交成功
                // 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
                consumer.commitSync();

                // 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
                consumer.commitAsync(new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                        if (exception != null) {
                            System.err.println("Commit failed for " + offsets);
                            System.err.println("Commit failed exception: " + exception.getStackTrace());
                        }
                    }
                });

            }*/
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值