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确认收到消息就算投递成功;实际使用时,根据应用特性选择,绝大多数情况下都会中和可靠性和性能选择第三种模型。
负载均衡
当broker或consumer加入或离开时会触发负载均衡算法,使得一个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))); } } } }