kafka使用与设计原理

第一部分 kafka基础简介

kafka简介

kafka是apache开源的基于zookeeper协调的分布式消息系统,具有高吞吐率(可做到单机每秒几十万qps,基于磁盘进行存储,做到时间复杂度O(1) )、高性能、实时、高可靠等特点,可实时处理流式数据。最早由Linkedin公司用scala语言开发。

kafka是消息中间件的一种,消息中间件还有active mq, rocket mq等。

消息中间件的作用

当client调用server时,如果server响应慢,则会采用异步,这时需要引用消息系统。这时从client–>server,变成了client(producer) --> message queue --> server(consumer)。

使用消息中间件的模式

点对点

生产者发送消息到queue中,然后消费者从queue中取出消息并消费消息。
注意:

  • 消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。
  • queue可支持多个消费者,但对一个消息而言,只会被一个消费者消费,其他消费者消费不到。
发布/订阅:

消息生产者将消息发布到topic中,同时有多个消费者订阅该消息。不同于点对点,发布到topic中的消息会被所有订阅者订阅。

JMS(Java Message Service)

Java EE平台上关于消息中间件的技术规范,它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的生产、发送、接收消息的接口简化企业应用的开发。

备注:kafka不是完全按照JMS规范实现的,Active MQ是基于JMS做的,JMS规范更重一些。

使用message queue的好处

1)client与server解耦
2)数据冗余(确保高可用,例如可失败重试)
3)扩展性、灵活性(如多个生产者、多个消费者的数目增加和减少)
4)高并发时消峰(相当于通过异步来解决高并发)

kafka的术语

生产者
消费者
topic

kafka将消息分门别类,每一类的消息称之为topic(主题)

broker

即kafka的一台服务器,已发布的消息会保存到一组kafka服务器中,集群中的每个服务器都是一个broker(即代理)
消息:kafka集群中存储的消息是以topic为分类做记录的,每个消息(也叫record)有一个key, 一个value, 一个时间戳构成。

kafka能干什么?

1)发布、订阅消息(流),内置分区,副本和故障转移:有利于处理大规模消息以容错的方式存储消息(流),消息被持久化到本地磁盘。
2)流处理:可以持续获取输入topic的数据,进行加工处理,然后写入输出topic。(主要配合大数据来使用)

kafka常见的应用场景

1)日志收集:然后用统一接口服务开发给各种消费者。
2)消息系统:解耦生产者、消费者、缓存消息。
3)用户活动跟踪:收集埋点,然后订阅者消息埋点数据做实时监控分析;或装载到hadoop或数据仓库,做离线的分析与挖掘。
4)运营指标统计:用于运维监控,如收集系统指标和应用指标,做监控报警。
5)流式处理:结合spark streaming和storm。

kafka核心API

1)productor API: 发布消息到一个或多个topic
2)consumer API: 订阅一个或多个topic, 并处理消费到的消息
3)stream API: 充当一个流处理器,从1个或多个topic消费输入流,并生产一个输出流到1个或多个topic,有效将输入流转换到输出流。
4)connector API: 用于不断从源系统(如DB)或应用程序中拉取数据到kafka, 或从kafka提交数据到宿系统或应用程序。
5)AdminClient API: 允许管理和检测topic, broker及其他kafka对象。

kafka的拓扑结构

在这里插入图片描述

kafka下载与安装**

官方文档:http://kafka.apache.org/documentation/
下载地址:http://kafka.apache.org/downloads
当前kafka版本是2.0.1
下载后解压即可,kafka不用安装。
kafka默认在本地使用。如果要远程访问,需要配置config/server.properties

通过命令行来简单使用kafka hello world**

/Users/shipeng/Documents/kafka学习/kafka_2.11-2.0.1
Step1. 启动kafka前,要先启动kafka自带的zookeeper
bin/zookeeper-server-start.sh config/zookeeper.properties &
Step2. 启动kafka
bin/kafka-server-start.sh config/server.properties
Step3. 创建topic
./bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic my-topic
Step4. 查看已创建的topic
./bin/kafka-topics.sh --list --zookeeper localhost:2181
Step5. 发送消息
./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic my-topic
Step6. 接收消息
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --from-beginning
Step7. 停止kafka
./bin/kafka-server-stop.sh
Step8. 停止zk
./bin/zookeeper-server-stop.sh

通过java客户端来做kafka的生产者与消费者**

Step1. 添加maven依赖

<dependency> 
	<groupId>org.apache.kafka</groupId> 
	<artifactId>kafka-clients</artifactId> 
	<version>0.9.0.1</version>
</dependency>

Step2. 写生产者代码
官方文档地址:http://kafka.apache.org/20/javadoc/index.html?org/apache/kafka/clients/producer/KafkaProducer.html

        public static void t1() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092");
		props.put("acks", "all"); // 发消息的应答保障:all表示所有leader的副本都要写成功并给应答。
		props.put("retries", 0); // 是否重试,0表示不重试
		props.put("batch.size", 16384); // 可批量发送的消息个数
		props.put("linger.ms", 1); // 累积1毫秒再发送
		props.put("buffer.memory", 33554432); // 累积1毫秒使用的buffer的大小
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 采用的kafka自带序列化类
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

		Producer<String, String> producer = new KafkaProducer<>(props);
		// 循环发送100条消息
		for (int i = 0; i < 100; i++)
			// producer.send为异步发送
			producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));
		System.out.println("producer complete");
		producer.close();
	}

Step3. 写消费者代码
官方文档地址:http://kafka.apache.org/20/javadoc/index.html?org/apache/kafka/clients/consumer/KafkaConsumer.html

        public static void t1() {
             Properties props = new Properties();
	     props.put("bootstrap.servers", "localhost:9092");
	     props.put("group.id", "test"); // 消息时的groupid
	     props.put("enable.auto.commit", "true"); // 收到消息后,是否自动提交
	     props.put("auto.commit.interval.ms", "1000"); // 消费时间间隔
	     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);
	     consumer.subscribe(Arrays.asList("my-topic"));
	     while (true) {
	         ConsumerRecords<String, String> records = consumer.poll(100);
	         for (ConsumerRecord<String, String> record : records)
	             System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
	     }
	}

第二部分 kafka实现原理

1. topic & partition & offset

kafka对同一个topic中的消息进行分区(partition),每个partition都是有序的、不可变的队列,并且可持续向其中添加消息。(即topic中每个partition都是一个队列)
partition中的消息都被分了一个序列号,称之为偏移量(offset),在partition中每个offset都是唯一的。
在这里插入图片描述
在创建topic时,可在config/server.properties中指定partition的数量,也可以在创建topic后修改partition的数量。

采用partition设计的好处

1)可以处理更多的消息,不受单台服务器的限制:kafka基于文件存储,可将日志分散到多个server上,从而避免文件达到单机磁盘的上限。
2)分区可以作为并行处理单元,提高处理速度和吞吐率:每一条消息被发送到broker中,会根据partition规则(默认为随机)选择被存储到哪一个partition。如果partition规则设置合理,则所有消息会被均匀地分布到不同的partition中,这样就实现了水平扩展。

2. topic & partition & log

对于每一个partition, kafka集群维护这一个分区的Log(即日志文件),每个partition在存储层面是一个append log文件。任何发布到该partition的消息都会被追加到Log文件的尾部(这就相当于磁盘的顺序写,性能极高,因为无数据迁移与拷贝(如向文件中间写则需要copy))。
kafka集群保存所有的消息,直到他们过期,无论消息是否被消费了。
实际上,kafka消费者所持有的元数据只有1个offset,即消费者在这个Log中的偏移位置。
这个偏移量由消费者控制:当消费者消费消息时,偏移量也是线性增加的。偏移量由消费者控制,消费者可以将偏移量重置为更老的一个偏移量,重新读取消息,一个消费者的操作不会影响其他消费者对此log的处理。
在这里插入图片描述

broker不需要记录哪些消息被消费者消费,offset完全由消费者客户端和zookeeper记录,这样broker就不需要锁,所以实现了高吞吐。

日志存放

在物理存储上,每个partition对应一个物理的文件夹,partition的命名规则为:topic名称+有序序号,第一个序号从0开始。
partition是物理的概念,topic是逻辑的概念。
kafka 日志文件的存放位置:在config/server.properties中配置:
log.dir = /tmp/kafka-logs
实际上,生产环境中kafka日志不会放在/tmp目录下。
例如,刚才的测试结果日志存放在:/tmp/kafka-logs/my-topic-0

shipengdeMacBook-Pro:my-topic-0 shipeng$ ls /tmp/kafka-logs/my-topic-0
00000000000000000000.index	00000000000000000002.snapshot
00000000000000000000.log	leader-epoch-checkpoint
00000000000000000000.timeindex
消息在磁盘上的格式

直接打开00000000000000000000.index文件是乱码。
消息是二进制格式,并作为一个标准,所以消息可以在producer, broker, client之间传输,无需再copy或转换。
消息格式如下:

message length : 4 bytes (value: 1+4+n) //消息长度
"magic" value : 1 byte crc : 4 bytes
payload : n bytes
做最好的在线学习社区
//版本号 //CRC校验码 //具体的消息

日志分段

每个partition的log相当于一个文件夹,里面的内容相当于一个巨型文件,被分配到多个大小相等的segment(段) 文件中。每个partition只需要支持顺序读写就行了。但每个segment消息数量不一定相等,这种特性方便old segment file被快速删除,有效提高了磁盘利用率。
在这里插入图片描述

segment文件存储结构

生产者发消息到某个topic, 消息会被均匀地分布到多个partition上,broker往对应的partition的最后一个segment上添加消息。
当某个segment上面的消息条数达到了配置阈值或消息发布时间超过阈值时,segment上面的消息会被flush到磁盘上,只有flush到磁盘上的消息,consumer才能消费。segment达到一定大小后,将不会再往该segment写数据,broker会创建新的segment.
segment文件由2大部分组成:index file & data file,这两个文件同时出现,一一对应,后缀分别为.index 和 .log,分别表示segment索引文件和数据文件。
segment文件命名规则:partition全局的第一个segment从0开始,后续每个segment文件名为上一个全局partition的最大offset(偏移message数)。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。例如:

     00000000000000000000.index
     00000000000000000000.log
     00000000000000170410.index
     00000000000000170410.log

其中,170410是偏移量。
这样每个segment中存储很多条消息,消息id有其逻辑位置决定,即从消息id可直接定位到消息的存储位置,避免id到位置的额外映射。
备注:segment跟log4j比较相似,文件多大或多长时间生成一个新的文件。

segment的文件存储图
在这里插入图片描述
.index 索引文件存储了大量的元数据,.log文件存储大量的消息。索引文件中的元数据指向对应数据文件中message的物理偏移地址。其中以 .index 索引文件中的元数据【3,348】为例,在 .log 数据文件中表示第三个消息,即在全局partition中为170410+3=170413个消息,该消息的物理便宜地址为348。

日志根据offset读消息的过程

Step1. 在存储中找到对应的segment文件(可找到index文件和log文件)(segment文件是以offset命名,可用二分法查找,时间复杂度O(logN)
Step2. 通过全局的offset计算到segment内的offset.
Step3. 根据segment文件的offset读取消息数据
注意:.index文件在中的key不连续,即采用的是稀疏索引,即索引不连续。这样做可以减少文件大小。
例如,上图中找第二个offset,由于没有2的key, 那么先读1的key, 然后计算1key对应的消息大小,例如1的消息的大小是142,则用142作为2的偏移地址,这样来找到2的消息。

采用稀疏索引的好处:
为了减少索引文件的大小,索引文件变小了,可以直接Load到内存,内存操作性能更快。

日志写操作过程

日志允许串行的追加消息到文件最后,当它达到配置文件中设置的大小(1GB),就会滚动到新的文件上。
日志采用了2个配置参数:
M: 它定义了强制OS刷新文件到磁盘之前主动写入的消息数量;
S: 它定义了几秒后强制刷新。
这样就提供了耐久性的保障,当系统崩溃的时候,最多丢M条消息,或丢S秒数据。这个机制与redis类似。

日志删除操作

日志会删除一个时间段的日志。kafka日志管理器允许通过“插入删除策略”来选择删除哪些文件。目前的策略是删除N天以前的日志(相对于修改时间,默认保存7天),文件大小也可用作删除策略。
即删除可以根据时间或文件大小,默认是时间。

对于删除操作,采用了copy-on-write方式来避免加锁。

3. kafka分布式:针对partition

log的partition被分配到集群中的n个服务器上,每个服务器处理它负责的partition。根据配置, 每个partition还可以复制到其他服务器作为备份容错。
每个partition被复制成多份放到不同的机器上,每个partition有一个leader,0个或多个follower。leader处理此分区的所有读写请求,而follower被动地复制数据。如果leader宕机,其他的一个follower会被推举为新的leader。一台服务器可能同时是一个partition的leader, 另一个partition的follower. 这样可以负载均衡,避免所有的请求都只让一台或某几台服务器处理。

kafka生产者

生产者向topic上发布消息,生产者也负责选择发布到topic上的哪个分区(即partition)。最简单的方式是对partition列表中的partition轮询,也可以根据算法按权重分区。

传统消费者

传统的消息模型可分为:点对点(即队列:一条消息只有一个消费者能消费到)** **发布/订阅(一条消息可被多个消费者消费) 模式。

kafka作为存储系统

kafka作为存储系统相比其他存储系统的优势:高性能。
高性能是通过写入到kafka的数据将写到磁盘,并复制到集群中来保证容错性,并允许生产者等待应答(ack: 可配置是否等待所有leader副本都写入完成),直到消息完全写入。
由于kafka写入是在文件尾部追加,读取时也是顺序读取,所以无论你服务器上有50KB还是50TB,执行性能都是相同的。
由消费者来控制文件读取的位置offset。
所以可以认为kafka是一种专用于高性能、低延迟,提交日志存储,复制,传播特殊用途的分布式文件系统。

kafka流处理

流处理指消费topic中的消息,处理后,再写入到topic中。
对于简单的处理,可直接用producer, consumer API来实现流处理。对于复杂的转换,kafka提供了更强大的Streams API, 可构建聚合计算或连接“流”到一起的复杂应用程序。

kafka消费者 consumer group

kafka为传统消费者模型中的两种方式(点对点和发布/订阅)提供了统一的消费者抽象模型:consumer group。
一个发布在topic中的消息,被分发给consumer group中的一个消费者(此时就是点对点模型)。如果所有的消费者都在不同的group中,就变成了发布/订阅模型。
kafka broker是完全无状态的,即不会记录每个消费者的消费记录,而由consumer group这个消费者来记录。
在这里插入图片描述
上图可以看出,consumer group不在kafka server中。

kafka客户端(consumer)通过TCP长连接到broker来从集群中消费消息,并透明地处理kafka集群中的故障服务器,透明地调节适应集群中变化的数据分区。

多个consumer的消费必须是顺序地读取partition中的message,新启动的cosnumer默认从partition队列最头端最新的地方开始阻塞地读message。如果觉得性能不高的话,可以通过加partition数量来做水平扩展。

一个consumer group下面的所有consumer thread一定会消费topic中所有的partition, 所以,推荐的设计是,consumer group下的consumer thread数量等于partition数量,这样效率是最高的。

一个consumer可以消费不同的partition, 但kafka不保证数据间的顺序性,即kafka只保证在一个partition上的数据是有序的,但读多个partition时,根据读partition的顺序,结果顺序会不同。(即只保证单个partition中消息顺序,不保证跨partition中的消息顺序)。

偏移量和消费者的位置

每条消息在partition中都有一个偏移量offset,即分区中的唯一位置,该offset是消息的唯一标识符。

kafka消费者会持有下一条偏移量的位置,它比消费者看到的最大偏移量的位置还大1个,在消费者每次调用poll来接收消息后,会自动+1。(备注:这个位置不需要消费者用户来手动维护)

在消费者成功poll拉取到消息后,会对kafka服务端做“提交”,这个“已移交”位置是已安全保存的最后偏移量。(这个offset在早期版本中保存在zk中,但由于这样zk承受了太高的并发,在后续版本中(大概1.0版本),改成把offset记录到了一个专门的topic中),如果kafka消费者进程挂掉后重启,消费者仍可恢复到这个偏移量继续消费。

消费者可选择定期自动提交偏移量给kafka服务端,也可以选择通过调用commit API来手动控制什么时候提交(默认为每消费到1条后自动提交)

消费者group & topic订阅

kafka通过将相同的group.id的消费者视为同一个消费者group。kafka通过进程池来消费消息,这些进程池中的进程可以在同一个机器或不同的机器(这里的机器指kafka集群中的服务器)上,这样可实现扩展性和容错性。

分组中的消费者们订阅同一个topic时,kafka将该topic的消息发送到每个消费者组中;对于每一个消费者组,kafka通过partition来load balance该分组中的所有成员,这样每个消费者组中的每个partition正好分配一个消费者。
备注:即一个group中同一个partition只能有一个consumer进程,且同一个group中的consumer数不要大于partition数,否则会造成有的consumer消费不到消息

消费者组的成员是动态维护的,如一个消费者故障,或当有新的消费者加入时,kafka将通过定时刷新机制,将其分配给分组中的新成员,去掉旧成员。

可将消费者group看做是由多个进程组成的单一的逻辑订阅者,所有进程都是单个消费者group的一部分(可看作是点对点的消息队列模型,group中的每个消费者都是一个进程),因此消息传递时就像队列一样,在group中的所有消费者之间Load balance。

当分组重新分配自动发生时,可通过ConsumerRebalanceListener通知消费者(即一个回调函数),这样可允许我们自己来写一些必要的应用程序级逻辑,如状态清除、手动提交偏移量等等逻辑。

kafka也需要消费者通过使用assign(Collection)手动分配指定分区,但如果使用了手动指定,那么动态分区分配和协调消费者组机制将失效(通常不手动指定)

发现消费者故障

当消费者订阅一组topic后,当调用poll(long)时,消费者将自动加入到group中。只要持续调用poll,消费者将一直保持可用,并继续从分配的分区中接收消息。此外,消费者向服务器定时发送心跳:如果消费者崩溃或无法在session.timeout.ms配置的时间内发送心跳,则消费者将被kafka服务端视为死亡,并将重新load balance消费者们。

消费者“活锁”:消费者仍在持续发送心跳,但它从不从partition中获取消息。而一个group中的一个partition只能有一个消费者进程,其他消费者无法消息该partition中的消息,这样这个消费者就是“占着茅坑不拉屎”,称为“活锁”。为了防止这种情况的发生,kafka采用了max.poll.interval.ms活跃检测机制,如果消费者调用poll获取消息的频率大于该时间,则kafka会把该消费者踢出该组,以便其他消费者接替他来消费消息。所以,如果消费者要留在group中不被踢,必须持续调用poll. 当消费者被踢出后,你会看到offset提交失败(调用commitSync()引发的CommitFailedException),这是一种安全机制,保证只有活动的消费者才能提交offset。

对于消息消费者获取到消息后,如果处理消费到的消息的时间不可预测时(即不知道处理消息的业务处理需要花费多少时间),这样推荐的方法是把消费到的消息放到另一个线程中处理(异步),让消费者继续调用poll(但要暂停从poll接收新消息,以防止消费者被踢)。但必须注意确保已提交的offset不超过实际的位置,你必须禁用自动提交,并只有在线程处理完成后,才能做手动提交。还要注意,你需要暂停从poll接收到新消息,让线程处理完之前的消息。
备注:这种对消费到的消息业务处理时间很长时,不能在业务处理完成之前就消费kafka中的下一条消息,另外,还要禁用自动提交,在业务处理完成后,再手动提交offset到kafka, 以防止丢消息。

kafka保证

  • 每个partition中的消息无论生产,还是消费,都与写入partition的消息顺序相同。
  • 如果一个topic配置了n个副本(replication factor), 那么允许n-1个服务器宕机,而不丢失任何(已commit的)消息。

第三部分 使用kafka提供的Java client API进行开发

1. Admin Client API 开发

官网文档:
http://kafka.apache.org/20/javadoc/index.html?org/apache/kafka/clients/producer/KafkaProducer.html

package kafka_test.kafka_test.admin_client;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.AdminClientConfig;
import org.apache.kafka.clients.admin.AlterConfigsResult;
import org.apache.kafka.clients.admin.Config;
import org.apache.kafka.clients.admin.ConfigEntry;
import org.apache.kafka.clients.admin.CreatePartitionsResult;
import org.apache.kafka.clients.admin.CreateTopicsResult;
import org.apache.kafka.clients.admin.DeleteTopicsResult;
import org.apache.kafka.clients.admin.DescribeClusterResult;
import org.apache.kafka.clients.admin.DescribeConfigsResult;
import org.apache.kafka.clients.admin.DescribeTopicsResult;
import org.apache.kafka.clients.admin.ListTopicsOptions;
import org.apache.kafka.clients.admin.ListTopicsResult;
import org.apache.kafka.clients.admin.NewPartitions;
import org.apache.kafka.clients.admin.NewTopic;
import org.apache.kafka.clients.admin.TopicDescription;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.config.ConfigResource;

public class AdminClientTest {
	private static final String TOPIC_NAME = "my-admin-topic";

	public static void main(String[] args) throws Exception {
		// createTopic();
//		listTopic();
//		deleteTopics();
//		listTopic();
//		listTopicWithCustomerOffsets();
//		describeTopic();
//		addPartitions();
//		describeTopic();
//		alterConfig();
//		describeConfig();
		describeCluster();
	}

	public static AdminClient getAdminClient() {
		Properties props = new Properties();
		props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

		AdminClient client = AdminClient.create(props);
		return client;
	}

	private static void createTopic() throws InterruptedException, ExecutionException {
		NewTopic topic = new NewTopic(TOPIC_NAME, 1, (short) 1);
		CreateTopicsResult ret = getAdminClient().createTopics(Arrays.asList(topic));
		ret.all().get();
		System.out.println("create topic is ok");
	}

	private static void listTopic() throws InterruptedException, ExecutionException {
		ListTopicsResult ret = getAdminClient().listTopics();
		Set<String> set = ret.names().get();
		System.out.println("topic names = " + set);
	}

	private static void listTopicWithCustomerOffsets() throws InterruptedException, ExecutionException {
		// __consumer_offsets
		ListTopicsOptions options = new ListTopicsOptions();
		options.listInternal(true);

		ListTopicsResult ret = getAdminClient().listTopics(options);
		Set<String> set = ret.names().get();
		System.out.println("topic names = " + set);
	}

	private static void describeTopic() throws Exception {
		DescribeTopicsResult ret = getAdminClient().describeTopics(Arrays.asList(TOPIC_NAME));
		Map<String, TopicDescription> ts = ret.all().get();

		for (Map.Entry<String, TopicDescription> entry : ts.entrySet()) {
			System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue());
		}
	}

	private static void describeConfig() throws Exception {
		DescribeConfigsResult ret = getAdminClient()
				.describeConfigs(Collections.singleton(new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME)));
		Map<ConfigResource, Config> configs = ret.all().get();

		for (Map.Entry<ConfigResource, Config> entry : configs.entrySet()) {
			ConfigResource key = entry.getKey();
			Config value = entry.getValue();

			System.out.println("resource key name=" + key.name() + ", type=" + key.type());

			Collection<ConfigEntry> configEntries = value.entries();
			for (ConfigEntry a : configEntries) {
				System.out.println("resource value name=" + a.name() + ", value=" + a.value());
			}
		}
	}

	private static void alterConfig() throws Exception {
		Config config = new Config(Arrays.asList(new ConfigEntry("preallocate", "false")));
		AlterConfigsResult ret = getAdminClient().alterConfigs(
				Collections.singletonMap(new ConfigResource(ConfigResource.Type.TOPIC, TOPIC_NAME), config));
		ret.all().get();
		System.out.println("alter config is ok");
	}
	
	// kafka的parition数目只能增加不能减少。
	private static void addPartitions() throws Exception {
		Map<String, NewPartitions> map = new HashMap<>();
		map.put(TOPIC_NAME, NewPartitions.increaseTo(3));
		
		CreatePartitionsResult ret = getAdminClient().createPartitions(map);
		ret.all().get();
		System.out.println("add partition is ok");
	}
	
	private static void deleteTopics() throws Exception{
		DeleteTopicsResult ret = getAdminClient().deleteTopics(Arrays.asList(TOPIC_NAME));
		ret.all().get();
		System.out.println("delete topic is ok");
	}
	
	private static void describeCluster() throws Exception{
		DescribeClusterResult ret = getAdminClient().describeCluster();
		System.out.println("cluster id = " + ret.clusterId().get() + ", controller=" + ret.controller().get());
		
		for (Node node : ret.nodes().get()) {
			System.out.println("now the node = " + node);
		}
	}
}

2. kafka producer API 开发

package kafka_test.kafka_test.helloworld;

import java.util.Properties;

import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

public class MyProducer {
	public static void main(String[] args) throws Exception {
		// t1();
		// t2();
//		t3();
		t4();
	}

	/*
	 * 异步发送不阻塞
	 */
	public static void t1() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092"); // 可指定一个或多个broker, 推荐写多个broker, 如果写一个万一挂了怎么办
		props.put("acks", "all"); // 发消息的应答保障:all表示所有leader的副本都要写成功并给应答,但性能较差。
		props.put("retries", 0); // 如果请求失败,是否重试,0表示不重试
		props.put("batch.size", 16384); // 可批量发送的消息大小,此处是当消息积累到16k大小时发送:减少网络IO次数
		props.put("linger.ms", 1); // 累积1毫秒再发送
		props.put("buffer.memory", 33554432); // 累积1毫秒使用的buffer的大小
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 采用的kafka自带序列化类
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

		Producer<String, String> producer = new KafkaProducer<>(props);
		// 循环发送100条消息
		for (int i = 0; i < 100; i++)
			// producer.send为异步发送
			producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));
		System.out.println("producer complete");
		producer.close();
	}

	/*
	 * 异步发送阻塞
	 */
	public static void t2() throws Exception {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092"); // 可指定一个或多个broker, 推荐写多个broker, 如果写一个万一挂了怎么办
		props.put("acks", "all"); // 发消息的应答保障:all表示所有leader的副本都要写成功并给应答,但性能较差。
		props.put("retries", 0); // 如果请求失败,是否重试,0表示不重试
		props.put("batch.size", 16384); // 可批量发送的消息大小,此处是当消息积累到16k大小时发送:减少网络IO次数
		props.put("linger.ms", 1); // 累积1毫秒再发送
		props.put("buffer.memory", 33554432); // 累积1毫秒使用的buffer的大小
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 采用的kafka自带序列化类
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

		Producer<String, String> producer = new KafkaProducer<>(props);
		// 循环发送100条消息
		for (int i = 0; i < 100; i++) {
			// producer.send为异步发送
			String key = "key-" + i;
			String value = "value-" + i;
			ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", key, value);
			RecordMetadata rmd = producer.send(record).get();
			System.out.println("rmd=" + rmd);
		}
		System.out.println("producer complete");
		producer.close();
	}

	/*
	 * 异步发送, callback: 完全无阻塞 callback的回调可保证顺序:先发出去的消息,在回调时也一定先回来。
	 */
	public static void t3() throws Exception {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092"); // 可指定一个或多个broker, 推荐写多个broker, 如果写一个万一挂了怎么办
		props.put("acks", "all"); // 发消息的应答保障:all表示所有leader的副本都要写成功并给应答,但性能较差。
		props.put("retries", 0); // 如果请求失败,是否重试,0表示不重试
		props.put("batch.size", 16384); // 可批量发送的消息大小,此处是当消息积累到16k大小时发送:减少网络IO次数
		props.put("linger.ms", 1); // 累积1毫秒再发送
		props.put("buffer.memory", 33554432); // 累积1毫秒使用的buffer的大小
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 采用的kafka自带序列化类
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

		Producer<String, String> producer = new KafkaProducer<>(props);
		// 循环发送100条消息
		for (int i = 0; i < 100; i++) {
			// producer.send为异步发送
			String key = "key-" + i;
			String value = "value-" + i;
			ProducerRecord<String, String> record = new ProducerRecord<String, String>("my-topic", key, value);
			producer.send(record, new Callback() {
				@Override
				public void onCompletion(RecordMetadata metadata, Exception exception) {
					System.out.println("the metadata=" + metadata.topic() + ", offset=" + metadata.offset());
				}
			});
		}
		System.out.println("producer complete");
		producer.close();
	}

	/*
	 * 异步发送不阻塞, 自定义分区算法
	 */
	public static void t4() throws Exception {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092"); // 可指定一个或多个broker, 推荐写多个broker, 如果写一个万一挂了怎么办
		props.put("acks", "all"); // 发消息的应答保障:all表示所有leader的副本都要写成功并给应答,但性能较差。
		props.put("retries", 0); // 如果请求失败,是否重试,0表示不重试
		props.put("batch.size", 16384); // 可批量发送的消息大小,此处是当消息积累到16k大小时发送:减少网络IO次数
		props.put("linger.ms", 1); // 累积1毫秒再发送
		props.put("buffer.memory", 33554432); // 累积1毫秒使用的buffer的大小
		props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 采用的kafka自带序列化类
		props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
		props.put("partitioner.class", "kafka_test.kafka_test.helloworld.MyPartition");

		Producer<String, String> producer = new KafkaProducer<>(props);
		// 循环发送100条消息
		for (int i = 0; i < 100; i++) {
			// producer.send为异步发送
			RecordMetadata rmd = producer
					.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)))
					.get();
			System.out.println("rmd=" + rmd.partition() + ", i=" + i);
		}
		System.out.println("producer complete");
		producer.close();
	}
}

采用自定义分区算法,需要实现Partitioner接口:

package kafka_test.kafka_test.helloworld;

import java.util.Map;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;

public class MyPartition implements Partitioner {

	@Override
	public void configure(Map<String, ?> configs) {
	}

	@Override
	public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
		int keyInt = Integer.parseInt((String)key);
		return (keyInt % 3);
	}

	@Override
	public void close() {
	}
}

3. kafka consumer API 开发

package kafka_test.kafka_test.helloworld;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.common.TopicPartition;

public class MyConsumer {
	public static void main(String[] args) {
		// t1();
//		t2();
//		t3();
		t4();
	}

	/*
	 * 自动提交offset
	 */
	public static void t1() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092");
		props.put("group.id", "test"); // 消息时的groupid
		props.put("enable.auto.commit", "true"); // 收到消息后,是否自动提交
		props.put("auto.commit.interval.ms", "1000"); // 消费时间间隔: 此处为1秒提交一回
		props.put("session.timeout.ms", "30000"); // 消费者session过期时间,通过心跳,超时则会被踢出group
		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);
		consumer.subscribe(Arrays.asList("my-topic"));
		try {
			while (true) {
				ConsumerRecords<String, String> records = consumer.poll(100);
				for (ConsumerRecord<String, String> record : records)
					System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(),
							record.value());
			}
		} finally {
			consumer.close();
		}
	}

	/*
	 * 手动提交offset
	 */
	public static void t2() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092");
		props.put("group.id", "test"); // 消息时的groupid
		props.put("enable.auto.commit", "false"); // 收到消息后,是否自动提交
		props.put("auto.commit.interval.ms", "1000"); // 消费时间间隔: 此处为1秒提交一回
		props.put("session.timeout.ms", "30000"); // 消费者session过期时间,通过心跳,超时则会被踢出group
		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);
		consumer.subscribe(Arrays.asList("my-topic"));

		List<ConsumerRecords<String, String>> buffer = new ArrayList<>();
		int batchSize = 100; // 每100个消息批量处理一次
		try {
			while (true) {
				ConsumerRecords<String, String> records = consumer.poll(100);
				for (ConsumerRecord<String, String> record : records) {
					buffer.add(records);
				}

				if (buffer.size() >= batchSize) {
					// 模拟业务逻辑:批量加入DB, 若访问DB出错了,则不提交到kafka
					System.out.println("now add data to DB");

					// 手工提交,告诉broker已经收到的消息都处理成功了,可以标记为“已提交”
					consumer.commitSync();

					buffer.clear();
				}
			}
		} finally {
			consumer.close();
		}
	}

	/*
	 * 手动提交offset, 分区进行精细化控制
	 */
	public static void t3() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092");
		props.put("group.id", "test"); // 消息时的groupid
		props.put("enable.auto.commit", "false"); // 收到消息后,是否自动提交
		props.put("auto.commit.interval.ms", "1000"); // 消费时间间隔: 此处为1秒提交一回
		props.put("session.timeout.ms", "30000"); // 消费者session过期时间,通过心跳,超时则会被踢出group
		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);
		consumer.subscribe(Arrays.asList("my-topic"));
		try {
			while (true) {
				ConsumerRecords<String, String> records = consumer.poll(100);
				for (TopicPartition partition : records.partitions()) {
					List<ConsumerRecord<String, String>> prs = records.records(partition);
					// 处理每个partition的消息
					for (ConsumerRecord<String, String> pr : prs) {
						System.out.println("partition=" + pr.partition() + ", key=" + pr.key() + ", offset="
								+ pr.offset() + ", value=" + pr.value());
					}

					// 消费完毕,提交给kafka新的offset
					long lastOffset = prs.get(prs.size() - 1).offset();
					// 提交的offset是当前消费到的最新的offset + 1
					consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
				}
			}
		} finally {
			consumer.close();
		}
	}
	
	/*
	 * 手动提交offset, 订阅指定分区
	 */
	public static void t4() {
		Properties props = new Properties();
		props.put("bootstrap.servers", "localhost:9092");
		props.put("group.id", "test"); // 消息时的groupid
		props.put("enable.auto.commit", "false"); // 收到消息后,是否自动提交
		props.put("auto.commit.interval.ms", "1000"); // 消费时间间隔: 此处为1秒提交一回
		props.put("session.timeout.ms", "30000"); // 消费者session过期时间,通过心跳,超时则会被踢出group
		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);
		
		TopicPartition p0 = new TopicPartition("my-topic", 0);
		TopicPartition p1 = new TopicPartition("my-topic", 1);
		TopicPartition p2 = new TopicPartition("my-topic", 2);
		
		consumer.assign(Arrays.asList(p0));
		
//		consumer.subscribe(Arrays.asList("my-topic"));
		try {
			while (true) {
				ConsumerRecords<String, String> records = consumer.poll(100);
				for (TopicPartition partition : records.partitions()) {
					List<ConsumerRecord<String, String>> prs = records.records(partition);
					// 处理每个partition的消息
					for (ConsumerRecord<String, String> pr : prs) {
						System.out.println("partition=" + pr.partition() + ", key=" + pr.key() + ", offset="
								+ pr.offset() + ", value=" + pr.value());
					}

					// 消费完毕,提交给kafka新的offset
					long lastOffset = prs.get(prs.size() - 1).offset();
					// 提交的offset是当前消费到的最新的offset + 1
					consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
				}
			}
		} finally {
			consumer.close();
		}
	}
}

第四部分 kafka集群 & Zookeeper

官网文档地址:
http://kafka.apache.org/documentation/#gettingStarted

1. 如何创建3个kafka broker的集群

Step1. 在kafka根目录配置文件中,copy下面的配置文件:

cp config/server.properties config/server-1.properties
cp config/server.properties config/server-2.properties

Step2. 编辑下面2个配置文件:
config/server-1.properties:

broker.id=1
listeners=PLAINTEXT://:9093
log.dirs=/tmp/kafka-logs-1

config/server-2.properties:

broker.id=2
listeners=PLAINTEXT://:9094
log.dirs=/tmp/kafka-logs-2

Step3. 启动这两个新节点

bin/kafka-server-start.sh config/server-1.properties &
bin/kafka-server-start.sh config/server-2.properties &

2. zookeeper在kafka中的应用

1)broker在zk上注册

kafka的所有broker是分布式部署且互相独立的,通过zookeeper作为注册系统将整个broker集群管理起来。zk上会有一个专门用来记录kafka broker服务器列表的节点:/brokers/ids
当每个broker启动时,都会注册到zk上,即到/brokers/ids下创建属于自己的节点,格式是:/brokers/ids/[0,1,…,N] , 此处/brokers/ids/…是zk环境上面的路径

kafka使用全局唯一的数字来指定每一个broker服务器。在zk上创建完kafka broker节点后,每个broker会把自己的IP和端口记录到zk上的节点。其中,broker创建的节点是临时节点,一旦broker宕机,则对应的临时节点也会从zk上删除。

2)topic注册

在kafka中,同一个topic的消息会被分到多个partition, 并将其分布到多个broker上,这些partition与broker的对应关系有zk维护。由zk上面专门的节点来记录,如/brokers/topics, 分区节点是临时节点。
kafka中的每个topic都会以/brokers/topics/[topic]/partitions/[0…N]的形式来记录,值是自己的broker ID ,并写入针对该topic的分区总数。

3)消费者注册

消费者服务器在初始化启动时,加入到消费者分组,步骤如下:
Step1. 注册到消费者分组:
每个消费者服务器启动时,都会到zk指定的节点下创建一个属于自己的消费者节点,如/consumers/[group id]/ids/[consumer id], 完成节点创建后,消费者会将自己订阅的topic消息写入该临时节点。
Step2. 对消费者group中的消费者的变化进行监听
每个消费者都需要关注所属消费者group中的其他消费者服务器的变化情况,即对/consumers/[group id]/ids节点注册的子节点变化进行watcher监听,一旦发现消费者新增会减少,就触发消费者的重新负载均衡。
Step3. 对broker服务器变化注册监听
消费者需要对/broker/ids/[0…N]节点中的节点进行监听,一旦发现broker服务器列表发生变化,就要根据具体情况来决定是否需要重新进行消费者负载均衡。
Step4. 进行消费者负载均衡
为了让同一个topic下的不同partition的消息,尽量均衡地被多个消费者消费,对于一个消费者group,如果组内的消费者服务器发生变更,则会进行消费者重新负载均衡

4)生产者负载均衡

由于每个broker启动时,都会完成broker在zk上注册,生产者会通过zk上节点的变化,来动态感知broker服务器列表的变更,这样就可以实现动态负载均衡。

5)消费者负载均衡

与生产者类似,Kafka消费者也通过在zk上注册来实现负载均衡,每个消费者group中包含若干消费者,每条消息智慧发送给group中的一个消费者,不同消费者group消费自己特定的topic下的消息,互不干扰。

6)消息partition与消费者的关系

对于每个消费者group, kafka都会为其分配一个全局唯一的group id, 同一个消费者group内部的所有消费者共享这个id;同时,kafka为每个消费者分配一个consumer id,采用"hostname:UUID"形式来表示。在kafka中,每个消息parition中只能同时有一个消费者进行消费,因此,需要在zk上记录消息partition与consumer之间的关系,每个消费者一旦确定了对一个消息partition的消费权力,需要将其consumer id写入到对应消息partition的临时节点上,例如:
/consumers/[group id]/owners/[topic]/[broker id-partition id]
其中,[broker id-partition id]就是一个消息分区的标识,节点内容就是该消费分区上的消息消费者的consumer id。

7)消费者offset

在消费者对指定消息partition进行消息消费过程中,需要定时地将消息消息进度offset记录到zookeeper上,以便在该消费者进行重启或者其他消费者重新接管该消息分期的消息消费后,能够从之前的消费进度开始继续进行消息消费。offset在zk中有一个专门的节点进行记录,其节点路径为:
/consumers/[group id]/offsets/[topic]/[broker id-partition id]
节点内容就是offset的值。

第五部分 kafka设计优势详解

1. kafka生产者原理

1)直接发送

kafka的生产者直接向消息发送到partition的leader的broker上。为了做到这一点,kafka所有节点都可以应答给生产者哪些服务器是正常的,哪些topic的parition的leader允许生产者在给定的时间内直接请求。
备注:所以在生产者生产消息时,随便指定集群中的某个IP(该IP可以告诉producer发消息发到哪),就可以成功将消息发送到kafka集群。

kafka producer发消息流程:
1)KafkaProducer向bootstrap.servers所配置的地址指向的Server发送MetadataRequest请求;
2)bootstrap.servers所配置的地址指向的Server返回MetadataResponse给KafkaProducer,MetadataResponse中包含了Kafka集群的所有元数据信息;
3)KafkaProducer与Kafka集群建立TCP连接。

2)负载均衡发送消息到partition

可由生产者来控制把消息发送到哪个partition,默认是随机的,也可以自己实现一个partition落点函数,允许用户通过key去指定分区。例如:选择用户id当做key,那么对给定的用户id的所有数据将被发送到相同的分区。

3)异步发送

生产者发送消息时采用批量处理的方式,即在内存中积累数据,在单个请求发送累积的大批量数据,这样会减少IO次数,提升吞吐量,缺点是消息会有少许延迟。

2. kafka存储的实现原理

1)使用磁盘:以页缓存为中心的设计

kafka高度依赖文件系统来存储和缓存消息,而通常认为“磁盘是缓慢的”,但kafka是如何做到通过磁盘实现高吞吐的呢?

磁盘性能的基础知识:顺序读写(即线性读写)与 随机读写

  • 对磁盘的写性能:顺序写入磁盘比随机写磁盘快近6000倍。
  • 对磁盘的读性能:在某些情况下,顺序读磁盘比内存的随机访问还快。
  • 操作系统提供了预读和预写的技术:即把多个大块数据预先取到内存来读;把较小的写入合并成一个大块后再物理写。
  • 操作系统通常用内存做磁盘缓存,称为pagecache,即操作系统很乐意将所有空闲的内存用来做磁盘缓存,尽管在内存挥手的时候会有一些性能上的代价。
  • 所有的磁盘读写都会通过这个内存缓存。

2)kafka使用文件系统,并依赖于pagecache(页缓存)

kafka是scala语言写的基于JVM的,这意味着:

  • JVM中数据存储在对象中时,会消耗比数据本身更对的内存(甚至是数据本身的2倍)
  • 随着JVM堆中数据的增加,JVM的GC会耗费系统资源。

kafka怎样对此做的优化设计呢?

kafka使用文件系统,并依赖于pagecache(页缓存)

这样比放在JVM中的对象中要好:

  • kafka通过自动访问所有可用的内存(而不是JVM堆内存,即使用进程外缓存),使得内存使用率提高至少一倍,并可能通过存储紧凑型字节结构再次提高一倍。按官方数据,在32G机器上可使用高达28G~32G的缓存,且无需GC。
  • 即使服务重启,由于是进程外缓存,此缓存仍可用,而如果用JVM缓存(进程内缓存)如果重启后重新从磁盘load, 10GB缓存需要10分钟。
  • kafka简单而巧妙的设计:不在内存中维护数据,所有数据写入文件系统上的持久化日志中,这实际上是将数据写入到了操作系统内核的页缓存上。

3)kafka访问的高性能:O(1)、访问性能与消息量大小完全无关

其他消息系统中,使用持久化,数据结构常使用btree。btree的时间复杂度是O(log N),成本相当高,且磁盘寻找在10ms的数量级,每个磁盘一次只能做一次寻找,且受并行性限制,所以,即使是少量磁盘搜索,也会导致很高的开销。

  • kafka的持久化队列建立在线性读和追加Log写(线性写)之上,所以读写操作通常都是O(1)。
  • kafka的性能与数据大小完全无关,无需任何性能损失就可以访问几乎无限制的磁盘空间。

3. kafka高性能的实现原理

磁盘访问性能低的原因:

  • 大量小IO操作
  • 过度的字节复制

kafka磁盘访问高性能的方法:零copy(页缓存、sendfile)、append log

1)避免大量小IO操作:批量消息发送

kafka把多个要发送的消息集合在一起发送。这样导致更大的网络数据包,更大的顺序磁盘操作,连续的内存块。这些都允许kafka将随机消息写入的突发流转换成流向消费者的线性写入。

2)避免过度的字节复制:统一格式标准、采用sendfile

为了避免高负载时大量字节复制对性能的影响,kafka采用有生产者、消费者共享的二进制消息格式,这样数据就可以在他们之间自由传输而无需转换。

  • broker维护的消息日志本身就是文件,每个文件都是由生产者、消费者使用相同的格式写入磁盘的。
  • unix操作系统提供了一个优化方式,用于将数据从页缓存传输到socket;在Linux中,通过sendfile系统调用来完成的。

备注:低效的方式:
在这里插入图片描述
Step1. 操作系统先从磁盘读到内核空间的页缓存
Step2. 应用程序将数据从内核空间的页缓存读入到用户空间的应用程序缓存
Step3. 应用程序将数据写回到内核空间的socket缓存
Step4. 操作系统将数据从socket缓存复制到网卡缓冲区,以便将数据从网络发出。
这样做了四次copy,两次系统调用,所以是低效的。

sendfile: 省去了与应用程序缓存的交互,从页缓存直接到socket缓存。
使用sendfile的方式,可避免再次copy:允许操作系统将数据直接从页缓存发送到网络上。
在这里插入图片描述

假设一个topic有多个消费者,消息数据只需要复制到页缓存中一次,然后就可以在每个消费者上面重复使用,而不必存储,也不必在每次读取时复制到用户空间,这样可以做到消费消息的速度接近于网络连接限制的速度。
零拷贝:这种页缓存和sendfile组合使用,意味着kafka集群的消费者都完全从缓存消费消息,而磁盘没有任何读取活动。这种优化方式成为零拷贝。
端到端的批量压缩
在某些情况下,瓶颈不是CPU或磁盘IO,而是网络带宽。
kafka通过递归消息集来支持压缩,集把一批消息一起压缩,并把压缩后的消息包发送到服务器,且消息以这种压缩后的形式写入,并在日志中保持压缩,且只能由消费者解压。kafka支持gzip和snappy压缩协议。

4. kafka消费者原理

1. 推 还是 拉

kafka是消费者从broker拉取消息,由消费者指定消息的偏移量,并收到那一块日志的起始位置。
采用“拉”的好处:

  • 消费者可以重新指定位置,重新消费
  • 消费者可根据自己处理消息的速度,自行拉取消息。而通过推的方式,只是以最快的速度传递消息,但消费者可能来不及处理,或者消息由于网络问题而丢掉。
  • 拉取方式可以让消费者批量处理消息,消费者可指定每次拉取多少条消息。而推的方式,要么一次性积累多条消息,然后在不知道消费者处理能力的基础上发送。
  • 拉取方式的不足:如果broker没有消息可供消费,那么消费者会轮询,等待直到有数据可消费。为了避免这种情况,kafka允许消费者在拉取时,使用“long pull"进行阻塞,直到数据到达。设置等待时间的好处是可积累消息,组成大数据块一起发送。

长轮询是长连接的一种,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则会 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。

综上,所以kafka采用拉而不是推。

2. kafka如何高效追踪已经消费的消息

对于大部分mq 产品的设计方案(爱奇艺聊天室就是这样的设计):

  1. 当消息被消费者消费后,在broker上记录,或等消费者确认后再记录,然后可立刻删除消息,这样可保证mq中的消息数量不变。这样的设计,对于存储消息数据不是很大的情况,是个务实的做法。
  2. 但如果broker每次通过网络发出消息给消费者后立即记录的话,如果消费者没有收到消息(如消费者服务器崩溃或网络故障),则会导致丢消息,为了解决这个问题,很多消息系统通过消费者返回ack来保证消费者收到了消息。
  3. 上面的方式虽然解决了丢消息的问题,但如果消费者收到了消息并成功处理,但返回ack时失败了,那么消息会被重复消费2次。另外也存在性能问题:broker需要保持每条消息的状态(未消费、已发送、已消费),已消费的消息才会被删除。

kafka的设计:
1)由消费者跟踪每个partition中已消费的最大的offset,并定期提交offset到broker, 在消费者服务器重启后,可从broker的offset中恢复。kafka提供了一个选项,可在指定broker中类存储所有给定消费者组的offset, 称为offset manager。
2)kafka的每个topic被分为一组完全有序的partition,每个partition只能有每个订阅消费者group中的一个消费者消费,这样可保证每个partition中的offset只是一个整数,即下一个待消费消息的偏移量。这样使得消息的应答更轻量级。
3)这种设计的另一个好处是,消费者可以回到已消费过的位置诚信消费。
4)还有个好处是,消费者可以不必实时消费,即可在消息产生后的一段时间后进行批量数据加载,如批量导数据到DB或大数据系统中。

3. kafka如何保证不丢消息

1)kafka生产者与消费者之间的担保语义
  • 最多一次:消息可能丢,但不会重。
  • 至少一次:消息不会丢,但可能重。
  • 正好一次。
2)kafka发送消息时的可靠性保障

当生产者发送一条消息时,一旦该消息commit到了日志,只要此消息所在partition的集群中有一个broker还活着,消息就不会丢失。
问题:如果生产者发消息时,正好遇到网络问题,就不能确定已经commit的消息时在故障前还是故障后。
针对此问题,旧版本的kafka,如果生产者没有收到消息commit的响应,则重发消息,对应的语义为“至少一次”,但可能造成消息重复。
新版本的kafka解决此问题通过“幂等性”来避免消息重复。即broker为每个生产者分配一个ID,然后通过生产者发送消息的ID来去重。但在语义上仍然为“至少一次”。
kafka生产者使用类似事务的方式,将消息发送到所有的topic: 即要么消息写入到所有topic成功,要么全部失败。对应的语义为“正好一次”。

3)kafka消费消息时的可靠性保障
  • kafka默认是保证消费者消费消息“至少一次”。
  • kafka允许用户通过禁止生产者重试(即重发消息),且消费者在处理一批消息前,就提交offset,这样来实现“最多一次”。
  • 而“正好一次”,需要借助kafka的外部业务系统,通过业务系统记录的offset来消费。

5. kafka的副本集原理

1. 副本集概述

kafka副本:kafka集群在各个broker上备份topic partition中的日志(日志中记录的消息),称为“副本”。我们可以自己设置每个topic的副本数。当集群出现故障时,可自动切换到这些副本,从而保障在故障时,消息仍然可用。
副本以topic的partition为单位,kafka使用的默认副本:就是不需要副本的topic的复制因子就是1。在正常情况下,kafka的每个partition都有一个单独的leader,0个或多个follower,副本的综述包括leader。kafka所有的读和写,都是从该partition的leader读或写。副本集只是当leader挂了时才会顶上。
通常,partition数会比broker数多,leader均匀分布到broker上,follower的日志完全与leader的日志完全相当。(当然,在任何时间点上,leader比follower可能多几条消息,因为尚未同步到follower)。
follower从leader中同步消息的方式时,follower作为消费者从leader中消费消息,并写入到自己的日志中。

2. 节点故障

kafka怎样定义一个broker还“活着”呢?有下面2个条件:
1)通过zookeeper的心跳机制,broker必须和zk之间有心跳。
2)如果该broker是个follower,它必须复制leader的消息,且不能落后太多。
leader会跟踪每个follower,如果一个follower死掉或卡住,leader会从同步副本列表中删除它。落后是通过replica.lag.max.messages配置控制,卡住是通过replica.lag.time.max.ms配置控制。

kafka如何处理故障节点?
1)如果该分区的消息都同步到了所有的副本的日志中,该消息视为“已提交”,只有“已提交”的消息才会让消费者消费。所以消费者不会消费到由于leader故障而丢失的消息。
2)假设某partition有2个副本(Leader也算一个副本),kafka的生产者在生产消息时,可选择是否等待全部副本消息确认,如acks=all。当选择等待全部副本做消息确认时,当2个副本中的1个副本挂了,即使配置了acks=all, 生产者生产消息也会成功。这里的“所有副本”指活着的副本和leader。但如果leader都挂了,那么生产者写入的消息就会丢失了。
3)kafka提供担保,在任何时候,只要至少一个同步副本活着,消息就不会丢。
4)在kafka短暂的故障转移期间,如leader挂了,转移到其副本,此时失败的节点仍可用(因为可访问其副本)。
5)kafka集群需要管理成百上千个partition, kafka会在集群内的所有broker上自动负载均衡所有的partition, 避免高热点的topic只落到少数几个broker上。

3. kafka的leader选举原理

1. 基本概念

1)Quorum:参议人数
指为了做决议,拥有做出决定权而必须出息的参议员的数量(一般指半数以上)
2)副本日志
kafka partition的核心是副本日志,kafka副本日志指需要复制leader的日志。
3)leader选举
当leader故障了,需要从follower中选出新的leader,但follower自己可能落后于leader或者已挂掉,所以要选出最新的follower作为leader。
如果kafka告诉生产者消息已发送,但此时leader故障了,那么新的leader必须要有这条消息。这样,如果leader在等待更多的follower同步完这条消息后,再应答生产者消息发送成功的话,虽然性能慢,但会有更多有资格成为leader的follower。这需要在性能和高可用之间做以权衡。
在有资格成为leader的follower之间,通过日志的数量做比较,选出一个Leader,那么这些有资格成为leader的follower的数目,就是所谓的quorum(法定人数)。

2. kafka为何不通过多数投票来选leader?
1)kafka要提供消息保障,有资格被选为leader的副本,必须包含了所有已提交的消息。
2)多数投票方式的缺点是:在副本数不够时,会让你没有follower可选,通常要容忍1个故障需要3个副本,2个故障需要5个副本。这样副本太多,浪费broker资源。

实际经验告诉我们,单个副本是不够的,但5个副本对于海量数据来说太浪费了。所以选举的方式对于海量数据的存储应用来说不适用。

3)kafka的leader选举方式:

kafka动态维护了一组同步leader数据的副本(即所谓的ISR,in-sync replicas),只有这个组的follower才有资格当选leader,kafka副本写入不被认为是已提交,只有所有的ISR副本都已经接收完才认为是已提交。这组ISR保存在zookeeper中。
有了这个方式,kafka的topic才可以容忍N个副本失败而不会丢消息,这里的N是指RSI中的副本数,实际上算是leader,一共有N+1份。kafka要求一个副本在加入ISR之前,必须再次完全重新同步,确保数据的一致性。
这种方式还有个非常好的特性:仅依赖于速度最快的服务器。

4)kafka选举leader过程的优化

关于Leader的选举,较差的实现方式是:当节点故障,Leader将在运行中的所有partition中选一个节点来顶替。
kafka的实现方式是:选出一个broker来当“控制器”,这个控制器来检查broker的故障级别,以及负责改变所有故障的broker中的受影响的leader的partition,这样的好处是,可批量处理多个需要Leader变更的partition,使得选举更快。
由于kafka broker集群由zk管理,当所有的broker去zk上注册时,只有一个可注册成功,这个成功的就会成为kafka broker controller。而其他的broker叫做kafka broker follower。

选出controller后,由controller从所有ISR中根据算法选出leader。

5)如果所有的ISR都挂了怎么办?

多数投票的方式也会遇到这个问题。对于kafka可以有两种选择:

  • 等待ISR中的副本起死回生,并选择该副本作为leader(希望它仍有数据)
  • 选择第一个副本(它不在ISR中)作为leader。

这就是在可用性和一致性之间做权衡:
如果等待ISR中的副本,那么只要副本不可用,则保持不可用,如果这些副本数据已经丢失,那么就永久不可用。
如果non-in-sync(非ISR)的副本,让它成为leader, 则无法保证消息不丢失。

实际上,kafka选择的是第二种方式,即所有ISR都死了时,选择非ISR中的副本作为leader。

6)持久性保障

对于特别看重持久性保障的场景,kafka提供了2种配置方式:

  • 禁用unclean leader选举:如果ISR中所有副本都不可用,则保持分区不可用。即宁愿不可用,也不冒着丢消息的风险。例如,配置为2时,如果ISR中副本小于2,则不允许写入。
  • 指定一个最小的ISR大小:如果ISR的大小高于最小值,则该分区才接受写入,这样可防止消息写到单个副本上,而变为不可用的风险。如果生产者使用的是acks=all,并保证最少这些ISR副本已确认,则设置才生效。
    min.insync.replicas这个参数设定ISR中最小副本数是多少,默认值为1.如果要提高数据的可靠性,在设置request.required.acks=-1的同时(即acks=all),也要min.insync.replicas这个参数的配合,这样才能发挥最大功效。

6. kafka日志压缩原理

1. 概述

日志不可能无限制的保留,完整的日志对单条记录更新多次的系统是不实用的。一个简单的机制是扔掉旧日志,但缺点是无法恢复了。
kafka的日志压缩:可确保kafka始终保留topic分区中每条消息的key的最后的值,这样意味着下游消费者可以获得最终的消息,而无需拿到所有变化的消息的信息。

2. kafka日志压缩的过程

kafka可以为每个topic的消息设置不同粒度的保留,如可设置大小、时间,即消息总量到达多大后删除,或多久之前的消息删除。也可以为日志文件选择其他压缩方式。
当应用程序崩溃或系统故障后还原,都可以通过压缩的日志来恢复到故障前。
kafka日志逻辑图,展示了kafka日志压缩的每条消息的offset的逻辑结构:
在这里插入图片描述

中间的数字是offset, 97位置是写入日志的地方,cleaner point是清除日志的地方。

kafka的日志原本是连续的offset, 经压缩后的日志,其尾部的偏移量仍保留原来的位置不会改变。但对于已经压缩的部分,如上图上的36,37,38都是等效的位置,读这些offset返回的结果都是从38位置开始。

要是在后台通过定期重新复制日志segment来完成的。清洗时不会阻塞读,可以限流IO吞吐量,以避免影响生产者和消费者。

kafka日志做了删除操作后,实际的压缩日志如下图:
在这里插入图片描述
可以看到,对于k1, k2, k3,每次压缩后都只留下最后的位置。

3. kafka日志压缩的保障

1)topic使用min.compaction.lag.ms来保障消息写入之前必须经过的最小时间长度,才能被压缩。
2)消息压缩时,不会改变原有日志中消息的排序,只是删除了一些。
3)消息的offset永远不会改变。
4)从日志开头进行消费的所有消费者,将至少看到其按顺序写入的最终状态的消息。如果删除日志与读取消息同时发生,将优先让消费者读取消息。

4. 日志压缩的工作方式

日志cleaner负责处理日志压缩,后台线程池重新复制日志segment文件,移除在日志Head中出现的消息,每个压缩线程工作方式如下:

  • 选择log head(日志头)到log tail(日志尾)比率最高的日志
  • 在head日志中找到每个key最后的offset
  • 把前面重复出现的key删除,把最新的、干净的segment复制到新的日志中。
  • 日志head本质是一个密集连续的hash表,每个entry使用固定的24 byte。这样,8 GB 的cleaner buffer一次迭代可清理大约366 GB 的日志(假设消息是1k)
5. 配置log cleaner

1)log cleaner默认是启动的,默认也会启动cleaner线程池,可以针对topic进行配置。log.cleanup.policy=compact,可以在创建topic时或只有alter topic时指定。
2)log cleaner可以配置保留日志“head"不压缩的最小数,通过设置压缩时间如:log.cleaner.min.compaction.lag.ms。这表明消息在最小的时间内不会被压缩。

附录:kafka什么场景下会丢消息

1. 网络异常

acks设置为0时,不和Kafka集群进行消息接受确认,当网络发生异常等情况时,存在消息丢失的可能;

2. 客户端异常

异步发送时,消息并没有直接发送至Kafka集群,而是在Client端按一定规则缓存并批量发送。在这期间,如果客户端发生死机等情况,都会导致消息的丢失;

3. 缓冲区满了

异步发送时,Client端缓存的消息超出了缓冲池的大小,也存在消息丢失的可能;

4. leader副本异常

acks设置为1时,Leader副本接收成功,Kafka集群就返回成功确认信息,而Follower副本可能还在同步。这时Leader副本突然出现异常,新Leader副本(原Follower副本)未能和其保持一致,就会出现消息丢失的情况;

5. 消费消息后没有业务处理就commit了

当消费kafka后,放入异步队列里等待业务处理时,这时要禁止自动提交,等业务处理后,再手动提交。这样可避免丢消息。

6、broker异步刷盘

kafka的broker写入到page cache就算成功,但是,page cache还是在内存中,并不会马上刷盘到磁盘中,如果在刷磁盘之前断电了,也会导致丢数据。

==========================
kafka问题:

  1. kafka如果在flush到磁盘前,重启数据是否会丢失?是否可通过冗余来实现数据不丢失
    【pache cache不属于JVM堆内存,进程重启后不会丢;但page cahe属于操作系统内存,断电后会丢。】
  2. 最佳实践:
    1)设置多少个Partition与consumer最合适?【默认broker和partition数量相同,通常来说,一个partition可承受10MB/s的写入吞吐】
    2)是否有需要partition缩容的场景?缩容会有什么问题?【partition不允许缩容,只能新增partition数】
    3)当partition扩容时,怎样提高consumer个数,是否有最佳实践方案(最高吞吐:同一个topic下,partition个数与consumer个数相等)
    4)当业务处理的速度较慢时,是消费后扔到队列中异步处理,等真正消费完再处理好?还是扔到队列后先SyncCommit再让队列慢慢处理好?kafka何时syncCommit合适?【可扔到队列中异步处理,然后继续消费(消费进度自己控制),但不要马上提交offset, 等异步处理完每条消息,再提交offset】
    5)有哪些场景会丢消息,是否有最佳实践来避免或解决此类问题?
    网络异常(acks=0时,不和kafka进行消息接收确认,当网络异常时,这条消息会丢)
    客户端异常(如客户端发消息前通过队列异步发送,此时如果客户端服务器宕机,队列中的消息就会丢:从客户端producer角度看)
    leader副本异常(acks=1时,leader副本收消息成功,则返回客户端发消息成功;而此时可能follower副本孩子同步,如果此时leader副本司机,这条消息就会丢)
    消费消息后没有业务处理就SyncCommit了。
  3. kafka是否可以做到确保消息不丢不重不乱序?怎样做到的?
    不丢:担保语义:最多一次、至少一次(kafka默认)、正好一次。
    不重:最少一次+唯一消息ID(kafka会默认给每条消息分配个唯一ID吗)
    不乱序:同一个producer落到同一个partition

关于kafka broker扩容,做数据迁移的做法:
https://cloud.tencent.com/developer/article/1866090

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值