kafka本身就是LinkIn公司开发用于日志系统的,所以其文件叫做log
-
点对对与发布订阅的区别
1.1 点对点模式,生产者发送一条消息到queue,只有一个消费者能收到。
1.2 发布订阅模式
发布者发送到topic的消息,只有订阅了topic的订阅者才会收到消息
rabbitMQ中实现发布订阅模式 当RabbitMQ需要支持多订阅时,发布者发送的消息通过路由同时写到多个Queue,不同订阅组消费此消息。
kafka发布订阅模式
Kafka只支持r息持久化,消费端为拉模型,消费状态和订阅关系由客户端端负责维护,消息消费完后不会立即删除,会保留历史消息。因此支持多订阅时,消息只会存储一份就可以了。
-
kafka背景介绍
kafka是最初由Linkedin公司开发,使用Scala语言编写,Kafka是一个分布式、分区的、多副本的、多订阅者的日志系统(分布式MQ系统),可以用于web/nginx日志,搜索日志,监控日志,访问日志等等。
kafka目前支持多种客户端语言:java,python,c++,php等等。 -
kafka高吞吐量的设计
数据磁盘持久化:消息不在内存中cache,直接写入到磁盘,充分利用磁盘的顺序读写性能。
zero-copy:减少IO操作步骤。
支持数据批量发送和拉取。
支持数据压缩。
Topic划分为多个partition,提高并行处理能力。 -
kafka信息存储
Kafka中的Message是以topic为基本单位的,不同的topic之间是相互独立的。每个topic又可以分成几个不同的partition(在创建topic时指定的),每个partition存储一部分Message。关系如下图partition是以文件的形式存储在文件系统中,比如,创建了一个名为t101的topic,其有5个partition,那么在Kafka的数据目录中(log.dirs指定的)中就有这样4个目录: t101-0,t101-1,t101-2,t101-3 ,其命名规则为<topic_name>-<partition_id>,里面存储的分别就是这4个partition的数据。 创建命令如下:
新数据是添加在文件末尾,不论文件数据文件有多大,这个操作永远都是O(1)的。
bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 4 --topic t101
查找某个offset的Message是顺序查找的。因此,如果数据文件很大的话,查找的效率就低。
为解决查找效率低的问题,kafka采用分段和索引
4.1 数据文件分段
比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。
4.2 为数据文件建索引
数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。
索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分,分别为相对offset和position。
#相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20=5。存储相对offset可减少索引文件占用的空间 。
#position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。
index文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。
4.3 查找message原理图
1)首先是用二分查找确定它是在哪个LogSegment中,自然是在第一个Segment中。 2)打开这个Segment的index文件,也是用二分查找找到offset小于或者等于指定offset的索引条目中最大的那个offset。自然offset为6的那个索引是我们要找的,通过索引文件我们知道offset为6的Message在数据文件中的位置为9807。
3)打开数据文件,从位置为9807的那个地方开始顺序扫描直到找到offset为7的那条Message。
Kafka的Message存储采用了分区(partition),分段(LogSegment)和稀疏索引这几个手段来达到了高效性
- zookeeper在kafka中的作用
其中5.5、5.6、5.7的是老版本的设计方式,新的版本偏移量已经不在存在zookeeper中。
5.1 管理broker集群
Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来。
在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:
/brokers/ids
每个broker启动的时候都会在zookeeper上进行注册。
Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去。其中,Broker创建的节点类型是临时节点,一旦Broker宕机,则对应的临时节点也会被自动删除。
5.2 管理topic信息
在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:
/borkers/topics
Kafka中每个Topic都会以/brokers/topics/[topic]的形式被记录,Broker服务器启动后,会到对应Topic节点(/brokers/topics)上注册自己的Brokerid并写入针对该Topic的分区总数,同样,这个分区节点也是临时节点。
5.3 生产者负载均衡
由于同一个Topic消息会被分区并将其分布在多个Broker上,因此,生产者需要将消息合理地发送到这些分布式的Broker上,那么如何实现生产者的负载均衡,Kafka支持传统的四层负载均衡,也支持Zookeeper方式实现负载均衡。
(1) 四层负载均衡,根据生产者的IP地址和端口来为其确定一个相关联的Broker。通常,一个生产者只会对应单个Broker,然后该生产者产生的消息都发往该Broker。这种方式逻辑简单,每个生产者不需要同其他系统建立额外的TCP连接,只需要和Broker维护单个TCP连接即可。但是,其无法做到真正的负载均衡,因为实际系统中的每个生产者产生的消息量及每个Broker的消息存储量都是不一样的,如果有些生产者产生的消息远多于其他生产者的话,那么会导致不同的Broker接收到的消息总数差异巨大,同时,生产者也无法实时感知到Broker的新增和删除。
(2) 使用Zookeeper进行负载均衡,由于每个Broker启动时,都会完成Broker注册过程,生产者会通过该节点的变化来动态地感知到Broker服务器列表的变更,实现动态的负载均衡机制。
Kafka的生产者会对ZooKeeper上的“Broker的新增与减少”、“Topic的新增和减少”和“Broker和Topic关联关系的变化”等事件注册Watcher监听
通过ZooKeeper的Watcher通知能够让生产者动态的获取Broker和Topic的变化情况。
5.4 消费者负载均衡
与生产者类似,Kafka中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的Broker服务器上接收消息,每个消费者分组包含若干消费者,每条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定的Topic下面的消息。
5.5 zookeeper记录分区与消费者的关系
对于每个消费者组 (Consumer Group),Kafka都会为其分配一个全局唯一的Group ID,Group内部的所有消费者共享该ID。
同时,Kafka为每个消费者分配一个Consumer ID,通常采用"Hostname:UUID"形式表示。
在Kafka中,规定了每个消息分区 只能被同组的一个消费者进行消费,因此,需要在 Zookeeper 上记录 消息分区 与 Consumer 之间的关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上,例如:
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
5.6 消息消费进度Offset记录
在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度Offset记录到Zookeeper上,以便在该消费者进行重启或者其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。Offset在Zookeeper中由一个专门节点进行记录,其节点路径为:
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]
节点内容就是Offset的值
消费者服务器在初始化启动时加入消费者分组的步骤如下
注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。
对消费者分组中的消费者的变化注册监听。每个消费者都需要关注所属消费者分组中其他消费者服务器的 变化情况, 即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。
对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。
进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个消费者消费而进行消费者与消息 分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。
-
新版本消费位移存储
老版本的消费位移信息是存储的zookeeper 中的, 但是zookeeper 并不适合频繁的写入查询操作, 所以在新版本的中消费位移信息存放在了__consumer_offsets内置topic中。
可以利用如下命令创建consumers group信息,创建group consumer_offsets_t105
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic t105 --from-beginning --group consumer_offsets_t105查询consumer_offsets_t105 在 __consumer_offsets topic 中存放的位移信息__consumer_offsets 默认分区50。通过如下公式即可获取:Math.abs("consumer_offsets_t105".hashCode()) % 50。
可以计算得位移偏移量是存在partitionId等于44分区上。
使用命令可以查询出消息的偏移信息。
bin/kafka-console-consumer.sh --topic __consumer_offsets --partition 44 --bootstrap-server localhost:9092 --formatter "kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter" -- from-beginning
可以根据命令查询消息的发布情况
bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic t105 --time -1
可以看出消息偏移与消息的发布的数据基本一致。
-
kafka java调用
7.1 生产者import java.util.Properties; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; public class ProducerDemo { public static void main(String[] args){ Properties properties = new Properties(); properties.put("bootstrap.servers", "localhost:9092"); properties.put("acks", "all"); properties.put("retries", 0); properties.put("batch.size", 16384); properties.put("linger.ms", 1); properties.put("buffer.memory", 33554432); properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = null; try { producer = new KafkaProducer<String, String>(properties); for (int i = 0; i < 100; i++) { String msg = "Message " + i; producer.send(new ProducerRecord<String, String>("t105", msg)); System.out.println("Sent:" + msg); } } catch (Exception e) { e.printStackTrace(); } finally { producer.close(); } } } 复制代码
7.2 消费者
复制代码
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
public class ConsumerDemo {
public static void main(String[] args){
Properties properties = new Properties();
properties.put("bootstrap.servers", "localhost:9092");
properties.put("group.id", "group4");
properties.put("enable.auto.commit", "true");
properties.put("auto.commit.interval.ms", "1000");
properties.put("auto.offset.reset", "none");
properties.put("session.timeout.ms", "30000");
properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
kafkaConsumer.subscribe(Arrays.asList("t105"));
while (true) {
ConsumerRecords<String, String> records = kafkaConsumer.poll( Duration.ofMillis(100)
);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("partition = "+ record.partition() +" offset = %d, value = %s", record.offset(), record.value());
System.out.println();
}
}
}
}
参数auto.offset.reset有三个值latest, earliest, none。<br>
earliest: automatically reset the offset to the earliest offset<br>
latest: automatically reset the offset to the latest offset<br>
none: hrow exception to the consumer if no previous offset is found for the consumer's group
复制代码
- kafka零拷贝
传统的IO机制
这一过程实际上发生了四次数据拷贝。首先通过系统调用将文件数据读入到内核态 Buffer(DMA 拷贝),然后应用程序将内存态 Buffer 数据读入到用户态 Buffer(CPU 拷贝),接着用户程序通过 Socket 发送数据时将用户态 Buffer 数据拷贝到内核态 Buffer(CPU 拷贝),最后通过 DMA 拷贝将数据拷贝到 NIC Buffer。同时,还伴随着四次上下文切换。
- Kafka的leader选举机制
只有leader 负责读写,follower只负责备份,如果leader宕机的话,Kafka动态维护了一个同步状态的副本的集合(a set of in-sync replicas),简称ISR,ISR中有f+1个节点,就可以允许在f个节 点down掉的情况下不会丢失消息并正常提供服。ISR的成员是动态的,如果一个节点被淘汰了,当它重新达到“同步中”的状态时,他可以重新加入ISR。因此如果leader宕了,直接从ISR中选择一个follower就行。
此时有两种方法可选,一种是等待ISR集合中的副本复活,一种是选择任何一个立即可用的副本,而这个副本不一定是在ISR集合中。这两种方法各有利弊,实际生产中按需选择。
如果要等待ISR副本复活,虽然可以保证一致性,但可能需要很长时间。而如果选择立即可用的副本,则很可能该副本并不一致。
- kafka Stream
一个流处理器从它所在的拓扑上游接收数据,通过Kafka Streams提供的流处理的基本方法, 如map()、filter()、join()以及聚合等方法,对数据进行处理,然后将处理之后的一个或者多个输出结果发送给下游流处理器。
kafka的流实例参考
juejin.im/post/5cd50a…
- kafka压缩
创建一个topic bin/kafka-topics.sh --create --bootstrap-server localhost:9092 --replication-factor 3 --partitions 1 --topic t106
describe topics bin/kafka-topics.sh --describe --bootstrap-server localhost:9092 --topic t106
往集群中发消息 bin/kafka-console-producer.sh --broker-list localhost:9092 --topic t106
集群消费消息 bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --from-beginning --topic t106
验证消息是否生产成功 bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic t105 --time -1
—————————
创建消费组消费消息 bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic t105 --from-beginning --group consumer_offsets_t105
查询偏移量消息 bin/kafka-console-consumer.sh --topic __consumer_offsets --partition 44 --bootstrap-server localhost:9092 --formatter "kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter" -- from-beginning
//通过config文件访问客户端 bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic t105 --group consumer_offsets_t105 --consumer.config config/consumer.properties