It is the path you have chosen. Take pride in it. Kotomine Kirei

无极之道

Kafka学习笔记

三大消息中间件对比
ActiveMQ是Apache下的一个子项目,是java写的消息队列,所以可以作为一个jar包,放到java项目里,用代码启动和配置,适用于这种简单的场景,单机吞吐量万级。
 
RabbitMQ使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发,单机吞吐量万级,是功能最丰富,最完善的企业级队列。基本没有做不了的,就算是做类似kafka的高可用集群,也是没问题的,不过安装部署麻烦了点。
 
Kafka是scala实现的一个高性能分布式Publish/Subscribe消息队列系统,具有快速持久化、高吞吐(十万级)、高堆积(支持topic下消费者较长时间离线,消息堆积量大)、完全分布式(Broker、Producer、Consumer都原生自动支持分布式,依赖zookeeper自动实现负载均衡)的特点,如果有集群要求,那么kafka是当仁不让的首选,尤其在海量数据,以及数据有倾斜问题的场景里,因为partition的缘故,数据倾斜问题自动解决。比如个别Topic的消息量非常大,在kafka里,调整partition数就好了。反而在rabbitmq或者activemq里,这个不好解决。
 
相关概念

1、AMQP协议(Advanced Message Queuing Protocol,高级消息队列协议)
AMQP是一个标准开放的应用层的消息中间件(Message Oriented Middleware)协议。AMQP定义了通过网络发送的字节流的数据格式。因此兼容性非常好,任何实现AMQP协议的程序都可以和与AMQP协议兼容的其他程序交互,可以很容易做到跨语言,跨平台。

2、一些基本的概念
Topic:
主题,每一类的消息称之为一个主题(Topic)。
Partition:
一个Topic中的消息数据按照多个分区组织,分区是kafka消息队列组织的最小单位,一个分区可以看作是一个FIFO( First Input First Output的缩写,先入先出队列)的队列。
Producer:
生产者,向broker发布消息(push)的应用程序。
Consumer:
消费者,从消息队列中请求消息(pull)的客户端应用程序。
Consumer group:
high-level consumer API 中,每个consumer 都属于一个consumer group,每条消息只能被consumer group中的一个 Consumer 消费,但可以被多个consumer group 消费。
broker:
AMQP服务端,用来接收生产者发送的消息并将这些消息路由给服务器中的队列,便于kafka将生产者发送的消息,动态的添加到磁盘并给每一条消息一个偏移量,所以对于kafka一个broker就是一个应用程序的实例。
ZooKeeper:
Kafka的运行依赖于Zookeeper。Topic、Consumer、Patition、Broker等注册信息都存储在ZooKeeper中。
 
Consumer与Partition的关系:
  一个partition只能被同一个consumer group下的一个consumer thread消费,不能一个consumer group的多个consumer同时消费一个partition,但是一个consumer thread可以消费多个partition。简单理解就是partition中的每个message只能被组(Consumer group 中的一个consumer(consumer 线程)消费,如果一个message可以被多个consumer(consumer 线程消费的话,那么这些consumer必须在不同的组。Kafka不支持一个partition中的message由两个或两个以上的同一个consumer group下的consumer thread来处理,除非再启动一个新的consumer group。所以如果想同时对一个topic做消费的话,启动多个consumer group就可以了。一般这种情况都是两个不同的业务逻辑,才会启动两个consumer group来处理一个topic。多个Consumer Group下的consumer可以消费同一条message,但是这种消费也是以O(1)的方式顺序的读取message去消费,所以一定会重复消费这批message的,不能向AMQ那样多个BET作为consumer消费(对message加锁,消费的时候不能重复消费message)。
  当启动一个consumer group去消费一个topic的时候,无论topic里面有多个少个partition,无论我们consumer group里面配置了多少个consumer thread,这个consumer group下面的所有consumer thread一定会消费全部的partition;即便这个consumer group下只有一个consumer thread,那么这个consumer thread也会去消费所有的partition。因此,最优的设计就是,consumer group下的consumer thread的数量等于partition数量,这样效率是最高的。
  如果producer的流量增大,当前的topic的partition数量=consumer数量,这时候的应对方式就是横向扩展:增加topic下的partition,同时增加这个consumer group下的consumer。
 
消息投递可靠性
一个消息如何算投递成功,Kafka提供了三种模式:
第一种是啥都不管,发送出去就当作成功,这种情况当然不能保证消息成功投递到broker;
第二种是Master-Slave模型,只有当Master和所有Slave都接收到消息时,才算投递成功,这种模型提供了最高的投递可靠性,但是损伤了性能;
第三种模型,即只要Master确认收到消息就算投递成功;实际使用时,根据应用特性选择,绝大多数情况下都会中和可靠性和性能选择第三种模型。
 
负载均衡
brokerconsumer加入或离开时会触发负载均衡算法,使得一个consumer group内的多个consumer的消费负载平衡。
 
常用命令
查看kafka进程:jps
配置信息:/config/server.properties
创建topic:./kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic test
                                zookeeper.connect          副本数      分区数    topic名
查看topic列表:./kafka-topics.sh --list --zookeeper 172.20.1.28:4101
单个topic信息:./kafka-topics.sh --describe --zookeeper "172.20.1.28:4101" --topic TM_ORDER
发送消息:./kafka-console-producer.sh --broker-list 172.20.1.28:9092 --topic demo
接收消息:./kafka-console-consumer.sh --zookeeper 172.20.1.28:4101 --topic demo --from-beginning
 
编写Kafka程序
1、pom.xml添加依赖
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka_2.12</artifactId>
    <version>0.10.2.1</version>
</dependency>

2、Producer

public class KafkaProducerTest {
    public static void main(String[] args) {
        produce();
    }
    private static void produce(){
        //创建kafka的生产者类
        try (Producer<String, String> producer = createProducer()) {
            for (int i = 0; i < 10; i++) {
                producer.send(new ProducerRecord<>("BS_REFUND_ORDER", Integer.toString(i), "message" + i));
                System.out.println("produce success: message" + i);
            }
        }catch (Exception e){
            System.out.println("produce failed due to " + e.getMessage());
        }
    }
    public static Producer createProducer(){
        Properties props = new Properties();
        props.put("bootstrap.servers", "172.20.1.28:4401");
        //“所有”设置将导致记录的完整提交阻塞,最慢的,但最持久的设置。
        props.put("acks", "all");
        //如果请求失败,生产者也会自动重试,即使设置成0
        props.put("retries", 0);

        props.put("batch.size", 16384);
        //默认立即发送,这里这是延时毫秒数
        props.put("linger.ms", 1);
        //生产者缓冲大小,当缓冲区耗尽后,额外的发送调用将被阻塞.时间超过max.block.ms将抛出TimeoutException
        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);
        return producer;
    }
}

3、Consumer

/**
 * @Title KafkaConsumerTest
 * @Description  自动提交offset偏移量
 */
public class KafkaConsumerTest {
    public static void main(String[] args) {
        consume();
    }

    public static KafkaConsumer createConsumer(){
        Properties props = new Properties();
        //brokerServer(kafka)ip地址,不需要把所有集群中的地址都写上,可是一个或一部分
        props.put("bootstrap.servers", "172.20.1.28:4401");
        //设置consumer group name,必须设置
        props.put("group.id", "BS_REFUND_ORDER_GROUP");
        //设置自动提交偏移量(offset),由auto.commit.interval.ms控制提交频率
        props.put("enable.auto.commit", "true");
        //偏移量(offset)提交频率
        props.put("auto.commit.interval.ms", "1000");
        //设置使用最开始的offset偏移量为该group.id的最早值。如果不设置,则会是latest即该topic最新一个消息的offset
        //如果采用latest,消费者只能得到其启动后,生产者生产的消息
        props.put("auto.offset.reset", "earliest");
        //从topic poll(拉)消息的会话处理时长
        props.put("session.timeout.ms", "30000");
        //poll的数量限制
        props.put("max.poll.records", "100");
        //设置key以及value的解析(反序列)类
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(props);
        return kafkaConsumer;
    }

    public static void consume(){
        KafkaConsumer<String, String> kafkaConsumer = createConsumer();
        //订阅主题列表topic
        kafkaConsumer.subscribe(Arrays.asList("BS_REFUND_ORDER"));

        while (true) {
            //拉取消息的延迟时间
            ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
            if (records.count() > 0) {
                for (ConsumerRecord<String, String> record : records) {
                    //正常情况下应该使用线程池处理,不应该这样处理
                    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
                }
            } else {
                System.out.println("No data in topic");
            }
        }
    }

    /**
     * 指定消费某个分区的消息
     *
     */
    public static void manualPartion() {
        TopicPartition partition0 = new TopicPartition("BS_REFUND_ORDER", 0);
        TopicPartition partition1 = new TopicPartition("BS_REFUND_ORDER", 1);
        KafkaConsumer<String, String> consumer = createConsumer();
        consumer.assign(Arrays.asList(partition0, partition1));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s  \r\n", record.offset(), record.key(), record.value());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

4、手动提交偏移量

public class ManualOffsetConsumer {
    private static Logger LOG = LoggerFactory.getLogger(ManualOffsetConsumer.class);

    public static void main(String[] args) {

    }

    public static KafkaConsumer createConsumer(){
        Properties props = new Properties();
        props.put("bootstrap.servers", "172.20.1.28:4401");
        props.put("group.id","BS_REFUND_ORDER_GROUP");
        props.put("enable.auto.commit", "false");
        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<String ,String> consumer = new KafkaConsumer<>(props);
        return consumer;
    }

    /**
     * 手动批量提交偏移量
     *
     */
    public static void manualOffset(){
        KafkaConsumer<String ,String> consumer = createConsumer();
        consumer.subscribe(Arrays.asList("BS_REFUND_ORDER"));
        //批量提交数量
        final int minBatchSize = 5;
        List<ConsumerRecord<String, String>> buffer = new ArrayList<>();
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                LOG.info("consumer message values is "+record.value()+" and the offset is "+ record.offset());
                buffer.add(record);
            }
            if (buffer.size() >= minBatchSize) {
                LOG.info("now commit offset");
                consumer.commitSync();
                buffer.clear();
            }
        }
    }

    /**
    * 消费完一个分区后手动提交偏移量
    *
    */
    public static void manualCommitPartion(){
        KafkaConsumer<String ,String> consumer = createConsumer();
        consumer.subscribe(Arrays.asList("BS_REFUND_ORDER"));
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
            for (TopicPartition partition : records.partitions()) {
                List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                for (ConsumerRecord<String, String> record : partitionRecords) {
                    LOG.info("now consumer the message it's offset is :" + record.offset() + " and the value is :" + record.value());
                }
                long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
                LOG.info("now commit the partition[ " + partition.partition() + "] offset");
                consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
            }
        }
    }

}

 

posted on 2019-03-30 16:50  无极之道  阅读(255)  评论(0编辑  收藏  举报

导航