文章目录
- 一、什么是Kafka
- 二、Kafka的基本使用
- 1. 单机环境搭建及命令行的基本使用
- 2. 集群搭建
- 3. Java API的基本使用
- 三、Kafka原理浅析
- 1. topic和partition的存储
- 2. 消息分段及索引查找原理
- 3. 日志清理策略
- 4. 副本高可用机制
- 5. 数据同步原理
- 6. 消息分发策略
- 7. 消费原理
一、什么是Kafka
Kafka也是一款消息队列中间件,与ActiveMQ和RabbitMQ不同的是,它不是基于JMS和AMQP规范开发的,而是提供了类似JMS的特性,同时Kafka比较重量级,天然支持集群分布式搭建以及数据分片备份,由Scala和Java编写,因其高性能和高吞吐量的特点被广泛用于大数据的传输场景。简单而言,Kafka就是一款适用于大数据场景下的消息队列。
如图所示,Kafka是基于发布订阅模型进行消息传输的,在发送接收消息前首先需要为每一个producer和consumer指定topic主题,即关注的消息类型,这样才能进行消息传输,而所有的topic都存储在服务器broker集群上。有一个基本的认识后,下面我们就来看看如何使用Kafka。
二、Kafka的基本使用
1. 单机环境搭建及命令行的基本使用
安装Kafka非常简单,这里基于centos7,Kafka2.3.0版本演示。将下载好的压缩包解压后,首先启动本地的Zookeeper服务(因为Kafka是要依赖Zookeeper的,也可以直接使用Kafka自带的Zookeeper,在bin目录下执行sh zookeeper-server-start.sh …/config/zookeeper.properties命令即可);然后启动修改Kafka的配置文件server.properties:
# 监听器,告诉外部连接需要使用的协议
listeners=PLAINTEXT://192.168.0.109:9092
# zookeeper服务器,集群以逗号分隔
zookeeper.connect=192.168.0.109:2181,192.168.0.106:2181,192.168.0.108:2181
使用以下命令启动服务即可:
# daemon表示后台启动
sh kafka-server-start.sh [-daemon] ../config/server.properties
这样Kafka的单机环境就搭建好了,接着我们就可以使用以下命令来操作Kafka:
# 创建test topic,replication表示要创建的副本集个数,不能大于集群服务器的数量;partitions表示分区数
sh kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
# 删除topic,很多博客说需要配置delete.topic.enable=true才能删除,其实最新版本默认值就是true,不用配置,所以最新的配置还是要以官网为准
sh kafka-topics.sh --delete --zookeeper localhost:2181 --topic test
# 查看topic列表
sh kafka-topics.sh --list --zookeeper localhost:2181
# 查看topic属性
sh kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
# 发送消息
sh kafka-console-producer.sh --broker-list localhost:9092 --topic test
# 消费消息,from-beginning表示从队列第一条开始消费消息,否则就是从启动消费者后接收到的第一条消息开始消费
sh kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
以上就是Kafka的基础使用命令,这里主要来看看查看topic属性命令,其它的这里就不过多演示了。当查看topic属性时,屏幕会打印以下信息:
- 第一行
- Topic:topic名称
- PartitionCount:分区数
- ReplicationFactor:副本数
- Configs:其它配置
- 第二行,从第二行开始,每一行表示一个分区
- Partition:分区编号
- Leader:该分区的Leader节点的broker.id
- Replicas:该分区存在的副本集的broker.id
- Isr:表示集群中有效节点的broker.id
2. 集群搭建
从上文我们可以看出搭建Kafka的单机环境是非常简单的,而作为一款天然支持分布式集群的消息队列,搭建其集群环境也非常简单,首先准备3台虚拟机,然后将配置每台机器的Kafka的server.properties文件:
# 搭建集群时需要保证每台机器的id都是唯一的,注意若要更换这个id,不仅仅改这里配置文件就可以了,还需要删除掉zookeeper上的brokers节点,否则会导致消费者接收不到消息
broker.id=1
搭建集群只需要修改这个就可以了,其它的配置请参见官网。这样我们就搭建好了一个具有3台broker的集群,也非常简单。
3. Java API的基本使用
使用Java API我们需要引入下面的依赖,版本可自行选择,不过最好和服务器版本保持一致:
org.apache.kafka
kafka-clients
2.3.0
然后分别创建Producer和Consumer:
public class AsyncProducer extends Thread {
private final KafkaProducer<Integer, String> producer;
private final String topic;
private final boolean isAsync;
public AsyncProducer(String topic, boolean isAsync) {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.0.109:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producerDemo");
/*
0:表示 producer 不需要等待 broker 的消息确认。这个选项时延最小但同时风险最大
(因为当 server 宕机时,数据将会丢失)。
1:表示 producer 只需要获得 kafka 集群中的 leader 节点确认即可,这个选择时延
较小同时确保了 leader 节点确认接收成功。
-1:需要 ISR 中所有的 Replica 给予接收确认,速度最慢,安全性最高,但是由于
ISR 可能会缩小到仅包含一个 Replica,所以设置参数为 all 并不能一定避免数据丢失
*/
properties.put(ProducerConfig.ACKS_CONFIG, "-1");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.IntegerSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<Integer, String>(properties);
this.topic = topic;
this.isAsync = isAsync;
}
@Override
public void run() {
int n = 0;
while (n < 100) {
String message = "message_" + n;
System.out.println("send: " + message);
if (isAsync) {
producer.send(new ProducerRecord<Integer, String>(topic, message), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (metadata != null) {
System.out.println("async-offset:" + metadata.offset() +
" -> partition" + metadata.partition());
}
}
});
} else {
try {
RecordMetadata recordMetadata=producer.
send(new ProducerRecord<Integer, String>(topic,message)).get();
System.out.println("sync-offset:"+recordMetadata.offset()+
" -> partition:"+recordMetadata.partition());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
n++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new AsyncProducer("test", false).start();
}
}
这里模拟向broker发送100条消息,可选择异步发送还是同步发送,同步发送时需要得到broker的确认才会继续发送下一条消息,方法很简单,主要来看看配置的含义:
- BOOTSTRAP_SERVERS_CONFIG:Kafka服务器地址
- CLIENT_ID_CONFIG:client端的标识,可做权限控制
- ACKS_CONFIG:确认模式
- KEY_SERIALIZER_CLASS_CONFIG:key序列化方式
- VALUE_SERIALIZER_CLASS_CONFIG:value序列化方式
- BATCH_SIZE_CONFIG:当一批消息大小达到指定的 batch.size 的时候会统一发送
- LINGER_MS_CONFIG:批量发送的间隔时间
- MAX_REQUEST_SIZE_CONFIG:请求的数据的最大字节数,为了防止较大的数据包影响到吞吐量,默认值为 1MB
public class Consumer extends Thread {
private final KafkaConsumer consumer;
public Consumer(String topic) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.0.109:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"consumerDemo");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.IntegerDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringDeserializer");
/*
latest:新的消费者将会从其他消费者最后消费的offset 处开始消费 Topic 下的消息
earliest:新的消费者会从该 Topic 最早的消息开始消费
none:新的消费者加入以后,由于之前不存在offset,则会直接抛出异常。
*/
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
consumer = new KafkaConsumer(properties);
consumer.subscribe(Collections.singletonList(topic));
}
@Override
public void run() {
while (true) {
ConsumerRecords<Integer, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<Integer, String> record : records) {
System.out.println("receive: " + record.value());
consumer.commitAsync(); // 未开启自动提交时需要手动提交
}
}
}
public static void main(String[] args) {
new Consumer("test").start();
}
}
配置含义如下:
- GROUP_ID_CONFIG:消费分组,组内是竞争,而组外则是非竞争的,即对于一个topic的一条消息来说,多个消费组可以同时消费这条消息,而同一个消费组中只能有一个消费者消费该条消息。
- ENABLE_AUTO_COMMIT_CONFIG:是否自动提交,为true时周期性提交消息,只有消息被提交后,该条消息才不会再次被消费。
- AUTO_COMMIT_INTERVAL_MS_CONFIG:自动提交消息的时间间隔
- KEY_DESERIALIZER_CLASS_CONFIG:key反序列化方式,需要和producer的序列化对应
- VALUE_DESERIALIZER_CLASS_CONFIG:value反序列化方式,需要和producer的序列化对应
- MAX_POLL_RECORDS_CONFIG:每次调用 poll 返回的最大消息数
以上就是Kafka的基本使用,这里只是演示了一些基本的配置,更详细的配置及说明可直接参考ProducerConfig和ConsumerConfig类,每个配置都有详细的说明文档。
三、Kafka原理浅析
1. topic和partition的存储
Kafka默认会持久化存储消息,存储位置在server.properties文件中可配置,默认是以下位置:
log.dirs=/tmp/kafka-logs
使用以下命令创建topic,会生成3个分区:
sh kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 3 --topic test
这三个分区会在三台服务器的上述目录中分别创建一个文件夹,名称格式为“topic名称-分区编号”,如下:
test-0
test-1
test-2
在文件夹中就是真正存储的消息文件和消息索引文件:
其中index和timeindex是都是索引文件,只是索引方式不同,log是存储数据的文件,而leader-epoch-checkpoint中则是保存了每一任leader的信息。
有关文件内容稍后再详细分析,我们先来考虑一个问题,当创建多个分区时,每个分区如何均匀分配到各个节点的呢?我想你很快就能想到取模算法,没错,kafka也正是使用这种算法,将第n个partition和m个broker按照n % m分配。
2. 消息分段及索引查找原理
为防止消息文件过大导致查询性能降低,Kafka提供了消息分段功能,只需要配置以下参数即可修改分段大小,默认是1GB:
# 单位时字节(b)
log.segment.bytes=1073741824
将其改为10240,然后往broker上发送200000条消息后我们可以看到以下效果:
这里我只截取了一个分区的部分内容,从上图可以看到消息被分段,文件大小基本上和我们设置的大小一样。另外,我们还可以看到每一个分段的文件名编号都不一样,该命名规则是承接上一个分段的最后一条消息的offset+1。
可以通过以下命令查看index文件内容:
sh kafka-dump-log.sh --files /tmp/kafka-logs/test-0/00000000000000000000.index
然后会输出以下内容
offset: 230 position: 0
offset: 268 position: 4350
offset: 441 position: 8426
接着再用以下命令查看log文件内容:
sh kafka-dump-log.sh --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log
截取部分内容如下:
baseOffset: 0 lastOffset: 230 count: 231 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 0 CreateTime: 1574344214051 size: 4350 magic: 2 compresscodec: NONE crc: 1996271569 isvalid: true
| offset: 0 CreateTime: 1574344213989 keysize: -1 valuesize: 9 sequence: -1 headerKeys: [] payload: message_2
.
.
.
baseOffset: 231 lastOffset: 268 count: 38 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 4350 CreateTime: 1574344214055 size: 745 magic: 2 compresscodec: NONE crc: 2614367386 isvalid: true
| offset: 231 CreateTime: 1574344214051 keysize: -1 valuesize: 11 sequence: -1 headerKeys: [] payload: message_695
.
.
.
| offset: 268 CreateTime: 1574344214055 keysize: -1 valuesize: 11 sequence: -1 headerKeys: [] payload: message_806
.
.
.
baseOffset: 422 lastOffset: 441 count: 20 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 8426 CreateTime: 1574344214077 size: 441 magic: 2 compresscodec: NONE crc: 3447448797 isvalid: true
| offset: 422 CreateTime: 1574344214074 keysize: -1 valuesize: 12 sequence: -1 headerKeys: [] payload: message_1268
.
.
.
| offset: 441 CreateTime: 1574344214077 keysize: -1 valuesize: 12 sequence: -1 headerKeys: [] payload: message_1325
光看这两个文件内容可能会比较晕,我画了下面的图帮助理解:
当客户端查询查询offset=232的消息时,首先判断在哪个segment,这里显然是到00000000000000000000.index文件中去查找(第二个片段是从442开始的),然后找到大于232且最接近232的offset,即268,得到对应的position为4350,通过这个position即可定位log文件中消息的位置(offset为231-268的消息position都为4350),最后挨个判断消息的offset==232即为我们要查找的消息。
以上就是Kafka消息分段即索引查找的原理,通过这样的机制,一方面能够大大减少单个文件的大小,也就提高了索引查找的效率,另一方面还能提高日志清除的效率。那Kafka是有哪些清理日志的策略呢?
3. 日志清理策略
在Kafka中,有两个清理日志的方式:一个是基于时间,超期的日志会被清理;另一个是基于日志文件大小,当日志文件过大时,会删除最旧的消息。分别由log.retention.hours(默认7天)和log.retention.bytes配置控制。
4. 副本高可用机制
Kafka将数据分区存储后可以提高查询的效率,但同时也带来一个问题,任何一个节点挂掉就会导致数据丢失,所以Kafka又提供了一个副本机制,即在多个节点冗余存储数据,任何一个节点挂掉都还有副本分区提供服务,可以通过以下命令创建一个具有3个分区3个副本的topic:
sh kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 3 --topic test
我这里也刚好3台broker,这样在每个数据存储文件夹中就会保存所有的分区,只是同样会分为leader和follower分区,其中leader分区提供读写服务,而follower只负责从leader同步数据:
如上图,绿色的表示leader分区,当leader挂掉后,会从剩下的follower分区重新选出leader提供服务。另外,我们还可以在zookeeper上通过以下命令查看分区信息:
get /brokers/topics/test/partitions/0/state
{"controller_epoch":16,"leader":2,"version":1,"leader_epoch":0,"isr":[2,3,1]}
leader就代表当前分区(test-0)的leader所在broker.id,isr表示消息量和leader相差不多的且可用broker的id,可用还能理解,也就是副本之间能够相互通信,但消息量差不多是什么意思呢?因为是副本机制是在集群中各节点同步数据冗余存储,因此,数据肯定是存在不一致的(如果要强一致,那么就不存在高可用了),也就代表,副本最后一条消息的offset和leader最后一条消息的offset之间的差值不能超过一定的阈值才会进入isr中。这有什么用呢?
首先,我们发送消息时,可以指定一个参数ACK_CONFIG(这个参数在上面代码中解释过其作用),也就是当生产者如果需要所有副本给出响应的时候,其实并不是真正的所有,只是isr中的所有broker响应即可。如果不这样,当有一个follower怠机时就会导致整个消息队列的性能降低,而使用isr,当任何一个follower延迟超出一定阈值时,就会将其踢出isr集群,这样,就不需要等待故障的follower响应,提高了吞吐率。
其次,当leader怠机时,会优先从isr中选出leader,也就避免了消息丢失。不过极端情况下当某个分区所有副本都不可用时也会出现数据丢失的情况,Kafka对此提供了两种解决方案:
- 一是等待isr中的任何一个副本节点活过来作为leader
- 而是等待第一个活过来的副本(不一定是isr中的)作为leader
两者的区别很容易看出来,前者可能会导致不可用时间延长,且当isr中的所有节点永久无法恢复时,这个分区就无法使用了,数据也就丢失了;而后者明显也无法保证数据的不丢失,但是可用性却提高了。
5. 数据同步原理
上文提到了副本是通过集群中各节点同步复制数据实现的,那么这个过程是怎样的呢?这里需要先了解两个参数LEO(log end offset)和HW(high water),前者表示当前副本中最后一条消息的offset,后者表示consumer可消费消息的最大offset,leader和follower会各自维护自己的LEO和HW。有什么用呢?
首先生产者发送消息时,只会发送到leader节点上,记录消息并更新自己的LEO,然后isr中的follower会主动去leader上拉取消息并更新自己的LEO和HW,等到下一次拉取时,再去更新leader的HW,也就是说leader上的HW是isr集群中最小的LEO,这样就能避免leader服务器怠机时导致消息的丢失。
以上都是关于消息的存储,下面我们就来看看消息是如何分发和消费的。
6. 消息分发策略
消息发送到哪个分区是由什么决定的呢?在上文的生产者代码中,我们可以看到消息是可以包含key和value的,并且消息分发策略也就是按照key的hash值对分区数取模,不过key是可以为null的,因此,在key为null的情况下,消息会随机发送到一个分区上,这个分区编号每间隔一段时间会更换一次,间隔时间由参数metadata.max.age.ms指定,默认是5分钟。
7. 消费原理
消费者可以消费哪些消息?当多个消费者订阅同一个topic但不是同一个group时,都能接收到相同的消息;而当处于同一个group时,消息就不能被重复消费。另外
对于多个分区,同组中的消费者同样也有分区分配策略,即每个消费者能消费的分区,这个分区可以由我们自己指定,也可以由系统自己决定。
- 自定义
// 当自定义消费分区时不再需要订阅,直接通过以下方式指定即可
// consumer.subscribe(Collections.singletonList(topic));
TopicPartition topicPartition=new TopicPartition(topic,分区编号);
kafkaConsumer.assign(Arrays.asList(topicPartition));
- 系统分配
系统分配消费分区默认有两种方式:Rang和RoundRobin,由参数partition.assignment.strategy指定,默认是Rang。
- Rang:当使用Rang策略时,会按照分区编号顺序平均分配给各个消费者,比如有11个分区,3个消费者,那么C1就会消费0、1、2、3,C2就会消费4、5、6、7,C3就消费8、9、10。
- RoundRobin:这个就比较简单了,就是一次轮流分配到各个消费者身上去,同样上面的例子就会是下面这样的情况:C1-0、3、6、9,C2-1、4、7、10,C3-2、5、8
那什么时候会触发分区分配呢?有以下几种情况:
- 同一个consumer group有新的消费者加入时
- 消费者离开当前consumer group时
- topic新增分区时
当以上情况发生时,就会触发rebalance机制,但是谁来执行该机制并且由谁来管理consumer group呢?Kafka提供了一个coordinator角色进行协调管理,在rebalance之前,服务端会选出负载最小的broker作为coordinator,然后coordinator会选择一个consumer作为leader,并将组员信息和订阅信息发送过去,leader会完成分区分配并将其发送给coordinator,其它consumer则去同步这个信息即完成了rebalance。所以,Kafka是将consumer分区分配策略放到客户端执行的,也就是说我们可以自己实现分配策略。