Flink常见问题集解决方案
1. 集群监控
1.1 kafka监控
1)查看消费状态
./kafka-consumer-groups.sh --bootstrap-server 192.168.0.104:9092 --group test-consumer-group001 --describe
1.2 Flink程序监控
参考:https://www.cnblogs.com/robots2/p/16673655.html
1.2.1 数据反压问题⭐
第一步:跟踪backpressure,找到反压的算子
解决方案一:优化数据处理逻辑,解决数据倾斜
解决方案二:增加消费者数量,修改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是否均匀,
## 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();
}
}
}