kafka

kafka

笔记来源于尚硅谷的kafka视频,加上自己和网上收集的一些整理

MQ传统应用场景之异步处理

image-20201229094728400

有点不严谨

消息队列的两种模式

image-20201229095020934

(2)发布/订阅模式(一对多,消费者消费数据之后不会消除消息)又分为推模式和拉模式,推模式需要考虑到消费者 消费的能力,而拉模式的话需要一个长时间的轮询去问kafka有没有堆积消息,这个是拉模式的弊端

​ 消息生产者(发布)将信息发布到topic中,同时有多个消息消费者(订阅)消费该消息,和点对点方式不同,发布到topic的消息会被所有订阅者消费

image-20201229095242526

定义

image-20201229095415511

基础架构

image-20201229095530964

image-20210919150533161

**备份指的是leader和follwer 对leader也是备份 **

首先,从数据组织形式来说,kafka有三层形式,kafka有多个主题,每个主题有多个分区,每个分区又有多条消息。

而每个分区可以分布到不同的机器上,这样一来,从服务端来说,分区可以实现高伸缩性,以及负载均衡,动态调节的能力。

===================================================================================

每一个节点都会有其他节点的备份(和ES的分片集群一样),仅仅是leader TopicA-Partition0 这种才提供服务,备份就仅仅是备份而已

===================================================================================

分区 Partition:内容数据都不同

会为每个分区提供一个副本存在不相邻的节点broker

副本是不会工作的(*)

===================================================================================

3个区只能3个节点(有问题 不是这样子的)

某一个分区(Partition)只能被某一个分组里面的某一个消费者消费

消费组:例如我有两个消费者形成一个消费组去订阅一个主题 那么就可以提高消费能力

里面消费里面的分区数据 最好对应分区的数量,也有不对应的情况出现

消费组可以提高消费能力

isr

kafka的ISR机制被成为“不丢消息”机制。在说ISR机制前,先讲一下kafka的副本(replica)。

kafka的Replica

1.kafka的topic可以设置有N个副本(replica),副本数最好要小于broker的数量,也就是要保证一个broker上的replica最多有一个,所以可以用broker id指定Partition replica。

2.创建副本的单位是topic的分区,每个分区有1个leader和0到多个follower,我们把多个replica分为Lerder replica和follower replica。

3.当producer在向partition中写数据时,根据ack机制,默认ack=1,只会向leader中写入数据,然后leader中的数据会复制到其他的replica中,follower会周期性的从leader中pull数据,但是对于数据的读写操作都在leader replica中,follower副本只是当leader副本挂了后才重新选取leader,follower并不向外提供服务。

kafka的“同步”

kafka不是完全同步,也不是完全异步,是一种特殊的ISR(In Sync Replica)

1.leader会维持一个与其保持同步的replica集合,该集合就是ISR,每一个partition都有一个ISR,它时有leader动态维护。

2.我们要保证kafka不丢失message,就要保证ISR这组集合存活(至少有一个存活),并且消息commit成功。

所以我们判定存活的概念时什么呢?分布式消息系统对一个节点是否存活有这样两个条件判断:第一个,节点必须维护和zookeeper的连接,zookeeper通过心跳机制检查每个节点的连接;第二个,如果节点时follower,它必要能及时同步与leader的写操作,不是延时太久。

如果满足上面2个条件,就可以说节点时“in-sync“(同步中的)。leader会追踪”同步中的“节点,如果有节点挂了,卡了,或延时太久,那么leader会它移除,延时的时间由参数replica.log.max.messages决定,判断是不是卡住了,由参数replica.log.time.max.ms决定。

kafka的commit是由ack机制决定的,下一节再讲ack机制。

https://www.cnblogs.com/listenfwind/p/12465409.html

topic中的数据分割成一个或多个分区partition

偏移量:就是分区里面的数据第几个 比如说分区0 里面的第二个 必须是顺序读取 例如偏移量3还没读你就不能读0分区下的第四个

cd kafka_2.12-2.2.0
# & 为后台启动
#启动zookeeper服务
bin/zookeeper-server-start.sh config/zookeeper.properties &
#启动kafka
bin/kafka-server-start.sh config/server.properties &

查看端口

netstat -nltp

image-20201229104132306

kafaka命令行(少用)

image-20201229104325830

kafka工作流程及文件存储机制

image-20201229110108533

partition才是最基本的存储单位、一个topic可以分多个partition

image-20210919201707700

  • Broker:消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群;
  • Topic:一类消息,例如page view日志、click日志等都可以以topic的形式存在,Kafka集群能够同时负责多个topic的分发;
  • Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队;
  • Segment:每个partition又由多个segment file组成;
  • offset:每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于partition唯一标识一条消息(相当于索引);
  • message:这个算是kafka文件中最小的存储单位,即是 a commit log。

一个partition就是一个追加的文件,只要你来了新的数据,它就会写在这个文件的后边

时间offset 位置offset 和文件构成一个partition

每一个partition(分区)可以有多个副本

说完了分区,再来说说副本。先说说副本的基本内容,在kafka中,每个主题可以有多个分区,每个分区又可以有多个副本。这多个副本中,只有一个是leader,而其他的都是follower副本。仅有leader副本可以对外提供服务。

多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。

这里通过问题来整理这部分内容。

kafka的副本都有哪些作用?
在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是从leader副本拉取消息,尽量让自己跟leader副本的内容一致。

说说follower副本为什么不对外提供服务?
这个问题本质上是对性能和一致性的取舍。试想一下,如果follower副本也对外提供服务那会怎么样呢?首先,性能是肯定会有所提升的。但同时,会出现一系列问题。类似数据库事务中的幻读,脏读。

比如你现在写入一条数据到kafka主题a,消费者b从主题a消费数据,却发现消费不到,因为消费者b去读取的那个分区副本中,最新消息还没写入。而这个时候,另一个消费者c却可以消费到最新那条数据,因为它消费了leader副本
 看吧,为了提高那么些性能而导致出现数据不一致问题,那显然是不值得的。

当你的数据越来越多的时候,kafka会自动追加一个新的文件就是segment,防止文件越来越大 segment.bytes 默认的大小是一个G

默认是一个星期清理一次文件、可以自己在yml中配置

image-20201229111617495

image-20201229111630404

image-20210919203048336

image-20201229111639359

image-20201229111946204

每个partion对应多个segment对应一个.index文件和.log .index存放了具体数据的索引,.index文件里面使用的是二分查找法来进行查找 ,

kafka生产者

分区策略

分区的原因

image-20201229112606123

image-20201229113109757

.

kafka是异步通信

数据可靠性的保证

image-20201229114512689

image-20201229114529360

kafka选择的是全部完成同步,才发送ack

image-20201229114544844

image-20201229114555768

image-20201229114606037

image-20201229114613292

image-20201229114631241

image-20201229114638338

3种机制:消息没有写、只要leader已经写完数据了、和所有人都写完数据 就发回执 就是ack 就是上面图的三种情况

follower就是备份

数据在server端的保存机制

kafka默认用的是第三种策略

注意:为了防止某一个节点迟迟不响应leader节点,leader搞了一个小团体机制 ISR (In Sync Replica),只要你这个节点迟迟不响应这个leader ,leader节点就会把不响应的节点提出这个小团体ISR。 只有在ISR团体里面的节点才能有写等权限

注意:节点活不活着不一定代表就在这个ISR团体中 也可能节点没挂 心跳正常、但是不响应leader节点,那这时候isr团体的leader节点就会把他T出去

kafka的ISR机制被成为“不丢消息”机制。在说ISR机制前,先讲一下kafka的副本(replica)。

kafka的Replica

1.kafka的topic可以设置有N个副本(replica),副本数最好要小于broker的数量,也就是要保证一个broker上的replica最多有一个,所以可以用broker id指定Partition replica。

2.创建副本的单位是topic的分区,每个分区有1个leader和0到多个follower,我们把多个replica分为Lerder replica和follower replica。

3.当producer在向partition中写数据时,根据ack机制,默认ack=1,只会向leader中写入数据,然后leader中的数据会复制到其他的replica中,follower会周期性的从leader中pull数据,但是对于数据的读写操作都在leader replica中,follower副本只是当leader副本挂了后才重新选取leader,follower并不向外提供服务。

kafka的“同步”

kafka不是完全同步,也不是完全异步,是一种特殊的ISR(In Sync Replica1.leader会维持一个与其保持同步的replica集合,该集合就是ISR,每一个partition都有一个ISR,它时有leader动态维护。

2.我们要保证kafka不丢失message,就要保证ISR这组集合存活(至少有一个存活),并且消息commit成功。

 

所以我们判定存活的概念时什么呢?分布式消息系统对一个节点是否存活有这样两个条件判断:第一个,节点必须维护和zookeeper的连接,zookeeper通过心跳机制检查每个节点的连接;第二个,如果节点时follower,它必要能及时同步与leader的写操作,不是延时太久。

如果满足上面2个条件,就可以说节点时“in-sync“(同步中的)。leader会追踪”同步中的“节点,如果有节点挂了,卡了,或延时太久,那么leader会它移除,延时的时间由参数replica.log.max.messages决定,判断是不是卡住了,由参数replica.log.time.max.ms决定。

kafka的commit是由ack机制决定的,下一节再讲ack机制。

故障处理细节

image-20201229140731596

image-20201229140828655

所有副本最小的LEO就是HW(high watermark)

所以consumer能消费的数据 只能是HW之前的数据

注意HW之前的数据可靠、

image-20210920151040575

我们的ISR针对的 是 一个partition中有多个副本 而不是针对节点broker的。记住是partition里面的副本

如果ack是1 或者-1 则由可能数据会重复

image-20201229145710065

幂等性

image-20201229150105785

image-20201229150136789

这只能是单次会话,也就是当生产者以及发送一条消息过去后,然后宕机又重新上线了,然后又重复发送刚才一条一模一样的消息,kafka还是会缓存起来,因为它的PID与上一次PID以及不同了,每一个新的启动都会重新分配一个新的PID,所以kafka就会认为这条消息还没有被接受过,所以会再次接受

其实就是通过一个全局唯一的主键来实现的

kafka消费者

消费方式

image-20201229151408968

pull模式是消费者向生产者拉取数据

push模式是生产者向消费者发送数据(主动发送数据)

kafka选择了拉模式 pull模式

而学习过的rabbitmq 拉模式和推模式 都用过。但是推荐使用push模式能实现高吞吐量

分区分配策略

image-20201229152210040

roundrobin是根据组来划分的 而range是根据主题来划分的

image-20201229152222447

roundrobin (轮询) 就像是发牌:必须要保证消费组里面的消费者消费的主题是(即AB消费的主题一样)一样的 否则容易出现这样的情况

image-20210920161121808

因为它会把主题所有都当成一个整体来进行轮询处理

image-20201229152402293

range是以单个主题来进行划分的 只考虑主题只有订阅了的组里面的消费者才能消费这个主题

image-20210920161635775

range J经过计算之后 上面3个下面2个 2个

RangeAssignor策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个topic

kafka默认的消费者的消费策略是range

offset的维护

image-20201229152734046

分组+topic+partition

image-20210920162808266

image-20210920163833561

同一个消费组里面的消费者 假设A消费到了offset3 然后宕机了 B可以加入消费者 继续消费offset4 因为他是以组为单位保存在zk(0.11版本前)或者说是保存在kafka内置的一个_consumer_offsets里面的。

不同组可以同时消费一个分区的内容

kafka高效读写数据

image-20201229153407116

image-20201229154109987

尽管持久化到pagecache上可能会造成宕机丢失数据的情况,但是这可以被kafka的Replication机制解决。如果为了保证这种情况 下数据不丢失而强制将pagecahe中的数据flush到磁盘,反而会降低性能

(3)零复制技术

image-20201229154539558

这三点来保证kafka高效读写数据

zookeeper在kafka中的作用

image-20201229161639189

leader选举的过程

image-20201229161706881

kafka事务

image-20210920195953749

这个TransactionID是客户端给的

image-20210920200513554

kafka api

Producer api

image-20210920201224497

image-20210920201532543

partitioner

package com.kkw.partitioner;

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

import java.util.Map;

/**
 * @author Kevin
 * @date 2021-09-20
 **/
public class MyPartitioner implements Partitioner {

    @Override
    public void configure(Map<String, ?> configs) {
        // TODO Auto-generated method stub

    }

    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void close() {
        // TODO Auto-generated method stub

    }

}
package com.kkw.partitioner.mypartitioner_producer;

import com.kkw.partitioner.MyPartitioner;
import org.apache.kafka.clients.producer.*;

import java.util.Properties;

/**kafka 生产者
 * @author Kevin
 * @date 2021-09-20
 **/
public class MyProducer {

    public static void main(String[] args) {
        //1、创建Kafka生产者的配置信息
        Properties properties = new Properties();
        //2、指定连接的Kafka集群
        properties.put("bootstrap.servers","127.0.0.1:9092");
        //3、ACK应答级别
        properties.put("acks","all");
        //4、重试次数
        properties.put("retries",3);
        //5、批次大小  一次发送数据的大小
        properties.put("batch.size",16384);
        //6、等待时间
        properties.put("linger.ms",1);
        //7、RecordAccumulator缓冲区大小
        properties.put("buffer.memory", 33554432);
        //8、key value的序列化类
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        //自定义分区器
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class);

        //9、创建生产者对象
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("test", "test-" + Integer.toString(i),
                    "test-" + Integer.toString(i)), new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    System.out.println("发送成功啦");
                }
            });
        }
        producer.close();
    }
}

Consumer

package com.kkw.consumer;

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.util.Arrays;
import java.util.Collections;
import java.util.Properties;

/**
 * @author Kevin
 * @date 2021-09-20
 **/
public class MyConsumer {
    public static void main(String[] args) {

        Properties props = new Properties();

        props.put("bootstrap.servers", "127.0.0.1:9092");
        props.put("group.id", "abc");
        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(Collections.singletonList("test"));

        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());
            }
        }
    }
}

Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。

由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以** consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费**。

所以 offset 的维护是 Consumer 消费数据是必须考虑的问题

public static final String AUTO_OFFSET_RESET_CONFIG = "auto.offset.reset";
Properties props = new Properties();
...
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put("group.id", "abcd");//组id需另设,否则看不出上面一句的配置效果
...
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

Kafka案例_消费者保存offset读取问题

props.put("enable.auto.commit", "true");

enable.auto.commit

If true the consumer’s offset will be periodically committed in the background.

TYPE:boolean

DEFAULT:true

Source

PS.我将Offset提交类比成数据库事务的提交。

虽然自动提交 offset 十分便利,但由于其是基于时间提交的, 开发人员难以把握offset 提交的时机。因此 Kafka 还提供了手动提交 offset 的 API

手动提交 offset 的方法有两种:

  1. commitSync(同步提交)
  2. commitAsync(异步提交)

两者的相同点是,都会将本次 poll 的一批数据最高的偏移量提交;

不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而 commitAsync 则没有失败重试机制,故有可能提交失败。

同步提交offset

由于同步提交 offset 有失败重试机制,故更加可靠,以下为同步提交 offset 的示例。

SyncCommitOffset.java

public class SyncCommitOffset {
	public static void main(String[] args) {
		Properties props = new Properties();
		...
		//<-----------------
		//关闭自动提交 offset
		props.put("enable.auto.commit", "false");
		...
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
		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());
			}
			//<---------------------------------------
			//同步提交,当前线程会阻塞直到 offset 提交成功
			consumer.commitSync();
		}
	}
}

异步提交offset

虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。

AsyncCommitOffset.java

public class AsyncCommitOffset {
	public static void main(String[] args) {
		Properties props = new Properties();
		...
		//<--------------------------------------
		//关闭自动提交
		props.put("enable.auto.commit", "false");
		...
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		consumer.subscribe(Arrays.asList("first"));// 消费者订阅主题
		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());
			}
			//<----------------------------------------------
			// 异步提交
			consumer.commitAsync(new OffsetCommitCallback() {
				@Override
				public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
					if (exception != null) {
						System.err.println("Commit failed for" + offsets);
					}
				}
			});
		}
	}
}

数据漏消费和重复消费分析

无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。先提交 offset 后消费,有可能造成数据的漏消费;而先消费后提交 offset,有可能会造成数据的重复消费。

自定义存储 offset

Kafka 0.9 版本之前, offset 存储在 zookeeper, 0.9 版本及之后,默认将 offset 存储在 Kafka的一个内置的 topic 中。除此之外, Kafka 还可以选择自定义存储 offset。

offset 的维护是相当繁琐的, 因为需要考虑到消费者的 Rebalace。

当有新的消费者加入消费者组、 已有的消费者推出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance

消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费

要实现自定义存储 offset,需要借助 ConsumerRebalanceListener, 以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现。(可将offset存入MySQL数据库)

CustomSaveOffset.java

public class CustomSaveOffset {
	private static Map<TopicPartition, Long> currentOffset = new HashMap<>();

	public static void main(String[] args) {
		// 创建配置信息
		Properties props = new Properties();
		...
		//<--------------------------------------
		// 关闭自动提交 offset
		props.put("enable.auto.commit", "false");
		...
		// 创建一个消费者
		KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
		// 消费者订阅主题
		consumer.subscribe(Arrays.asList("first"), 
			//<-------------------------------------
			new ConsumerRebalanceListener() {
			// 该方法会在 Rebalance 之前调用
			@Override
			public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
				commitOffset(currentOffset);
			}

			// 该方法会在 Rebalance 之后调用
			@Override
			public void onPartitionsAssigned(Collection<TopicPartition> partitions) {

				currentOffset.clear();
				for (TopicPartition partition : partitions) {
					consumer.seek(partition, getOffset(partition));// 定位到最近提交的 offset 位置继续消费
				}
			}
		});
		
		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());
				currentOffset.put(new TopicPartition(record.topic(), record.partition()), record.offset());
			}
			commitOffset(currentOffset);// 异步提交
		}
	}

	// 获取某分区的最新 offset
	private static long getOffset(TopicPartition partition) {
        //自己实现 因为是自定义的 可以放在mysql 从mysql里面查出来也可以
		return 0;
	}

	// 提交该消费者所有分区的 offset
	private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
	}
}

Kafka案例_API自定义拦截器(需求分析)

拦截器原理

Producer 拦截器(interceptor)是在 Kafka 0.10 版本被引入的,主要用于实现 clients 端的定制化控制逻辑。

对于 producer 而言, interceptor 使得用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时, producer 允许用户指定多个 interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。 Intercetpor 的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:

  • configure(configs):获取配置信息和初始化数据时调用。
  • onSend(ProducerRecord):该方法封装进 KafkaProducer.send 方法中,即它运行在用户主线程中。 Producer 确保在消息被序列化以及计算分区前调用该方法。 用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的 topic 和分区, 否则会影响目标分区的计算。
  • onAcknowledgement(RecordMetadata, Exception)该方法会在消息从 RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送过程中失败时调用。 并且通常都是在 producer 回调逻辑触发之前。 onAcknowledgement 运行在producer 的 IO 线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢 producer 的消息发送效率。
  • close():关闭 interceptor,主要用于执行一些资源清理工作

如前所述, interceptor 可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个 interceptor,则 producer 将按照指定顺序调用它们,并仅仅是捕获每个 interceptor 可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。

Kafka案例_API自定义拦截器(代码实现)

需求

实现一个简单的双 interceptor 组成的拦截链。

  • 第一个 interceptor 会在消息发送前将时间戳信息加到消息 value 的最前部;
  • 第二个 interceptor 会在消息发送后更新成功发送消息数或失败发送消息数。

案例实操

增加时间戳拦截器

TimeInterceptor.java

import java.util.Map;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

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

	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		// 创建一个新的 record,把时间戳写入消息体的最前部
		return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
				"TimeInterceptor: " + System.currentTimeMillis() + "," + record.value().toString());
	}

	@Override
	public void close() {
	}

	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
		// TODO Auto-generated method stub
		
	}
}
增加时间戳拦截器

统计发送消息成功和发送失败消息数,并在 producer 关闭时打印这两个计数器

CounterInterceptor.java

import java.util.Map;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

public class CounterInterceptor implements ProducerInterceptor<String, String>{

	private int errorCounter = 0;
	private int successCounter = 0;
	
	@Override
	public void configure(Map<String, ?> configs) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
		return record;
	}

	@Override
	public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
		// 统计成功和失败的次数
		if (exception == null) {
			successCounter++;
		} else {
			errorCounter++;
		}
	}

	@Override
	public void close() {
		// 保存结果
		System.out.println("Successful sent: " + successCounter);
		System.out.println("Failed sent: " + errorCounter);
		
	}

}
producer 主程序

InterceptorProducer.java

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;

public class InterceptorProducer {
	public static void main(String[] args) {
		// 1 设置配置信息
		Properties props = new Properties();
		props.put("bootstrap.servers", "127.0.0.1:9092");
		props.put("acks", "all");
		props.put("retries", 3);
		props.put("batch.size", 16384);
		props.put("linger.ms", 1);
		props.put("buffer.memory", 33554432);
		props.put("key.serializer",
				"org.apache.kafka.common.serialization.StringSerializer");
		props.put("value.serializer",
				"org.apache.kafka.common.serialization.StringSerializer");
		//<--------------------------------------------
		// 2 构建拦截链
		List<String> interceptors = new ArrayList<>();
		interceptors.add("com.lun.kafka.interceptor.TimeInterceptor");
		interceptors.add("com.lun.kafka.interceptor.CounterInterceptor");
		
		props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
		
		String topic = "test";
		Producer<String, String> producer = new KafkaProducer<>(props);
		// 3 发送消息
		for (int i = 0; i < 10; i++) {
			ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i);
			producer.send(record);
		}
		
		// 4 一定要关闭 producer,这样才会调用 interceptor 的 close 方法
		producer.close();
		
	}
}
Kafka案例_API自定义拦截器(案例测试)

img

39.Kafka案例_监控Eagle的安装

什么是Kafka Eagle

Kafka Eagle is open source visualization and management software. It allows you to query, visualize, alert on, and explore your metrics no matter where they are stored. In plain English, it provides you with tools to turn your kafka cluster data into beautiful graphs and visualizations.

Kafka Eagle是开源可视化和管理软件。它允许您查询、可视化、提醒和探索您的指标,无论它们存储在哪里。简单地说,它为您提供了将kafka集群数据转换为漂亮的图形和可视化的工具。

Source

一个运行在Tomcat的Web应用。

安装与运行

在Linux上安装与运行

在Windows上安装与运行
  • 安装JDK,设置环境变量时,路径最好不要带空格,否则,后序运行ke.bat抛异常。若路径必须带有空格,可以通过小技巧,让ke.bat成功运行。这个技巧是:若你的JAVA_HOME的变量值为C:\Program Files\Java\jdk1.8.0_161,则将其改成C:\progra~1\Java\jdk1.8.0_161
  • Kafka Eagle下载页面下载安装包。也可在网盘下载,链接在本文首部
  • 解压安装包,然后设置环境变量KE_HOME,其值如C:\Kafka\kafka-eagle-web-1.3.7。若想打开cmd输入命令ke.bat运行Kafka Eagle,在PATH环境变量的值头添加%KE_HOME%\bin;
  • 修改配置文件%KE_HOME%\conf\system-config.properties
  • (可选)Kafka Server的JVM调参,用文本编辑器打开%KAFKA_HOME%\bin\windows\kafka-server-start.bat,其中的set KAFKA_HEAP_OPTS=-Xmx1G -Xms1G改为set KAFKA_HEAP_OPTS=-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70
######################################
# multi zookeeper&kafka cluster list
######################################<---设置ZooKeeper的IP地址
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=localhost:2181

######################################
# zk client thread limit
######################################
kafka.zk.limit.size=25

######################################
# kafka eagle webui port
######################################
kafka.eagle.webui.port=8048

######################################
# kafka offset storage
######################################
cluster1.kafka.eagle.offset.storage=kafka
#cluster2.kafka.eagle.offset.storage=zk

######################################
# enable kafka metrics
######################################<---metrics.charts从false改成true
kafka.eagle.metrics.charts=true
kafka.eagle.sql.fix.error=false

######################################
# kafka sql topic records max
######################################
kafka.eagle.sql.topic.records.max=5000

######################################
# alarm email configure
######################################
kafka.eagle.mail.enable=false
kafka.eagle.mail.sa=alert_sa@163.com
kafka.eagle.mail.username=alert_sa@163.com
kafka.eagle.mail.password=mqslimczkdqabbbh
kafka.eagle.mail.server.host=smtp.163.com
kafka.eagle.mail.server.port=25

######################################
# alarm im configure
######################################
#kafka.eagle.im.dingding.enable=true
#kafka.eagle.im.dingding.url=https://oapi.dingtalk.com/robot/send?access_token=

#kafka.eagle.im.wechat.enable=true
#kafka.eagle.im.wechat.token=https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=xxx&corpsecret=xxx
#kafka.eagle.im.wechat.url=https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=
#kafka.eagle.im.wechat.touser=
#kafka.eagle.im.wechat.toparty=
#kafka.eagle.im.wechat.totag=
#kafka.eagle.im.wechat.agentid=

######################################
# delete kafka topic token
######################################
kafka.eagle.topic.token=keadmin

######################################
# kafka sasl authenticate
######################################
cluster1.kafka.eagle.sasl.enable=false
cluster1.kafka.eagle.sasl.protocol=SASL_PLAINTEXT
cluster1.kafka.eagle.sasl.mechanism=PLAIN
cluster1.kafka.eagle.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="kafka-eagle";

#cluster2 在此没有用到,将其注释掉
#cluster2.kafka.eagle.sasl.enable=false
#cluster2.kafka.eagle.sasl.protocol=SASL_PLAINTEXT
#cluster2.kafka.eagle.sasl.mechanism=PLAIN
#cluster2.kafka.eagle.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="admin" password="kafka-eagle";

######################################
# kafka jdbc driver address
######################################
kafka.eagle.driver=org.sqlite.JDBC
#将url设置在本地
kafka.eagle.url=jdbc:sqlite:/C:/Kafka/kafka-eagle-web-1.3.7/db/ke.db
#进入系统需要用到的账号与密码
kafka.eagle.username=root
kafka.eagle.password=123456
  • 运行
    • 运行ZooKeeper
    • 运行Kafka集群,另外运行kafka server前,需设置JMX_PORT,否则Kafka Eagle 后台提示连接失败。执行命令行set JMX_PORT=9999 & start bin\windows\kafka-server-start.bat config\server.properties设置JMX_PORT且运行Kafkaserver。在单节点开启Kafka集群,小心端口号冲突。
    • 点击%KE_HOME%\bin\ke.bat,运行Kafka Eagle。
    • 打开浏览器,在地址栏输入http://localhost:8048/ke/,然后在登录页面,输入在配置文件%KE_HOME%\conf\system-config.properties设置的账号与密码。
    • 登录成功,便可进入Kafka Eagle

img

Kafka案例_监控Eagle的使用

QUICK START

Kafka案例_Kafka之与Flume对接

Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集、聚合和传输的系统,Flume支持在日志系统中定制各类数据发送方,用于收集数据;同时,Flume提供对数据进行简单处理,并写到各种数据接受方(可定制)的能力。

Source

kafka面试题

题库
1.Kafka中的ISR(InSyncRepli)、OSR(OutSyncRepli)、AR(AllRepli)代表什么?
ISR : 速率和leader相差低于10秒的follower的集合
OSR : 速率和leader相差大于10秒的follower
AR : 所有分区的follower

2.Kafka中的HW、LEO等分别代表什么?
HW : 又名高水位,根据同一分区中,最低的LEO所决定
LEO : 每个分区的最高offset

3.Kafka的用途有哪些?使用场景如何?
1.用户追踪:根据用户在web或者app上的操作,将这些操作消息记录到各个topic中,然后消费者通过订阅这些消息做实时的分析,或者记录到HDFS,用于离线分析或数据挖掘
2.日志收集:通过kafka对各个服务的日志进行收集,再开放给各个consumer
3.消息系统:缓存消息
4.运营指标:记录运营监控数据,收集操作应用数据的集中反馈,如报错和报告

4.Kafka中是怎么体现消息顺序性的?
每个分区内,每条消息都有offset,所以只能在同一分区内有序,但不同的分区无法做到消息顺序性

5.“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?
对的,超过分区数的消费者就不会再接收数据

  1. 有哪些情形会造成重复消费?或丢失信息?
    先处理后提交offset,会造成重读消费
    先提交offset后处理,会造成数据丢失

7.Kafka 分区的目的?
对于kafka集群来说,分区可以做到负载均衡,对于消费者来说,可以提高并发度,提高读取效率

8.Kafka 的高可靠性是怎么实现的?
为了实现高可靠性,kafka使用了订阅的模式,并使用isr和ack应答机制
能进入isr中的follower和leader之间的速率不会相差10秒
当ack=0时,producer不等待broker的ack,不管数据有没有写入成功,都不再重复发该数据
当ack=1时,broker会等到leader写完数据后,就会向producer发送ack,但不会等follower同步数据,如果这时leader挂掉,producer会对新的leader发送新的数据,在old的leader中不同步的数据就会丢失
当ack=-1或者all时,broker会等到leader和isr中的所有follower都同步完数据,再向producer发送ack,有可能造成数据重复

9.topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
可以增加

bin/kafka-topics.sh --zookeeper localhost:2181/kafka --alter --topic topic-config --partitions 3
1
10.topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
不可以,先有的分区数据难以处理

11.简述Kafka的日志目录结构?
每一个分区对应一个文件夹,命名为topic-0,topic-1,每个文件夹内有.index和.log文件

12.如何解决消费者速率低的问题?
增加分区数和消费者数

13.Kafka的那些设计让它有如此高的性能??
1.kafka是分布式的消息队列
2.对log文件进行了segment,并对segment建立了索引
3.(对于单节点)使用了顺序读写,速度可以达到600M/s
4.引用了zero拷贝,在os系统就完成了读写操作

14.kafka启动不起来的原因?
在关闭kafka时,先关了zookeeper,就会导致kafka下一次启动时,会报节点已存在的错误
只要把zookeeper中的zkdata/version-2的文件夹删除即可

15.聊一聊Kafka Controller的作用?
负责kafka集群的上下线工作,所有topic的副本分区分配和选举leader工作

16.Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
在ISR中需要选择,选择策略为先到先得

17.失效副本是指什么?有那些应对措施?
失效副本为速率比leader相差大于10秒的follower
将失效的follower先提出ISR
等速率接近leader10秒内,再加进ISR

18.Kafka消息是采用Pull模式,还是Push模式?
在producer阶段,是向broker用Push模式
在consumer阶段,是向broker用Pull模式
在Pull模式下,consumer可以根据自身速率选择如何拉取数据,避免了低速率的consumer发生崩溃的问题
但缺点是,consumer要时不时的去询问broker是否有新数据,容易发生死循环,内存溢出

19.Kafka创建Topic时如何将分区放置到不同的Broker中?
首先副本数不能超过broker数
第一分区是随机从Broker中选择一个,然后其他分区相对于0号分区依次向后移
第一个分区是从nextReplicaShift决定的,而这个数也是随机产生的

20.Kafka中的事务是怎么实现的?☆☆☆☆☆
kafka事务有两种
producer事务和consumer事务
producer事务是为了解决kafka跨分区跨会话问题
kafka不能跨分区跨会话的主要问题是每次启动的producer的PID都是系统随机给的
所以为了解决这个问题
我们就要手动给producer一个全局唯一的id,也就是transaction id 简称TID
我们将TID和PID进行绑定,在producer带着TID和PID第一次向broker注册时,broker就会记录TID,并生成一个新的组件__transaction_state用来保存TID的事务状态信息
当producer重启后,就会带着TID和新的PID向broker发起请求,当发现TID一致时
producer就会获取之前的PID,将覆盖掉新的PID,并获取上一次的事务状态信息,从而继续上次工作
consumer事务相对于producer事务就弱一点,需要先确保consumer的消费和提交位置为一致且具有事务功能,才能保证数据的完整,不然会造成数据的丢失或重复

21.Kafka中的分区器、序列化器、拦截器是否了解?它们之间的处理顺序是什么?
拦截器>序列化器>分区器

22.Kafka生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?

使用两个线程:
main线程和sender线程
main线程会依次经过拦截器,序列化器,分区器将数据发送到RecourdAccumlator(线程共享变量)
再由sender线程从RecourdAccumlator中拉取数据发送到kafka broker
相关参数:
batch.size:只有数据积累到batch.size之后,sender才会发送数据。
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。

23.消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
offset + 1
图示:

生产者发送数据offset是从0开始的

消费者消费的数据offset是从offset+1开始的

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值