Kafka初识
Kafka是什么
Kafka是最初由LinkedIn公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统。
设计理念
- 低延迟:持久化消息、消费消息时间复杂度都为O(1)
- 高吞吐:普通机器也可以实现每秒发送10W条消息
- 水平扩展:broker、producer、consumer都支持在线水平扩展,
- 顺序性:每个partition内的消息被顺序消费
基本概念
- Broker: Kafka集群包含若干个服务器,每个服务器视为一个broker
- Topic: 发送到Kafka的消息都有一个唯一的类别,称为topic
- Partition: 每个Topic会被分成若干个partition,每个partition对应一个文件夹,其存储了实际的消息,每条消息只会被存到一个partition中
- Repication: 副本,每个分区都有若干副本,当分区leader宕机时,会从分区副本中选择一个成为新的leader
- Producer: 消息的生产者
- Consumer: 消息的消费者,一个消费者可以订阅若干个topic以便消费其中的消息
- Controller: 也是一个broker,除了具有基本broker的功能外,还负责分区选举和故障切换,controller的工作机制会在下一章介绍
- ISR(In Sync Replica): 每个分区包含一个ISR列表,里面都是和master数据保持一致的副本(包括master)
Kafka架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9JyWxlud-1617009556500)(https://tech-proxy.bytedance.net/tos/images/1616471663079_ba5d8a77b5ab14ac4b248114edb8fb6d.png)]
- producer直接和broker通信,broker中存有元数据信息(有多少个broker,每个topic有多少分区,每个分区的master是谁),这样就避免了producer和zk通信
- 每个broker都和zk通信
- consumer也直接和broker通信
Topic逻辑结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vs3PWGrd-1617009556503)(https://tech-proxy.bytedance.net/tos/images/1614600427026_e0ebccba6d7fc22b7c66e1d317ea0640.png)]
- 一个topic可以包含多个partition,每个partition对应一个文件夹,而每个partition又可以包含多个segment,每个segment对应一个文件
- 生产者以append-only的方式往partition写数据(顺序写盘的速率 ≈ 随机读内存速率),消费者消费时从低位开始消费,消息符合FIFO
- 一个topic可以配置多个partition,可以存储任意多的数据
- 每个partition都可以设置消息有效期,到期后,消息无论是否被消费都会被清除
- 每条消息被发送到分区后都会指定一个offset,该offset在分区中递增
ZooKeeper简介
ZooKeeper是一个轻量级的分布式协调服务,目前kafka是强依赖于zk进行元数据管理、配置管理、集群管理等功能的。
Kafka中zooKeeper的存储结构如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CFCkoMXf-1617009556504)(https://tech-proxy.bytedance.net/tos/images/1616665891208_bad1b0bfa1b20720fc20fe66a15e5ddc.png)]
生产者 Producer
Kafka消息结构
public class ProducerRecord<K, V> {
private final String topic;//消息所属topic
private final Integer partition;//消息所属分区
private final Headers headers;//消息头
private final K key;//如果partition没指定且key存在,kafka会根据key来计算partition
private final V value;//消息体
private final Long timestamp;//时间戳
}
消息发送方式
- 同步发送:发送消息后producer会一直阻塞等待broker的响应,如果发生可重试错误Kafka会自动重试,其底层也是调的异步发送。
- 异步发送:异步发送消息,producer只发送消息,并不关心返回值,可以注册回调函数对异常情况进行处理。
发送确认
acks参数指定了一条消息被多少个分区副本同步后才能被认为写入成功,其取值情况如下:
- acks=0,这种情况生产者不等待broker响应,不保证消息被同步到broker。这种模式下吞吐量最大,但丢消息的可能性最大。
- acks=1,只要分区的master成功写入这条消息就会被认为写入成功。这种情况下不保证消息被同步到其他副本,同时也存在丢消息的可能性。
- acks=all,必须等待在isr中的所有分区副本都成功写入这条消息才会被认为写入成功。这种情况下吞吐量最低,单丢消息的概率也最低。
分区器
在每条Kafka的消息中都包含消息所属分区,如果在发送时没有指定分区信息和key,kafka会使用默认的轮询(Round Robin)策略来得到分区信息。当然,我们也可以自定义分区策略,比如topic为test的消息统一放到最后一个分区。
public class CustomerParatitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
//这里通过主题名称得到该主题所有的分区信息
List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
int numPartition = partitionInfos.size();
if (numPartition <= 1) {
return 0;
}
//后面进行逻辑判断返回哪个分区, 这里比如 key为test的 选择放入最后一个分区
if (key.toString().equals("test")) {
return partitionInfos.size() - 1;
}
//返回源码中默认的 按照原地址散列
return (Math.abs(Utils.murmur2(keyBytes)) % (numPartition - 1));
}
}
消费者 Consumer
消费者组 Consumer Group
每个消费者都从属于一个消费者组,一个消费者组中的所有消费者订阅的主题相同并且offset共享,每个消费者消费主题中一部分分区的消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OaZJpgZt-1617009556506)(https://tech-proxy.bytedance.net/tos/images/1615357489200_5a955e4197039f43ba78060282193a9e.png)]
注意:这里consumer4是消费不到消息的,为什么?
- 保证分区内的消息被顺序消费
- 实现起来更为简单,不用考虑多个消费者同时消费同一个分区时的数据竞争问题
再均衡Rebalance
Consumer在启动时就会被指定消费的分区(由消费者协调器实现,下一篇介绍),如果消费者组中消费者数量发生变化就会触发分区重分配,会重新计算每个消费者所消费的分区,这就是再均衡。在这期间,消费者组对外不可用,不会消费消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ajx6BSgF-1617009556509)(https://tech-proxy.bytedance.net/tos/images/1615722729325_a4d30535ef1841cc2c43e8f07af5ab63.png)]
Offset管理
Consumer通过提交offset来记录消费进度,kafka将消费者提交的offset存在一个叫做_consumer_offsets的topic中,所以consumer提交偏移量实际上就是往这个topic发消息,kafka支持3种提交offset的方式。
- 自动提交
设置enable.auto.commit=true,kafka会每隔一段时间(默认5s)检查是否提交过偏移量,没有则会自动提交,该过程发生在拉取消息poll()中。
- 同步提交
设置enable.auto.commit=false,调用commitSync()可以手动同步提交offset,该方法会一直阻塞直到成功/异常/超时。 - 异步提交
设置enable.auto.commit=false,调用commitAsync()可以手动异步提交offset,该方法不会阻塞,但失败时不会重试。
为什么异步提交失败时不会重试?
第一次调用commitAsync()提交的offset为10,第二次调用commitAsync()提交的offset为20,此时第一次由于网络原因导致超时,而第二次提交成功。如果此时第一次请求重试则会覆盖第二次的提交
在提交offset之前发生再均衡,以上3种情况都会造成消息的重复消费,可以通过实现再均衡监听器解决。
/**
* 再均衡监听器
*/
private static class HandleRebalance implements ConsumerRebalanceListener{
/**
* 方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里读取了。
* @param partitions
*/
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
System.out.println("Lost partitions in rebalance.Committing current offsets:" + currentOffsets);
consumer.commitSync(currentOffsets);
}
/**
* 方法会在重新分配分区之后和消费者开始读取消息之前被调用。
* @param partitions
*/
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
}
}
Kafka vs RocketMQ
性能对比
下图为阿里官方对Kafka和RocketMQ在不同个数的topic情况下得到的压测报告,可以看出在topic个数较小时Kafka性能是优于RocketMQ的,但随着topic个数增加,Kafka性能大打折扣,远远不如RocketMQ。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fJkLcR8k-1617009556511)(https://tech-proxy.bytedance.net/tos/images/1615345728700_55f226a0ea67c061d81d3b905b382980.png)]
思考:为什么在少量topic时Kafka性能优于RocketMQ,而topic个数多时结果又相反呢?
- topic较少时:由于Kafka在发送端做了批处理,producer会将多条消息封装成一个批次,然后发给broker。
- topic较多时:Kafka的文件模型为一个partition对应一个文件,而RocketMQ是所有queue共享一个CommitLog。所以在多topic下,Kafka消息的分散落盘策略会导致磁盘IO竞争成为瓶颈,而RocketMQ则是顺序写磁盘,速度很快。