kafka
目录
开始
简介
官方网站:https://kafka.apache.org/
官方网站中有软件下载地址和更详细的文档,值得参考!
是由领英(LinkedIn)设计实现,利用Scala语言编写,后贡献给了Apache,kafka是是一种分布式的消息队列,能构建实时流计算平台;可用于离线批处理(MR、Hive、Apark)和实时计算(Storm、SparkStreaming、Flink),一般用于flume和storm之间,另外kafka还实现了活动流和运营数据流;
- 活动流:客户访问量产生的数据,;
- 运营数据流:本地服务器产生的数据;
其只要功能包括:
- 发布订阅数据流,类似于消息队列(具有消息队列的相关功能);
- 屏蔽消费者和生产者之间的异构性,实现解耦;
- 是分布式架构,可吞吐海量数据,数据不丢,但是没有可靠事物;
- kafka的消费者采用的poll方式获取数据,故消费速率由消费者控制;
- kafka提供了数据的持久化机制和副本冗余机制,无论是否消费,数据都会存储,并且容错性高;
kafka为什么快
kafka的消息是保存或缓存在磁盘上的,一般认为磁盘上的读写性能都不会太高,但实际上kafka的特性之一就是高吞吐率,即使是普通的服务器,也可以轻松支持每秒百万级的写入,超过大部分其他MQ;
写入
kafka会把收到的消息写入到硬盘中,保证了数据不会丢失,同时为了优化写入速度采用了两种技术:顺序写入和MMFile;
顺序写入:kafka中,每一个分区对应一个文件,收到消息后会把数据插入到文件末尾;(该方式的缺陷就是没发删除数据,所以kafka不会直接删除数据,而是使用了两种策略来删除数据)
MMF(Memory Mapped Files):即kafka数据并不是实时写入硬盘的,而是使用了内存映射文件,其原理是利用操作系统的Page实现文件到物理内存的直接映射,实现对物理内存的操作会在适当的时候同步到硬盘上,这种方式省去了用户空间到内核空间复制的开销,极大的提升了I/O性能;但同时也存在不可靠的缺陷,因为写在MMF中的数据并没有立即写入硬盘,需要程序主动flush(可以通过参数producer.type设置);
读取
kafka使用了零拷贝技术,直接将上面的MMF作为文件句柄,将文件内容发送给消费者;利用偏移量(offset)记录从哪里开始读取。同时还使用了批量压缩的发送消息以节省带宽。
安装
- 解压
- 修改或添加如下配置
server.properties
;
# 给每一个Kafka节点配置编号,集群中每个节点编号应该不同
broker.id=0
# 消息的存储目录
log.dirs=/data/kafka_2.11-1.0.0/kafka-logs
# Zookeeper的连接地址
zookeeper.connect=hadoop01:2181,hadoop02:2181,hadoop03:2181
# 允许删除主题,默认false
delete.topic.enable=true
# 端口,默认9092
# port=9092
# 下面是些常用的配置
# 默认分区数(创建topic没指定时才生效),数量大小决定了消费的并行度
num.partitions=3
# 分区日志文件的最大大小
log.segment.bytes=1073741824
# 分区日志文件达到此事件也使用新文件
log.retention.hours=168
# 生产者的确认机制,0-不需要确认,直接写入;1-至少要leader写入成功;all-保证所有备份写入成功
acks=all
# 生产端的批处理大小
batch.size=16384
# 生产端默认就有延时,这个是配置kafka的延时,这样可以聚合更多的消息,默认0,无延时
linger.ms=0
启动:./bin/kafka-server-start.sh ./config/server.properties
;
kafka集群中没有选举问题,没有leader;
常见命令
操作命令:
./bin/kafka-topics.sh --list --bootstrap-server hadoop01:9092
./bin/kafka-topics.sh --delete --bootstrap-server hadoop01:9092 --topic xxx
- 创建主题:
./bin/kafka-topics.sh --create --zookeeper hadoop01:2181 --replication-factor 1 --partitions 1 --topic xxx
; - 查看所有主题:
./bin/kafka-topics.sh --list --zookeeper hadoop01:2181
; - 启动生产者:
./bin/kafka-console-producer.sh --broker-list hadoop01:9092 --topic xxx
; - 启动消费者(用于测试,不会使用消费者组):
./bin/kafka-console-consumer.sh --zookeeper hadoop01:2181 --topic xxx --from-begining
; - 删除主题:
./bin/kafka-topics.sh --delete --zookeeper hadoop01:2181 --topic xxx
; - 创建分组:
./bin/kafka-console-consumer.sh --bootstrap-server hadoop01:9092 --topic xxx --from-beginning --new-consumer
; - 获取分组列表名称:
./bin/kafka-consumer-groups.sh --bootstrap-server hadoop01:9092 --list --new-consumer
; - 在一个分组内创建消费者:
./bin/kafka-console-consumer.sh --bootstrap-server hadoop01:9092 --topic xxx --from-beginning --consumer.config config/consumer.properties
核心概念
topic
主题是生产者和消费者传递消息的基础,具有以下特点:
- 主题(topic):每个生产者消费者都要对应一个主题,每个topic在磁盘中都对应一个目录;
- topic逻辑上可认为是一个queue,每条消息都需要指定topic;
- topic会分为一个或者多个partition
partition
一个主题可以指定多个分区,分区在物理上就是一个文件目录,底层是队列,满足FIFO,具有以下特点:
- 分区(partitions):分区数等于目录数,且会分布在不同的节点;
- 生产者发送的消息会轮询的append到partition中,属于顺序写磁盘,效率高,相当于随机写内存;
- 消费后的数据不会立即删除,而遵循消息删除策略:
- 按时间间隔删除:log.retention.hours=168(1周);
- 按日志大小清理:log.segment.bytes=1073741824(1GB);
- 是否开启清理:log.cleaner.enable=false;
replication
副本是为了保证数据的容错性,可以在创建主题时指定副本数量,但不能操作broker数量,副本具有以下特点:
- 副本因子(replication-factor):创建主题时指定,决定副本数量(不能超过节点数量),以分区作为单元备份,
- kafka中删除不会立即进行;
- 副本之间会选举一个leader,leader和follower之间会同步备份,生产者和消费者只和leader交互;
- 副本的选举是通过controller完成的,controller只有某一台kafka会运行;
producer
生产者用于向kafka发送数据,生产者可以是flume、web等源;
消费者(组)
用于从kafka消费数据,一般消费者可以有storm、spark等计算框架,还可以为一些消费者指定消费者组,消费者组,即一个或者多个consumer放在同一个组中,消费者组具有以下特点:
- 具有组间共享、组内竞争的特点;
- 组间共享:每个组都可以获取相同的数据;
- 组内竞争:同一个组内的成员会竞争同一个数据,最终只有一个能获取;
- 一个分区对应组内一个消费者,一个消费者可以对应多个分区(即当组内消费者数大于分区数时,多余的消费者会空闲,当分区数大于组内消费者数时一个消费者可能会对应多个分区);
- 在一个分组内创建消费者:
./bin/kafka-console-consumer.sh --bootstrap-server hadoop01:9092 --topic xxx --from-beginning --consumer.config config/consumer.properties
- 消费者组对应的offset文件为:
Math.abs(groupID.hashCode()) % 50
;
三种语义
kafka生产者产生数据的流程如下:
- 生产者产生数据后,会访问zookeeper,找到副本leader的brokerid;
- 生产者根据brokerid发送数据给leader;
- leader将操作记录写到log中;
- follower通过RPC访问leader,保证数据一致性,完成后follower向leader返回ack;
- leader收到所有ISR的ack后,提交offset,并向生产者返回ack;
ISR机制:若同步数据过程中,返回了ack的副本节点会放到ISR队列,这个队列是用于选举副本leader的;
kafka的三种语义:
- 至少一次:数据一定不会丢,但可能有重复数据(0.11版本之前的默认值);
- 至多一次:数据一定不会重复,但可能会丢失;
- 精确一次:数据一定不会丢且不会重复,方法是在每条数据中添加全局递增id,收到数据后会和已有的最新的id比较,若id小于等于最新id则舍弃该数据,且无论如何都返回ack(设置
enable.idempotence=true
即可开启);
同样,消费者端也有类似的三种语义;
产生原因:
- 重复消费:当数据已经被处理,然后自动提交offset时,消费者出现故障或者有新消费者加入组导致再均衡,这时候offset提交失败,使得这一批已经处理的数据信息没有记录,导致后续会重复消费一次;
- 丢失数据:如果业务处理时间较长,这时业务还未处理完成,offset已经提交了,但是在后续的消息处理中发生错误,导致数据未正常消费,使得后续消费者将不在消费这批数据,导致这批数据丢失;
至少一次(重复消费)实现:关闭自动提交(enable.auto.commit=false
),在业务处理完成后手动提交offset;
至多一次(丢失数据)实现:关闭自动提交,在处理业务前手动提交offset;
精确一次实现:
- 数据有状态或数据容器具备幂等性:可以根据数据信息判断是否重复消费,这时可以设置为至少一次,在消费逻辑中判断数据重复,以达到幂等效果;
- 数据无状态:利用关系型数据库存储offset,主要思路为:利用seek方法指定offset消费,在启动消费者时,去数据库中查询offset信息,如果第一次启动则从0开始,利用关系型数据库的事务特性,将业务处理与offset记录操作放在同一个事务中进行;
offset与消费机制
Consumer从broker读取消息后,可以选择commit,该操作会保存在partition的offset中,下次再读会从下一条开始读取,从而保证不被重复消费,offset的主要特点有:
- 查看offset:到zookeeper的kafka相关目录可以找到(/consumers/g1/offsets/t1/0);
- 在sparkStreaming中加入的消费者组offset默认最大(即不消费加入以前的数据),通过kafka命令方式加入的消费者组offset默认为0;
kafka消息的默认保留策略是:要么保留一定时间要么保留消息达到一定大小,任意满足一个条件,该消息就会被删除;
创建消费者组相关命令有:
- 创建分组:
./bin/kafka-console-consumer.sh --bootstrap-server hadoop01:9092 --topic xxx --from-beginning --new-consumer
; - 获取分组列表名称:
./bin/kafka-consumer-groups.sh --bootstrap-server hadoop01:9092 --list --new-consumer
;
索引机制
- kafka每次接收数据都会持久化到本地磁盘上,在partition下的log文件中;
- log文件大小默认1G,数据超过log文件大小后会产生新的log文件;
- log文件会以偏移量命名;
- 每个log文件在同一目录下都有对应的index文件,也就是log的索引文件;
- 索引文件是一个稀疏索引,即从log随机选一些偏移量记录索引,类似于跳表;
- 查询时会先在索引中查找到数据文件的范围,然后在log中的对应范围进行二分查找;
- 索引文件会被映射到内存中;
kafka HA
- 同一个partition可能会有多个replica,这些replica之间会选出一个leader与producer和consumer交互;
- 当partition对应的leader宕机后会从follower中的ISR队列中选举出新的leader;
- 一般只有ISR中的成员才能成为leader,但是当所有replica都不工作时,则会选举第一个复活的replica作为leader;
- kafka通过Controller选举leader;
Java连接kafka
依赖
方式一:
- 添加依赖:
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
方式二:
- 将kafka安装目录的libs目录下的所有jar包引入即可;
创建topic
@Test
public void create_topic() {
Properties props = new Properties();
// 多个用逗号隔开,下面的示例代码也是一样
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.48.101:9092");
AdminClient client = KafkaAdminClient.create(props);
client.createTopics(Arrays.asList(new NewTopic("test01", 1, (short) 1),
new NewTopic("test02", 1, (short) 1)));
client.close();
}
删除topic
@Test
public void delete_topic() {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.48.101:9092");
AdminClient client = KafkaAdminClient.create(props);
client.deleteTopics(Arrays.asList("test01", "test02"));
client.close();
}
生产者
@Test
public void producer() {
Properties props = new Properties();
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.IntegerSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.48.101:9092");
// 添加数据
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 50; i++) {
ProducerRecord<String, String> message = new ProducerRecord<>("test01", "hello-" + i);
producer.send(message);
}
producer.close();
}
消费者(组)
@Test
public void consumer(){
Properties props = new Properties();
props.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.48.101:9092");
props.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "test");
props.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
props.setProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
props.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
String topic = "test01";
// 下面这段消费者利用了seek方法实现了从头开始消费
List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
List<TopicPartition> topicPartitions = new ArrayList<>(partitionInfos.size());
for (PartitionInfo partitionInfo : partitionInfos) {
topicPartitions.add(new TopicPartition(topic, partitionInfo.partition()));
}
consumer.assign(topicPartitions);
Map<TopicPartition, Long> offsets = consumer.beginningOffsets(topicPartitions);
for (TopicPartition partition : topicPartitions) {
Long offset = offsets.get(partition);
System.out.println(partition + "===>" + offset);
consumer.seek(partition, offset);
}
// 这行就是普通的消费者,不能和上面的一起用
// consumer.subscribe(Arrays.asList(topic));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}