Kafka1.0日常维护

日常维护操作

4.1 创建topic两个备份6个分区:

./kafka-topics.sh --create --zookeeper app212:2181 --replication-factor 2 --partitions 6 --topic t_test

4.2 查看存在的topic:

./kafka-topics.sh --zookeeper app212:2181 --list

4.3 查看topic详细信息:


./kafka-topics.sh --describe --zookeeper app212:2181 --topic t_test

topic详细信息

4.4 生产数据:

./kafka-console-producer.sh --broker-list localhost:9092 --topic t_test

模拟生产数据

4.5 消费数据:

消费数据:
./kafka-console-consumer.sh --zookeeper app211:2181 --topic t_test

模拟消费数据

4.6 查看topic某分区偏移量最大(小)值

./kafka-run-class.sh kafka.tools.GetOffsetShell --topic t_test  --time -1 --broker-list 10.242.167.216:9092,10.242.167.217:9092,10.242.167.218:9092 --partitions 0
time为-1时表示最大值,time为-2时表示最小值

偏移量

4.7 删除topic

只会删除zookeeper中的元数据,消息文件须手动删除

./kafka-topics.sh --zookeeper app212:2181 --delete --topic t_test

4.8 增加topic分区数(只能增加)

./kafka-topics.sh --zookeeper app212:2181  --alter --topic t_test --partitions 10

4.8.1 查看group消费情况

kafka-consumer-groups.sh --bootstrap-server host:9092 --group group_name --describe

4.9 增加topic副本数

创建partitions-extension-push-token-topic.json
vim /opt/kafka/partitions-extension-push-token-topic.json

{
    "partitions": [
        {
            "topic": "t_test",
            "partition": 0,
            "replicas": [
                4,
                5,
				6
            ]
        },
        {
            "topic": "t_test",
            "partition": 1,
            "replicas": [
                5,
                6,
				1
            ]
        },
        {
            "topic": "t_test",
            "partition": 2,
            "replicas": [
                6,
                1,
				2
            ]
        },
		{
            "topic": "t_test",
            "partition": 3,
            "replicas": [
                1,
                2,
				3
            ]
        },
		{
            "topic": "t_test",
            "partition": 4,
            "replicas": [
                2,
                3,
				4
            ]
        },
		{
            "topic": "t_test",
            "partition": 5,
            "replicas": [
                3,
                4,
				5
            ]
        }
    ],
    "version": 1
}

执行变更:

命令:./kafka-reassign-partitions.sh --zookeeper localhost:2181  --reassignment-json-file
[root@dbmspreapp212 bin]# ./kafka-reassign-partitions.sh --zookeeper localhost:2181  --reassignment-json-file  ../partitions-extension-push-token-topic.json --execute
Current partition replica assignment

{"version":1,"partitions":[{"topic":"t_test","partition":1,"replicas":[5,6],"log_dirs":["any","any"]},{"topic":"t_test","partition":5,"replicas":[3,4],"log_dirs":["any","any"]},{"topic":"t_test","partition":2,"replicas":[6,1],"log_dirs":["any","any"]},{"topic":"t_test","partition":3,"replicas":[1,2],"log_dirs":["any","any"]},{"topic":"t_test","partition":4,"replicas":[2,3],"log_dirs":["any","any"]},{"topic":"t_test","partition":0,"replicas":[4,5],"log_dirs":["any","any"]}]}

Save this to use as the --reassignment-json-file option during rollback
Successfully started reassignment of partitions.

验证结果:

./kafka-reassign-partitions.sh --zookeeper localhost:2181  --reassignment-json-file  ../partitions-extension-push-token-topic.json --verify
[root@dbmspreapp212 bin]# ./kafka-reassign-partitions.sh --zookeeper localhost:2181  --reassignment-json-file  ../partitions-extension-push-token-topic.json --verify
Status of partition reassignment: 
Reassignment of partition t_test-5 completed successfully
Reassignment of partition t_test-2 completed successfully
Reassignment of partition t_test-0 completed successfully
Reassignment of partition t_test-4 completed successfully
Reassignment of partition t_test-1 completed successfully
Reassignment of partition t_test-3 completed successfully

查看结果
变更前:

[root@dbmspreapp212 bin]# ./kafka-topics.sh --describe --zookeeper localhost:2181 --topic t_test
Topic:t_test	PartitionCount:6	ReplicationFactor:2	Configs:
	Topic: t_test	Partition: 0	Leader: 4	Replicas: 4,5	Isr: 4,5
	Topic: t_test	Partition: 1	Leader: 5	Replicas: 5,6	Isr: 5,6
	Topic: t_test	Partition: 2	Leader: 6	Replicas: 6,1	Isr: 6,1
	Topic: t_test	Partition: 3	Leader: 1	Replicas: 1,2	Isr: 1,2
	Topic: t_test	Partition: 4	Leader: 2	Replicas: 2,3	Isr: 2,3
	Topic: t_test	Partition: 5	Leader: 3	Replicas: 3,4	Isr: 3,4

变更后

[root@dbmspreapp212 bin]# ./kafka-topics.sh --describe --zookeeper localhost:2181 --topic t_test
Topic:t_test	PartitionCount:6	ReplicationFactor:3	Configs:
	Topic: t_test	Partition: 0	Leader: 4	Replicas: 4,5,6	Isr: 4,5,6
	Topic: t_test	Partition: 1	Leader: 5	Replicas: 5,6,1	Isr: 5,6,1
	Topic: t_test	Partition: 2	Leader: 6	Replicas: 6,1,2	Isr: 6,1,2
	Topic: t_test	Partition: 3	Leader: 1	Replicas: 1,2,3	Isr: 1,2,3
	Topic: t_test	Partition: 4	Leader: 2	Replicas: 2,3,4	Isr: 2,3,4
	Topic: t_test	Partition: 5	Leader: 3	Replicas: 3,4,5	Isr: 3,4,5

4.10 减少topic副本数

类似于增加副本数,只需要更改json文件的replicas配置项

4.11 Kafka集群扩容(重分配topic分区)

创建topic-to-move.json
vim /opt/kafka/topic-to-move.json

{
    "topics": [
        {
            "topic": "t_test"
        }
    ],
    "version": 1
}

生成迁移分配规则json文件:

./kafka-reassign-partitions.sh --zookeeper localhost:2181 --topics-to-move-json-file ../topic-to-move.json --broker-list "1,2,3,4,5,6,8" --generate
[root@dbmspreapp212 bin]# ./kafka-reassign-partitions.sh --zookeeper localhost:2181 --topics-to-move-json-file ../topic-to-move.json --broker-list "1,2,3,4,5,6,8" --generate
Current partition replica assignment
{"version":1,"partitions":[{"topic":"t_test","partition":1,"replicas":[6,1],"log_dirs":["any","any"]},{"topic":"t_test","partition":5,"replicas":[4,5],"log_dirs":["any","any"]},{"topic":"t_test","partition":6,"replicas":[5,6],"log_dirs":["any","any"]},{"topic":"t_test","partition":2,"replicas":[1,2],"log_dirs":["any","any"]},{"topic":"t_test","partition":3,"replicas":[2,3],"log_dirs":["any","any"]},{"topic":"t_test","partition":4,"replicas":[3,4],"log_dirs":["any","any"]},{"topic":"t_test","partition":0,"replicas":[5,6],"log_dirs":["any","any"]}]}

Proposed partition reassignment configuration
{"version":1,"partitions":[{"topic":"t_test","partition":1,"replicas":[1,8],"log_dirs":["any","any"]},{"topic":"t_test","partition":5,"replicas":[5,4],"log_dirs":["any","any"]},{"topic":"t_test","partition":6,"replicas":[6,5],"log_dirs":["any","any"]},{"topic":"t_test","partition":2,"replicas":[2,1],"log_dirs":["any","any"]},{"topic":"t_test","partition":3,"replicas":[3,2],"log_dirs":["any","any"]},{"topic":"t_test","partition":4,"replicas":[4,3],"log_dirs":["any","any"]},{"topic":"t_test","partition":0,"replicas":[8,6],"log_dirs":["any","any"]}]}

其中的Current partition replica assignment指的是迁移前的partition replica;Proposed partition reassignment configuration 指的就是迁移分配规则json。需要将该json文件保存到json文件中(如expand-cluster-reassignment.json)
vim /opt/kafka/expand-cluster-reassignment.json

{"version":1,"partitions":[{"topic":"t_test","partition":1,"replicas":[1,8],"log_dirs":["any","any"]},{"topic":"t_test","partition":5,"replicas":[5,4],"log_dirs":["any","any"]},{"topic":"t_test","partition":6,"replicas":[6,5],"log_dirs":["any","any"]},{"topic":"t_test","partition":2,"replicas":[2,1],"log_dirs":["any","any"]},{"topic":"t_test","partition":3,"replicas":[3,2],"log_dirs":["any","any"]},{"topic":"t_test","partition":4,"replicas":[4,3],"log_dirs":["any","any"]},{"topic":"t_test","partition":0,"replicas":[8,6],"log_dirs":["any","any"]}]}

执行迁移分配

./kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ../expand-cluster-reassignment.json --execute
[root@dbmspreapp212 bin]# kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ../expand-cluster-reassignment.json --execute
Current partition replica assignment

{"version":1,"partitions":[{"topic":"t_test","partition":1,"replicas":[6,1],"log_dirs":["any","any"]},{"topic":"t_test","partition":5,"replicas":[4,5],"log_dirs":["any","any"]},{"topic":"t_test","partition":6,"replicas":[5,6],"log_dirs":["any","any"]},{"topic":"t_test","partition":2,"replicas":[1,2],"log_dirs":["any","any"]},{"topic":"t_test","partition":3,"replicas":[2,3],"log_dirs":["any","any"]},{"topic":"t_test","partition":4,"replicas":[3,4],"log_dirs":["any","any"]},{"topic":"t_test","partition":0,"replicas":[5,6],"log_dirs":["any","any"]}]}

Save this to use as the --reassignment-json-file option during rollback
Successfully started reassignment of partitions.

验证

./kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ../expand-cluster-reassignment.json --verify
[root@dbmspreapp212 bin]# ./kafka-reassign-partitions.sh --zookeeper localhost:2181 --reassignment-json-file ../expand-cluster-reassignment.json --verify
Status of partition reassignment: 
Reassignment of partition t_test-5 completed successfully
Reassignment of partition t_test-2 completed successfully
Reassignment of partition t_test-0 completed successfully
Reassignment of partition t_test-4 completed successfully
Reassignment of partition t_test-6 completed successfully
Reassignment of partition t_test-1 completed successfully
Reassignment of partition t_test-3 completed successfully

结果:

[root@dbmspreapp212 bin]# ./kafka-topics.sh --zookeeper localhost:2181 --describe --topic t_test
Topic:t_test	PartitionCount:7	ReplicationFactor:2	Configs:
	Topic: t_test	Partition: 0	Leader: 8	Replicas: 8,6	Isr: 6,8
	Topic: t_test	Partition: 1	Leader: 1	Replicas: 1,8	Isr: 1,8
	Topic: t_test	Partition: 2	Leader: 1	Replicas: 2,1	Isr: 1,2
	Topic: t_test	Partition: 3	Leader: 2	Replicas: 3,2	Isr: 2,3
	Topic: t_test	Partition: 4	Leader: 3	Replicas: 4,3	Isr: 3,4
	Topic: t_test	Partition: 5	Leader: 4	Replicas: 5,4	Isr: 4,5
	Topic: t_test	Partition: 6	Leader: 5	Replicas: 6,5	Isr: 5,6

4.12 Kafka集群节点下线

和扩容相反,--broker-list "1,2,3,4,5,8",比如下线节点6,然后生成文件等进行操作。
重新leader、重新均衡,下线节点完成后重新均衡下
./kafka-preferred-replica-election.sh --zookeeper localhost:2181

<think>我们面临的任务是:使用Java每小时写入百万条数据到Kafka,并且根据数据中的时间和ID将数据写入不同的文件。 注意:Kafka本身不直接支持将数据写入不同的文件,但我们可以通过Kafka的消费者来消费数据,然后按照要求将数据写入不同的文件。 步骤分解: 1. 生产者(Producer)端:使用Java编写一个高效的Kafka生产者,每小时产生百万条数据(约100万条/小时,即大约每秒278条,这个量级对Kafka来说很轻松)。 2. 消费者(Consumer)端:编写一个消费者,从Kafka消费数据,然后根据消息中的时间(按小时)和ID将消息写入到不同的文件中。 但是,根据问题描述,我们可能需要将数据按照时间和ID分区写入文件。这里有两种理解: a) 在生产者端,我们只是将数据发送到Kafka,然后由消费者负责分区写入文件。 b) 在生产者端,我们可能希望根据时间和ID将数据发送到Kafka的不同分区(这样消费者可以并行处理),然后消费者按分区消费并写入文件。 然而,问题要求“按消息时间小时和ID分区写入不同文件”,这意味着最终存储是按小时和ID分文件的。因此,我们可以在消费者端完成这个分组和写入。 具体方案: 生产者端: - 创建Kafka生产者,配置必要的属性(如bootstrap.servers, key.serializer, value.serializer等)。 - 每条消息包含时间(timestamp)和ID,以及消息内容。 - 我们可以选择使用时间的小时部分和ID作为消息的key,这样相同小时和ID的消息会被发送到同一个分区(保证分区内有序)。但注意,Kafka的分区数量是固定的,而我们的ID可能很多,所以我们需要设计一个合理的key,使得数据能够均匀分布到各个分区,同时相同小时和ID的数据进入同一个分区。 但是,如果我们希望相同小时和ID的数据进入同一个分区,那么我们可以将key设置为:`小时_ID`。这样,相同的组合会进入同一个分区(因为Kafka的分区分配是基于key的hash值)。但是,这样可能会导致分区数据倾斜(如果某个ID数据量特别大)。因此,我们需要权衡。 另一种做法:生产者不关心文件的分区,只负责发送消息到Kafka,然后由消费者根据消息的时间小时和ID将数据写入不同的文件。 消费者端: - 创建Kafka消费者,订阅相应的主题。 - 消费者可以多线程或多实例运行,但要注意:同一个小时和ID的数据应该被同一个消费者实例处理,这样才能写入同一个文件。否则,不同的消费者实例可能会同时写入同一个文件,造成冲突。 - 因此,我们可以考虑使用Kafka的分区分配机制:每个分区由一个消费者实例处理。在消费者内部,我们根据消息的小时和ID将消息分组,然后按组写入不同的文件。 但是,由于同一个小时和ID的数据可能分布在不同的分区上(因为生产者可能使用不同的key,或者不使用key导致轮询分区),这样同一个小时和ID的数据可能被不同的消费者处理,从而无法写入同一个文件。 所以,我们需要保证同一个小时和ID的数据进入同一个分区,这样同一个消费者实例就可以处理这些数据,然后写入同一个文件。 因此,生产者的设计很关键: 生产者发送消息时,使用复合key(小时和ID的组合)来确保相同小时和ID的数据进入同一个分区。这样在消费者端,同一个分区的数据由同一个消费者线程处理,这个线程可以按小时和ID创建不同的文件输出流,将消息写入对应的文件。 但是,消费者端需要处理多个小时和ID的组合,因此需要管理多个文件。同时,要注意文件按小时和ID分开,例如:`2022050112_ID001.txt`,其中2022050112表示2022年5月112时,ID001是具体的ID。 另外,消费者需要定期关闭已经完成的小时的文件(例如,当前时间已经进入下一个小时,那么上一个小时的文件就可以关闭了)。但是,由于消息可能乱序(但Kafka保证分区内有序,所以同一个分区内相同小时和ID的消息是顺序到达的),但不同分区的消息可能时间交错。因此,如果我们希望按小时关闭文件,需要谨慎处理。 考虑到每小时百万条的数据量,消费者处理能力也需要考虑。我们可以使用多线程消费者组,每个消费者处理若干个分区。每个消费者线程内部,按小时和ID维护一个文件输出流的映射(Map),并定期清理过时的流(比如一小时前的小时)。 具体实现步骤: 生产者(Java): - 创建KafkaProducer实例。 - 构建消息:消息的key为字符串,格式:`小时_ID`(例如:`2022050112_12345`),其中小时可以从消息中的时间字段提取(注意时区),ID就是数据中的ID。 - 消息的value可以是原始数据(可以是JSON格式,或者其它格式)。 - 发送消息:使用`producer.send(new ProducerRecord<>(topic, key, value))`。 消费者(Java): - 创建KafkaConsumer实例,并设置group.id,订阅主题。 - 使用多线程(或多个消费者实例)来消费,但注意分区分配。 - 每个消费者线程(或实例)在poll到消息后: for (ConsumerRecord<String, String> record : records) { // 解析key,得到小时和ID String key = record.key(); String[] parts = key.split("_"); String hour = parts[0]; String id = parts[1]; String value = record.value(); // 根据hour和id确定文件 String fileName = hour + "_" + id + ".txt"; // 获取该文件对应的输出流(可以缓存输出流,避免频繁打开关闭) // 将value写入文件,并加上换行符 } - 注意:需要管理文件输出流,避免打开太多文件(可以设置一个最大打开文件数,并关闭长时间不用的流)。或者,我们可以在每次写入时打开文件(追加模式),写入后关闭。但是这样效率低,但考虑到每秒写入量不大(278条/秒,每个消费者线程处理的分区数据量会更少),所以可以接受。但为了效率,建议缓存输出流。 关于文件流缓存: 我们可以使用一个Map<String, BufferedWriter>,key是文件名(hour_id),value是对应的BufferedWriter。 同时,我们需要一个机制来关闭那些已经不再使用的文件流(例如,当前小时已经过去,那么上一个小时的文件流可以关闭)。我们可以记录每个文件流的最后使用时间,然后有一个后台线程定期清理超过一定时间(比如1小时)没有使用的流。或者,在写入时判断,如果当前写入的小时不是当前小时(比如当前系统时间是13点,而消息的小时是12点),那么我们可以关闭12点的所有文件流?但是,有可能同一个小时的消息会间隔一段时间才到来(比如延迟),所以不能立即关闭,可以设置一个延迟关闭时间(比如1小时)。 另外,注意消费者需要定期提交偏移量(offset)。 但是,上述方案在消费者组扩容缩容时会触发分区重平衡,这时消费者需要关闭自己负责的所有文件流,并提交偏移量。在重新分配分区后,新的消费者会从上次提交的偏移量开始消费,但可能同一个分区被分配给另一个消费者,那么之前打开的文件流就不在这个消费者上了。因此,我们不需要在重平衡时关闭所有流,因为重平衡后,当前消费者可能不再负责某些分区,那么这些分区对应的文件流就应该关闭(因为不再由这个消费者处理)。但是,我们无法预先知道哪些分区会被重新分配,所以需要在重平衡发生时关闭所有流,并提交偏移量。Kafka的消费者API提供了重平衡监听器(ConsumerRebalanceListener)。 实现重平衡监听器: - 在重平衡开始之前(即当前消费者要失去某些分区时),我们需要提交偏移量,并关闭所有文件流(因为不知道会失去哪些分区,所以全部关闭?或者只关闭失去的分区对应的流?但我们的流是按文件管理的,而文件是按key(小时和ID)创建的,与分区的对应关系不直接)。实际上,一个文件可能包含多个分区的数据(因为同一个小时和ID的数据只在一个分区),但一个消费者可能处理多个分区,而一个分区可能包含多个小时和ID的数据。所以,在重平衡时,我们无法知道哪些文件流会受到影响。 因此,更简单的做法:不缓存文件流,每次写入都打开文件(追加模式),写入后关闭。这样就不需要管理缓存,也不会因为重平衡而需要关闭流。但这样效率较低,因为每次写入都要打开关闭文件。 考虑到每小时百万条,即每秒278条,如果每个文件每秒只写入几条(因为ID很多,所以每个文件写入的频率不高),那么每次打开关闭文件是可以接受的。但如果有多个ID,每个ID每秒写入多次,那么频繁打开关闭文件可能会成为瓶颈。 因此,我们可以折中:为每个文件使用缓冲区,定期刷新并关闭文件(比如每100条刷新一次,或者每秒刷新一次)。但是,这样在重平衡时可能会丢失缓冲区内的数据。 所以,为了保证数据不丢失,我们可以在每次写入后都刷新并关闭文件?这样安全,但性能差。 另一种方案:使用带缓冲的写入,但在重平衡时强制刷新所有缓冲区并关闭所有文件流。我们可以通过实现ConsumerRebalanceListener来在分区被撤销时进行刷新和关闭。 具体步骤: 1. 在消费者中维护一个Map<String, BufferedWriter>,用于缓存文件流。 2. 在重平衡监听器的onPartitionsRevoked方法中,我们提交偏移量(这之前,我们可能需要刷新所有缓存流并关闭文件,但这样会丢失当前正在处理的消息吗?注意,我们是在拉取下一批消息之前处理重平衡的,所以当前缓存的数据都是已经处理的消息,应该立即写入文件并提交偏移量?但实际上,我们可能已经处理了消息但还没有写入文件(在缓冲区中),所以需要刷新并关闭所有文件流,确保数据写入磁盘。 3. 在重平衡监听器的onPartitionsAssigned方法中,我们可以不做特别处理,因为文件流会在需要时创建。 但是,重平衡时,我们不应该关闭所有文件流,因为有些分区可能仍然由当前消费者处理。我们只需要关闭那些不再需要的文件流?实际上,我们无法知道哪些文件流不再需要(因为同一个文件可能由多个分区的数据组成,而分区被重新分配后,我们不知道哪些文件还会被用到)。所以,我们选择在每次写入后刷新并关闭文件流(即不缓存),这样虽然效率低,但简单可靠。 考虑到数据量(每秒278条),如果每个文件每秒写入次数不多,那么每次打开关闭文件的代价可以接受。 因此,消费者端可以这样实现: for (ConsumerRecord<String, String> record : records) { String key = record.key(); String[] parts = key.split("_"); String hour = parts[0]; String id = parts[1]; String value = record.value(); String fileName = hour + "_" + id + ".txt"; // 以追加模式打开文件,写入一行,然后关闭 try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) { writer.write(value); writer.newLine(); } catch (IOException e) { e.printStackTrace(); } } 这样,每次写入后都会关闭文件,确保数据被刷新到磁盘。同时,重平衡时不需要特殊处理,因为每次写入都是独立的。 但是,这样频繁打开关闭文件,如果每个文件每秒有多次写入,那么性能可能不佳。我们可以考虑为每个文件设置一个写入缓冲区,定期刷新。但是,这样会增加复杂性,而且需要处理重平衡时的刷新。 权衡:如果每个文件每秒写入次数很少(比如,每个ID每秒只有几条),那么这样处理没问题。如果同一个文件(同一个小时和ID)每秒写入次数很多(比如几十次),那么我们可以使用一个缓存机制,将同一个文件的多条写入合并,然后批量写入。但是,这需要维护一个按文件分组的缓冲区,并定时刷新(比如每100条或者每秒)。 我们设计一个可选的优化:在消费者内部,维护一个按文件名分组的缓冲区(Map<String, List<String>>),并启动一个定时线程,每秒刷新所有缓冲区到文件。同时,在每次poll循环中,如果某个缓冲区的记录数达到100,也立即刷新。这样,我们减少了文件打开关闭的次数。但是,在重平衡时,我们需要刷新所有缓冲区,以确保数据不丢失。 实现这种优化: - 在消费者中,维护一个Map<String, BufferedWriter>用于文件流,以及一个Map<String, List<String>>用于缓冲区(或者将缓冲区直接放在流对象中,但为了简单,我们不用流对象,而是用带缓冲的写入器,并且我们不用自己再缓冲,BufferedWriter本身就有缓冲)。但是,我们想要的是按文件分组,然后批量写入(多条记录一次写入),所以我们可以使用一个Map<String, BufferedWriter>来缓存每个文件的写入流,然后定期刷新。 - 但是,BufferedWriter本身有缓冲区,我们可以在写入一定数量后调用flush(),但这样并不能减少打开关闭文件的次数。所以,我们想要的是保持文件打开,然后定期刷新缓冲区并关闭文件(比如每5秒关闭一次),或者当缓冲区记录数达到100条时关闭并重新打开(这样不行,因为这样会丢失之前的数据)。 实际上,我们想要的是:在消费者处理期间,保持文件流打开,并定期刷新(但不关闭),直到我们确定这个文件暂时不会有新数据(比如这个小时已经过去)才关闭。但这样会保持很多文件打开,可能会超过系统限制。 因此,我们采用一个折中:为每个文件使用一个带缓冲的写入器(BufferedWriter),并且每写入100条记录就刷新一次(但不关闭),然后每隔5秒,不管缓冲区有多少条,都刷新一次。同时,我们还要定期检查哪些文件已经超过一定时间(比如1小时)没有写入,就关闭其文件流。 具体实现: 1. 在消费者类中,定义: Map<String, BufferedWriter> writers = new HashMap<>(); Map<String, Integer> writeCount = new HashMap<>(); // 每个文件的写入次数(用于计数刷新) long lastFlushTime = 0; // 上次全局刷新时间 2. 在poll循环中,处理每条消息: String fileName = ...; BufferedWriter writer = writers.get(fileName); if (writer == null) { writer = new BufferedWriter(new FileWriter(fileName, true)); // 追加模式 writers.put(fileName, writer); writeCount.put(fileName, 0); } writer.write(value); writer.newLine(); // 更新计数 int count = writeCount.get(fileName) + 1; writeCount.put(fileName, count); if (count >= 100) { writer.flush(); writeCount.put(fileName, 0); // 重置计数 } 3. 在每次poll循环后(或者定时),检查是否距离上次全局刷新超过5秒,如果是,则刷新所有writer,并重置计数(或者只刷新,不重置计数,因为计数是为了每100条刷新一次,而全局刷新是另一个机制): long now = System.currentTimeMillis(); if (now - lastFlushTime > 5000) { for (BufferedWriter writer : writers.values()) { writer.flush(); } lastFlushTime = now; // 同时,可以在这里关闭那些超过1小时没有写入的writer(需要记录每个文件的最后写入时间) } 4. 此外,我们还需要记录每个文件的最后写入时间,然后定期(比如每分钟)检查,如果某个文件最后写入时间超过1小时,则关闭其writer并从map中移除。 5. 重平衡监听器:在分区被撤销时(onPartitionsRevoked),我们需要刷新所有writer(确保数据写入磁盘),然后关闭所有writer,并提交偏移量(注意,KafkaConsumer会在重平衡前自动提交偏移量,但我们可以选择手动提交以确保精确控制)。实际上,我们可以配置自动提交,但为了确保不丢失数据,最好在重平衡前手动提交。 6. 在重平衡监听器的onPartitionsRevoked中,我们: - 刷新所有writer(flush) - 关闭所有writer - 清空writers和writeCount - 手动提交偏移量(如果使用手动提交) 7. 在重平衡监听器的onPartitionsAssigned中,我们不需要做特别操作。 注意:消费者需要处理正常关闭,同样需要刷新并关闭所有writer。 这个方案相对复杂,但可以提升性能。如果数据量不大,建议使用每次写入都打开关闭文件的简单方案。 考虑到每小时百万条,即每秒278条,如果ID数量很多(比如1000个ID),那么每个文件每秒平均0.278条,即每个文件每3-4秒才写入一条。这样,我们使用每次写入打开关闭文件的方案,每个文件每秒打开关闭0.278次,对于文件系统来说是可以接受的。因此,简单方案更可取。 因此,我们选择简单方案:每次写入都打开文件(追加模式),写入一行,关闭文件。 生产者代码示例: ```java import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import java.util.Properties; public class KafkaDataProducer { public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "kafka-broker1:9092,kafka-broker2:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 可以配置批处理和压缩等优化 props.put("batch.size", 16384); // 16KB props.put("linger.ms", 5); // 等待最多5ms,如果批处理未满也会发送 props.put("compression.type", "snappy"); // 压缩 KafkaProducer<String, String> producer = new KafkaProducer<>(props); // 模拟每小时百万条数据 long startTime = System.currentTimeMillis(); long endTime = startTime + 3600000; // 1小时 int count = 0; while (System.currentTimeMillis() < endTime) { // 模拟生成一条数据 String id = "ID" + (count % 1000); // 假设有1000个不同的ID long timestamp = System.currentTimeMillis(); // 当前时间戳 // 将时间戳转换为小时格式,这里使用SimpleDateFormat,注意时区 java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyyMMddHH"); sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC")); String hour = sdf.format(new java.util.Date(timestamp)); String key = hour + "_" + id; String value = "message " + count + " with timestamp " + timestamp; producer.send(new ProducerRecord<>("my-topic", key, value)); count++; // 控制发送速率,大约每秒278条 if (count % 278 == 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } producer.close(); } } ``` 消费者代码示例(简单方案,每次写入都打开关闭文件): ```java import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; import java.util.Collections; import java.util.Properties; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; public class KafkaDataConsumer { public static void main(String[] args) { Properties props = new Properties(); props.put("bootstrap.servers", "kafka-broker1:9092,kafka-broker2:9092"); props.put("group.id", "file-writer-group"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); // 设置自动提交偏移量,也可以手动提交 props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("my-topic")); while (true) { ConsumerRecords<String, String> records = consumer.poll(java.time.Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { String key = record.key(); // key的格式:hour_id String[] parts = key.split("_"); if (parts.length < 2) { // 格式错误,跳过 continue; } String hour = parts[0]; String id = parts[1]; String value = record.value(); String fileName = hour + "_" + id + ".txt"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) { writer.write(value); writer.newLine(); } catch (IOException e) { e.printStackTrace(); } } } } } ``` 注意:消费者需要处理异常,并且在实际应用中可能需要优雅关闭。 另外,文件写入的目录需要存在,否则会抛出异常。可以预先创建目录,或者在写入前检查目录是否存在并创建。 以上方案可以满足需求。但是,如果同一个文件(同一个小时和ID)的写入非常密集(比如每秒几百次),那么频繁打开关闭文件会成为瓶颈。此时,我们可以考虑使用优化方案(缓存文件流并定时刷新)。但根据我们的数据分布(1000个ID,每秒总共278条,所以每个文件每秒0.278次),所以不需要优化。 最后,关于Kafka的配置,生产者和消费者都可以根据实际环境调整。 相关问题: 1. 如何提高Kafka生产者的吞吐量? 2. 如何确保Kafka消费者在故障时不会丢失数据? 3. Kafka消费者如何实现多线程消费? 4. 如何监控Kafka生产者和消费者的性能? 5.Kafka中,如何管理消费者的偏移量? 请注意,以上代码示例是简化的,实际应用中需要处理异常、关闭资源、配置优化等。</think>### Java实现每小时写入百万条数据到Kafka并按条件分区存储 #### 解决方案设计 1. **生产者端**:使用高效Kafka生产者批量发送数据 2. **消费者端**:按时间小时和ID分区写入不同文件 3. **分区策略**:使用复合键(时间小时+ID)确保相同分区的数据进入同一个文件 #### 生产者实现(Java) ```java import org.apache.kafka.clients.producer.*; public class HighVolumeProducer { public static void main(String[] args) { // 1. Kafka配置 Properties props = new Properties(); props.put("bootstrap.servers", "kafka1:9092,kafka2:9092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("batch.size", 16384); // 16KB批处理 props.put("linger.ms", 5); // 最大延迟5ms props.put("compression.type", "snappy"); // 压缩提升吞吐量[^1] // 2. 创建生产者 Producer<String, String> producer = new KafkaProducer<>(props); // 3. 模拟每小时百万数据 (约278条/秒) long startTime = System.currentTimeMillis(); for (int i = 0; i < 1_000_000; i++) { // 生成带时间和ID的数据 String eventTime = getHourTimestamp(); // 格式: yyyyMMddHH String id = "ID_" + (i % 1000); // 假设1000个不同ID // 构建复合键: 时间小时+ID String compositeKey = eventTime + "_" + id; String value = "Data-" + i + "|" + System.currentTimeMillis(); // 异步发送 producer.send(new ProducerRecord<>("hourly_data_topic", compositeKey, value), (metadata, e) -> { if (e != null) e.printStackTrace(); }); // 控制速率 (约278条/秒) if (i % 278 == 0) Thread.sleep(1000); } producer.close(); System.out.println("Produced 1M messages in " + (System.currentTimeMillis()-startTime)/1000 + "s"); } private static String getHourTimestamp() { return new SimpleDateFormat("yyyyMMddHH").format(new Date()); } } ``` #### 消费者实现(文件分区写入) ```java import org.apache.kafka.clients.consumer.*; public class PartitionedFileWriter { public static void main(String[] args) { // 1. Kafka配置 Properties props = new Properties(); props.put("bootstrap.servers", "kafka1:9092,kafka2:9092"); props.put("group.id", "file-writer-group"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("auto.offset.reset", "earliest"); // 2. 创建消费者 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(Collections.singletonList("hourly_data_topic")); // 3. 文件写入管理 Map<String, BufferedWriter> openFiles = new HashMap<>(); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { // 解析复合键: eventHour_id String[] keys = record.key().split("_"); String eventHour = keys[0]; String id = keys[1]; // 创建分区目录 Path dirPath = Paths.get("./data/" + eventHour); if (!Files.exists(dirPath)) Files.createDirectories(dirPath); // 获取文件写入器 String filePath = dirPath + "/" + id + ".txt"; BufferedWriter writer = openFiles.computeIfAbsent(filePath, k -> new BufferedWriter(new FileWriter(k, true))); // 追加模式 // 写入数据 writer.write(record.value()); writer.newLine(); } // 每小时关闭过期文件 cleanUpFiles(openFiles); } } private static void cleanUpFiles(Map<String, BufferedWriter> openFiles) throws IOException { String currentHour = new SimpleDateFormat("yyyyMMddHH").format(new Date()); Iterator<Map.Entry<String, BufferedWriter>> it = openFiles.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, BufferedWriter> entry = it.next(); String fileHour = entry.getKey().split("/")[2]; // 从路径提取小时 // 关闭非当前小时的文件 if (!fileHour.equals(currentHour)) { entry.getValue().close(); it.remove(); } } } } ``` #### 关键优化点 1. **生产者优化**: - 批处理:`batch.size=16384` + `linger.ms=5` 减少网络请求 - 压缩:`snappy`压缩提升吞吐量30-50%[^1] - 异步发送:使用回调处理异常 2. **分区策略**: - 复合键`时间小时_ID`确保相同分区的数据进入同一文件 - Kafka自动分区分配保证负载均衡[^2] 3. **文件管理**: - 目录结构:`./data/yyyyMMddHH/ID.txt` - 文件缓存:避免频繁开关文件 - 定时清理:每小时关闭过期文件句柄 4. **容错处理**: - 消费者偏移量自动提交 - 文件追加模式避免数据丢失 - 异常处理回调(生产者) #### 性能测试建议 | 指标 | 单节点目标 | 优化手段 | |------|------------|----------| | 吞吐量 | 100K+ msg/s | 批处理+压缩 | | 延迟 | <100ms | 减少linger.ms | | 持久化 | 零数据丢失 | acks=all配置[^3] | > **部署建议**:3节点Kafka集群可轻松处理每小时百万级写入,分区数建议设置为消费者实例的2-3倍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值