Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像Hadoop一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。
1.1 kafka具有以下特性:
1.2 Kafka的使用场景:
1.1 下载
zk下载:https://zookeeper.apache.org/releases.html
kafka下载:https://kafka.apache.org/downloads
1.2 启动
前提:JDK环境安装配置好
zk启动执行:zkServer.cmd
kafka启动执行:kafka-server-start.bat
配置文件主要为server.properties;如果自定义了zk端口等需要同步更改这里面的zk配置,包括其他一些kafka的配置也是从这里改,这里说一下kafka的数据目录参数(名字叫log.dirs其实是kafka的数据保存目录并非是日志文件目录) 如:log.dirs=/tmp/kafka-logs 我们可以自定义数据目录方便熟悉kafka是如何保存数据的。
Producer:Producer即生产者,消息的产生者,是消息的入口。
Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等……
Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。
Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹!
Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上- 位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。
Message:每一条发送的消息主体。
Consumer:消费者,即消息的消费方,是消息的出口。
Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!
Zookeeper:kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性。
3.2 同一个消费者组中一个分区只能被一个消费者消费,一个消费者可以消费多个分区的数据。每个分区有自己的offset
3.3 kafka的数据文件
kafka的数据文件保存在server.properties文件中,log.dirs指定的目录下
其中.index文件保存着.log文件对应的消息的稀疏索引类似于跳表,可以快速定位到某个offset的数据在数据文件中的位置;.log文件达到一定大小后,新的消息会重新生成一个.log文件,log文件名称就是该文件保存的消息的初始offset前面填充0;
3.4 消息的查找过程:
1、 先找到offset的368801message所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件。
2、 打开找到的segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。
由于该文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。
3、 根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。
这套机制是建立在offset为有序的基础上,利用segment+有序offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!至此,消费者就能拿到需要处理的数据进行处理了。那每个消费者又是怎么记录自己消费的位置呢?
在早期的版本中,消费者将消费到的offset维护zookeeper中,consumer每间隔一段时间上报一次,这里容易导致重复消费,且性能不好!在新的版本中消费者消费到的offset已经直接维护在kafk集群的__consumer_offsets这个topic中。
Kafka中主题的每个Partition有一个预写式日志文件,每个Partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到Partition中,Partition中的每个消息都有一个连续的序列号叫做offset, 确定它在分区日志中唯一的位置。、
Kafka每个topic的partition有N个副本,其中N是topic的复制因子。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个Broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的预写式日志有序地写到其他节点上。N个replicas中。其中一个replica为leader,其他都为follower,leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据,以保证leader挂掉之后follower可以被选为主。
4.1 副本同步队列 ISR (in-Sync Replicas)
所谓同步,必须满足如下两个条件:
默认情况下Kafka对应的topic的replica数量为1,即每个partition都有一个唯一的leader,为了确保消息的可靠性,通常应用中将其值(由broker的参数offsets.topic.replication.factor指定)大小设置为大于1,比如3。 所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟。任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。
4.2 HW和LEO
HW俗称高水位,是HighWatermark的缩写,取一个partition对应的ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置。另外每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。对于来自内部broKer的读取请求,没有HW的限制。
hw和leo只是为了保证副本之间数据的同步性,不是为了保证数据不丢失。
4.3 ack机制
Kafka的ack机制,指的是producer的消息发送确认机制,这直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像鱼与熊掌不可兼得,只能平衡。
ack有3个可选值,分别是1,0,-1。
5.1顺序写磁盘
将写磁盘的过程变为顺序写,可极大提高对磁盘的利用率。
5.2.充分利用PageCache
使用Page Cache的好处如下:
1.I/O Scheduler会将连续的小块写组装成大块的物理写从而提高性能;
2.I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间;
3.充分利用所有空闲内存(非JVM内存)。如果使用应用层Cache(即JVM堆内存),会增加GC负担;
4.读操作可直接在Page Cache内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过Page Cache)交换数据;
5.如果进程重启,JVM内的Cache会失效,但Page Cache仍然可用。
Broker收到数据后,写磁盘时只是将数据写入Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由Kafka层面的Replication机制去解决。如果为了保证这种情况下数据不丢失而强制将Page Cache中的数据Flush到磁盘,反而会降低性能。也正因如此,Kafka虽然提供了flush.messages和flush.ms两个参数将Page Cache中的数据强制Flush到磁盘,但是Kafka并不建议使用。如果数据消费速度与生产速度相当,甚至不需要通过物理磁盘交换数据,而是直接通过Page Cache交换数据。同时,Follower从Leader Fetch数据时,也可通过Page Cache完成。
5.3.支持多Disk Drive
5.4 零拷贝
6.1需要知道的几点
自动提交是按时间段自动提交offset,其提交的是pool收到的消息的最后一个offset,如果拉取了100条消息还没处理完,此时执行了自动提交后机器宕机,则会丢失数据;自动提交默认是开启的,可以使用如下代码禁用:props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, “false”);
rebalance后消费者会从分区已提交的offset位点继续消费
auto.offset.reset:
- earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费;
- latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
- none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
使用kafka需引入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.0.1</version>
</dependency>
6.2 生产者代码
public class MyKafkaProducer {
public static void main(String[] args) throws Exception, InterruptedException {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
props.put(ProducerConfig.ACKS_CONFIG, "all");// leader 确认机制 0 1 all
props.put(ProducerConfig.RETRIES_CONFIG, 1);// 重试次数
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);// 生产者批发送大小
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);// 生产者达不到批发送大小,最短等待时间
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);// RecordAccumulator 缓冲区大小
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// key的序列化器
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// value的序列化器
Producer<String, String> producer = new KafkaProducer<>(props);
try {
for (int i = 0; i < 100; i++) {
//指定了key会按keyHash对分区数取余决定发到哪个分区,没有指定key会自动生成一个自增id
ProducerRecord<String, String> record = new ProducerRecord<>("testoffset", Integer.toString(i), Integer.toString(i));
//默认都是异步发送,如果想同步发送producer.send(xxx).get()单条发送
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception exception) {
if (exception != null) {
//当存在异常的时候,失败逻辑
System.out.println("发送异常");
} else {
//发送成功之后这里的RecordMetadata内包含此条消息写入到JDQ集群的分区和对应的位点信息等
System.out.println("success");
}
}
});
}
/**
* flush : 执行flush代表内存中的消息数据都会send到服务端,可根据callback来判断成功与否
* 自己控制flush
* 注意:这里可发送一批数据之后再掉flush,达到批量的效果
*/
producer.flush();
} finally {
producer.close(); //关闭消费者
}
}
}
6.3 消费者代码
public class MyKafkaConsumer {
/**
* 参考方法:
*
* consumer.partitionsFor(topic)
* 查询topic的分区信息,当本地没有这个topic的元数据信息的时候会往服务端发送的远程请求
* 注意: 没有权限的topic的时候会抛出异常(org.apache.kafka.common.errors.TopicAuthorizationException)
* consumer.assignment()
* 查询指定方式或订阅方式分配到当前消费者的分区信息,还未订阅或重新分配时为空
*
* consumer.position(new TopicPartition(topic, 0))
* 获取下次拉取的数据的offset, 如果没有offset记录则会抛出异常
*
* consumer.committed(new TopicPartition(topic, 0))
* 获取已提交的位点信息,如果没有查询到则返回null
*
* consumer.beginningOffsets(Arrays.asList(new TopicPartition(topic, 0)));
* consumer.endOffsets(Arrays.asList(new TopicPartition(topic, 0)));
* consumer.offsetsForTimes(timestampsToSearch);
* 查询最小,最大,或者任意时间的位点信息
*
* consumer.seek(new TopicPartition(topic, 0), 10972);
* consumer.seekToBeginning(Arrays.asList(new TopicPartition(topic, 0)));
* consumer.seekToEnd(Arrays.asList(new TopicPartition(topic, 0)));
* 指定offset消费,seek调用需要写到ConsumerRebalanceListener中否则会报错No current assignment for partition
* 或者可以使用assign手动分配分区就可以直接使用seek方法,但是assign不会进行rebalance
*
* consumer.assign(Arrays.asList(new TopicPartition(topic, 0)));
* 手动分配消费的topic和分区进行消费,这里不会出发group management操作,指定分区消费数据
* 和subscribe方法互斥,如果assign 之后或者之后调用subscribe 则会报错,不允许再进行分配,2方法不能一起使用
*
* consumer.subscribe(topics);
* 自动发布消费的topic进行消费,这里触发group management操作
* 和assign方法互斥,如果subscribe 之后或者之后调用assign 则会报错,不允许再进行分配,2方法不能一起使用
*
* 注: group management
* 根据group 进行topic分区内部的消费rebanlance
* 例如消费的topic包含3个分区,启动了4个相同鉴权的客户端消费
* 分区0 -- consumer1 分区1 -- consumer2 分区2 --- consumer3 consumer4则会空跑不消费数据
* 当分区consumer1挂掉的时候则会出现rebalance,之后变为
* 分区0 -- consumer2 分区1 -- consumer3 分区2 --- consumer4
*/
private static AtomicBoolean running = new AtomicBoolean(true);
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");//
props.put(ConsumerConfig.GROUP_ID_CONFIG, "miner"); //消费者组
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); //自动提交
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "1"); //每次最多拉取1条
//(1)实际应用中,消费到的数据处理时长不宜超过max.poll.interval.ms,否则会触发rebalance
//(2)如果处理消费到的数据耗时,可以尝试通过减小max.poll.records的方式减小单次拉取的记录数(默认是500条)
//指定consumer两次poll的最大时间间隔(默认5分钟),如果超过了该间隔consumer client会主动向coordinator发起LeaveGroup请求,触发rebalance;然后consumer重新发送JoinGroup请求
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "10000"); //两次pool最大时间间隔
//props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); //自动提交最短时间
//key反序列化类
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
//value反序列化类
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
Collection<String> topics = Arrays.asList("testoffset");
consumer.subscribe(topics, new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> collection) {
System.out.println("====rebalance前");
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> collection) {
System.out.println("====rebalance后");
for (TopicPartition topicPartition:collection) {
//指定offset拉取
consumer.seekToBeginning(Collections.singletonList(new TopicPartition("testoffset", 0)));
}
}
});
//手动指定分区方式订阅
/*List list = Collections.singletonList(new TopicPartition("testoffset", 0));
consumer.assign(list);
List partitionInfos = consumer.partitionsFor("testoffset");
for (PartitionInfo partitionInfo:partitionInfos) {
int partitionNumber = partitionInfo.partition();
OffsetAndMetadata offsetAndMetadata = consumer.committed(new TopicPartition("testoffset", partitionNumber));
long offset = offsetAndMetadata.offset();
TopicPartition topicPartition = new TopicPartition("testoffset", partitionNumber);
consumer.seek(topicPartition, 0);
}*/
//查看已提交的offset
/* OffsetAndMetadata testoffset = consumer.committed(new TopicPartition("testoffset", 0));
System.out.println("===1:" + testoffset.metadata());
System.out.println("===2:" + testoffset.offset());*/
while (true) {
//没有分到分区的消费者record size=0
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(3)); //拉取数据
if (records != null && !records.isEmpty()) {
for (ConsumerRecord<String, String> record : records) {
System.out.printf("====:partition = %s,offset = %d, key = %s, value= %s%n", record.partition(), record.offset(), record.key(), record.value());
}
}
Thread.sleep(1000);
if(!running.get()){
break;
}
}
//异步提交
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
}
});
consumer.close();
}
}