Flink常见问题集解决方案

1. 集群监控

1.1 kafka监控

1)查看消费状态

./kafka-consumer-groups.sh --bootstrap-server 192.168.0.104:9092 --group test-consumer-group001  --describe

image-20240417093812187

1.2 Flink程序监控

参考:https://www.cnblogs.com/robots2/p/16673655.html

1.2.1 数据反压问题⭐

第一步:跟踪backpressure,找到反压的算子

image-20240417094432935

解决方案一:优化数据处理逻辑,解决数据倾斜

解决方案二:增加消费者数量,修改int numConsumers = 3的数量

import org.apache.kafka.clients.consumer.ConsumerConfig;  
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.common.serialization.StringDeserializer;  
  
import java.time.Duration;  
import java.util.Arrays;  
import java.util.Collections;  
import java.util.Properties;  
  
public class KafkaConsumerExample {  
  
    public static void main(String[] args) {  
        // 定义消费者配置  
        Properties props = new Properties();  
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");  
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-consumer-group");  
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
        // 其他配置...  
  
        // 订阅的主题  
        String topic = "my-topic";  
  
        // 要启动的消费者数量  
        int numConsumers = 3;  
  
        // 启动多个消费者实例  
        for (int i = 0; i < numConsumers; i++) {  
            new Thread(() -> {  
                KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
                consumer.subscribe(Collections.singletonList(topic));  
  
                while (true) {  
                    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));  
                    for (ConsumerRecord<String, String> record : records) {  
                        System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());  
                    }  
                }  
            }).start();  
        }  
    }  
}

解决方案三:优化消费者并发数(创建线程池,可不可以直接设置并发数)

import org.apache.kafka.clients.consumer.ConsumerConfig;  
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.common.serialization.StringDeserializer;  
  
import java.time.Duration;  
import java.util.Collections;  
import java.util.Properties;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class KafkaConcurrentConsumerExample {  
  
    public static void main(String[] args) {  
        // 定义消费者配置  
        Properties props = new Properties();  
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");  
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-consumer-group");  
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
        // 其他配置...  
  
        // 订阅的主题  
        String topic = "my-topic";  
  
        // 创建固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(10); // 假设我们希望有10个并发消费者  
  
        // 提交多个消费者任务到线程池  
        for (int i = 0; i < 10; i++) {  
            final int threadNumber = i;  
            executorService.submit(() -> {  
                KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
                consumer.subscribe(Collections.singletonList(topic));  
  
                while (true) {  
                    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));  
                    for (ConsumerRecord<String, String> record : records) {  
                        // 处理记录...  
                        System.out.printf("Thread %d: offset = %d, key = %s, value = %s%n", threadNumber, record.offset(), record.key(), record.value());  
                    }  
                }  
            });  
        }  
  
        // 优雅地关闭线程池和消费者(在实际应用中需要实现)  
        // ...  
    }  
}

解决方案四:提高批量拉取数据的数量(设置配置文件properties:)

// props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);  
import org.apache.kafka.clients.consumer.ConsumerConfig;  
import org.apache.kafka.clients.consumer.KafkaConsumer;  
import org.apache.kafka.common.serialization.StringDeserializer;  
  
import java.time.Duration;  
import java.util.Collections;  
import java.util.Properties;  
  
public class KafkaConsumerExample {  
    public static void main(String[] args) {  
        Properties props = new Properties();  
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");  
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-consumer-group");  
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());  
          
        // 设置 max.poll.records 参数,这里假设我们每次想拉取最多 500 条记录  
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);  
          
        // 其他配置...  
        // props.put(...);  
  
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
        consumer.subscribe(Collections.singletonList("my-topic"));  
  
        while (true) {  
            // 调用 poll 方法,根据 max.poll.records 设置拉取记录  
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));  
  
            for (ConsumerRecord<String, String> record : records) {  
                // 处理记录  
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());  
            }  
        }  
  
        // 关闭消费者连接  
        consumer.close();  
    }  
}
  • 解决方案五:创建单独的线程或线程池进行消息拉取,逻辑处理放在后面
  • 解决方案六:增加分区数,但是需要对应的slot的匹配,或者可能增添数据背压问题

1.2.2 数据倾斜问题⭐

第一步:监控subtask,查看bytes recevied是否均匀,

image-20240417100809703

## AI问答:flink实时计算,出现数据倾斜时,应该怎么处理?

在 Flink 实时计算中,数据倾斜是一个常见的问题,它可能导致某些 Task 实例处理的数据量远大于其他 Task 实例,从而引发性能瓶颈和计算延迟。以下是一些处理 Flink 中数据倾斜的常用方法:

重新设计 Key:
如果数据倾斜是由于特定的 Key 导致的,尝试重新设计 Key,使其分布更均匀。
使用更复杂的哈希函数或自定义分区器,以便更均匀地分配数据。
Salting:
在 Key 的基础上添加随机前缀(salt),使得原本倾斜的 Key 分布到多个不同的分区中。
在处理完数据后,再去除这个随机前缀。
预聚合:
在数据进入 Flink 之前,先进行预聚合处理,减少 Key 的数量。
这可以减少 Flink 中 Task 的数量,并可能缓解数据倾斜问题。
广播状态:
如果某个 Key 的数据量特别大,可以考虑使用广播状态(Broadcast State)来处理。
将这个 Key 的数据广播到所有 Task 中,然后并行处理。
动态调整并行度:
Flink 支持在运行时动态调整 Task 的并行度。
监控系统的性能指标,当发现数据倾斜时,动态增加倾斜 Task 的并行度。
使用窗口和批处理:
对于一些时间窗口内的聚合操作,如果数据倾斜严重,可以考虑将流处理转为批处理,或者使用更大的时间窗口来减少倾斜的影响。
数据重分区:
使用 Flink 的 rebalance() 或 keyBy(new RandomKeySelector()) 进行重分区,以随机方式重新分配数据。
但这种方法可能会增加网络通信的开销。
监控与日志分析:
启用 Flink 的监控和日志功能,分析数据倾斜的原因和发生的位置。
根据分析结果,针对性地进行优化。
优化资源分配:
根据 Task 的负载情况,动态调整 TaskManager 的资源分配。
对于负载较重的 Task,可以分配更多的 CPU 和内存资源。
考虑使用其他计算框架或技术:
如果 Flink 始终无法解决数据倾斜问题,可以考虑使用其他计算框架(如 Spark Streaming)或技术(如分布式缓存)来处理数据。
处理数据倾斜时,需要根据具体的业务场景和数据特点来选择合适的方法。同时,也需要不断地进行监控和调优,以确保系统的性能和稳定性。
  • 解决方案一:重新设计 Key,后面加入随机数

  • import org.apache.flink.api.common.functions.KeySelector;  
    import org.apache.flink.streaming.api.datastream.DataStream;  
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;  
    import org.apache.flink.util.Collector;  
      
    import java.util.Random;  
      
    public class CustomKeySelector implements KeySelector<Event, String> {  
        private static final Random RANDOM = new Random();  
      
        @Override  
        public String getKey(Event value) throws Exception {  
            // 获取原始 Key  
            String originalKey = value.getUserId();  
      
            // 重新设计 Key,这里简单地在原始 Key 后面添加了一个随机数  
            // 注意:这种方法可能不适合所有场景,因为它可能增加 Key 的总数量  
            // 在实际应用中,可能需要更复杂的策略来确保 Key 的均匀分布  
            String newKey = originalKey + "_" + RANDOM.nextInt(1000);  
            return newKey;  
        }  
    }  
      
    public class FlinkDataSkewExample {  
        public static void main(String[] args) throws Exception {  
            final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();  
      
            // 假设我们有一个 DataStream<Event> events  
            DataStream<Event> events = // ... 从某处获取事件流  
      
            // 使用自定义 KeySelector 重新设计 Key,并进行 keyBy 操作  
            DataStream<Event> keyedStream = events.keyBy(new CustomKeySelector());  
      
            // 接下来可以对 keyedStream 进行聚合、窗口等操作  
            // ...  
      
            env.execute("Flink Data Skew Example");  
        }  
    }
    
  • 解决方案二:重新设计 Key,加入随机前缀

  • import org.apache.flink.api.common.functions.KeySelector;  
    import java.util.UUID;  
      
    public class SaltingKeySelector implements KeySelector<MyData, String> {  
        @Override  
        public String getKey(MyData value) throws Exception {  
            // 生成一个随机的 salt  
            String salt = UUID.randomUUID().toString().substring(0, 8);  
            // 将 salt 添加到原始 Key 上,形成新的 Key  
            return salt + "_" + value.getOriginalKey();  
        }  
    }
    
  • 解决方案三:提前聚合

  • 解决方案四:增添key(原有DataStream中添加专门用于分窗口的字段)

  • def dealing_input(str):(String,String){
        val keyby_key = scala.util.Random.nextInt(20).toString+"-"+key
        return (data,keyby_key)
    }
    input.keyby(_._2).window().xxx
    

2. 容错恢复

2.1 checkpoint的开启与恢复⭐

2.1.1 开启checkpoint

public class CheckpointExample {  
  
    public static void main(String[] args) throws Exception {  
        // 创建执行环境  
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();  
  
        // 启用 Checkpointing,设置间隔为 1000 毫秒  
        env.enableCheckpointing(1000);  
  
        // 设置 Checkpoint 模式为恰好一次 (Exactly-Once)  
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);  
  
        // 设置 Checkpoint 超时时间为 60000 毫秒  
        env.getCheckpointConfig().setCheckpointTimeout(60000);  
  
        // 设置允许的最大并发 Checkpoint 数量为 1  
        env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);  
  
        // 配置状态后端(这里以内存状态后端为例,实际生产环境应使用持久化存储)  
        // env.setStateBackend(new RocksDBStateBackend("hdfs://namenode:9000/checkpoint", true));
        env.setStateBackend(new MemoryStateBackend(1024));  
        // 设置开启,5s一备份,checkpoint模式EXACTLY_ONCE

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;  
import org.apache.flink.streaming.api.functions.source.SourceFunction;  
  
        // 创建数据源,这里仅作为示例,你需要替换成实际的数据源  
        SourceFunction<String> source = ...; // 你的数据源  
  
        // 构建你的 Flink 作业逻辑  
        env.fromSource(source, WatermarkStrategy.noWatermarks(), "Source Name")  
            // ... 你的转换操作 ...  
            .print(); // 输出到控制台,这里仅作为示例  
  
        // 执行作业  
        env.execute("Checkpoint Example");  
    }  
}
## 也可通过修改flink-conf.yaml进行全局配置,但推荐编程 API 进行每个单独设置,灵活一些

2.1.2 checkpoint的恢复

在 Apache Flink 中,Checkpoint 的恢复通常是自动的,不需要显式编写代码来恢复 Checkpoint。
如果你想要手动触发 Checkpoint(虽然通常不推荐这样做,因为 Flink 会自动管理 Checkpoint),你可以使用 CheckpointCoordinator 的 triggerCheckpoint(long maxCheckpointAttemptTime) 方法。但请注意,手动触发 Checkpoint 可能会干扰 Flink 的自动 Checkpoint 管理,并可能导致不可预期的行为。

2.2 state的备份与恢复

state相当于是checkpoint中的一部分
所以state的备份与恢复,就是checkpoint的备份与恢复

2.3 kafka的备份与恢复⭐

2.3.1 kafka的备份

kafka的备份方案一:一是offset的持久存储,二是offset的手动提交

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.common.TopicPartition;  
import org.apache.kafka.common.serialization.StringDeserializer;  
  
import java.time.Duration;  
import java.util.Collections;  
import java.util.HashMap;  
import java.util.Map;  
import java.util.Properties;  
import java.util.Set;  
  
public class KafkaConsumerWithOffsetManagement {  
    public static void main(String[] args) {  
        Properties props = new Properties();  
        props.put("bootstrap.servers", "localhost:9092");  
        props.put("group.id", "my-consumer-group");  
        props.put("key.deserializer", StringDeserializer.class.getName());  
        props.put("value.deserializer", StringDeserializer.class.getName());  
        props.put("enable.auto.commit", "false"); // 禁用自动提交offset  
  
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
        consumer.subscribe(Collections.singletonList("my-topic"));  
  
        // 从持久化存储中恢复之前的offset(这里仅作为示例,实际实现取决于你的持久化策略)  
        Map<TopicPartition, Long> recoveredOffsets = recoverOffsetsFromPersistentStorage();  
  
        // 如果有恢复的offset,则设置消费者的初始offset  
        if (!recoveredOffsets.isEmpty()) {  
            consumer.assign(recoveredOffsets.keySet());  
            consumer.seek(recoveredOffsets);  
        }  
  
        try {  
            while (true) {  
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));  
                for (ConsumerRecord<String, String> record : records) {  
                    System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());  
  
                    // 处理消息逻辑...  
  
                    // 手动提交offset到持久化存储(这里仅作为示例)  
                    commitOffsetToPersistentStorage(record.topic(), record.partition(), record.offset() + 1);  
  
                    // 手动提交offset到Kafka  
                    consumer.commitSync(Collections.singletonMap(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1)));  
                }  
            }  
        } finally {  
            consumer.close();  
        }  
    }  
  
    // 模拟从持久化存储中恢复offset的方法  
    private static Map<TopicPartition, Long> recoverOffsetsFromPersistentStorage() {  
        // 这里应该是从数据库、Redis等持久化

2.3.2 kafka的恢复

方案一: 从持久化存储恢复

        // 从持久化存储中恢复之前的offset(这里仅作为示例,实际实现取决于你的持久化策略)  
        Map<TopicPartition, Long> recoveredOffsets = recoverOffsetsFromPersistentStorage();  
        
        // 假设这里你从外部存储获取了之前的offset  
        // Map<TopicPartition, Long> offsets = ...; // 读取或恢复之前的offset  
    // 模拟从持久化存储中恢复offset的方法  
    private static Map<TopicPartition, Long> recoverOffsetsFromPersistentStorage() {  
        // 这里应该是从数据库、Redis等持久化

方案二:启用自动提交,那么消费会自动从上一次的位置进行消费

// 这种方案,只需设置自动提交即可
// 此时的offset会存入到__consumer_offsets主题中,但是这个主题存储的是序列化后的数据,所以读取时很麻烦,且有可能影响它的运作
// 所以,只需要设置自动提交, 让他自己从上次的消费位置消费即可

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.common.serialization.StringDeserializer;  
  
import java.time.Duration;  
import java.util.Collections;  
import java.util.Properties;  
  
public class KafkaConsumerExample {  
    public static void main(String[] args) {  
        Properties props = new Properties();  
        props.put("bootstrap.servers", "localhost:9092");  
        props.put("group.id", "my-consumer-group");  
        props.put("key.deserializer", StringDeserializer.class.getName());  
        props.put("value.deserializer", StringDeserializer.class.getName());  
        props.put("enable.auto.commit", "true"); // 启用自动提交offset  
        props.put("auto.commit.interval.ms", "1000"); // 设置自动提交offset的间隔  
  
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);  
        consumer.subscribe(Collections.singletonList("my-topic"));  
  
        try {  
            while (true) {  
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(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();  
        }  
    }  
}
posted @ 2024-04-23 11:10  付十一。  阅读(75)  评论(0)    收藏  举报