flink基础
官网:https://nightlies.apache.org/flink/flink-docs-release-1.17/zh/docs/
问题假设
1)在我们app操作的用户行为日志怎么能够实时处理,以便于进行实时分析,做出反馈?
2)在618或者双11数据激增时候怎么保证数据的高吞吐,低延迟的实时消费?
3)数据消费的时候怎么保证上下游的数据一致性,可靠性,容错性?
4)怎么做好上下游链路数据的监控,以便于出现问题的时候即使反馈或者程序容错?
简介
在数据量激增的时代,各种业务场景都有大量的业务数据产生,对于这些不断产生的数据应该如何进行有效的处理,成为当下大多数公司所面临的问题。目前比较流行的大数据处理引擎 Apache Spark,基本上已经取代了MapReduce 成为当前大数据处理的标准。但对实时数据处理来说,Apache Spark 的 Spark-Streaming 还有性能改进的空间。Spark-Streaming 的流计算本质上还是批(微批)计算,Apache Flink 就是近年来在开源社区不断发展的技术中的能够同时支持高吞吐、低延迟、高性能的纯实时的分布式处理框架。
Flink(Apache Flink)是一个流式处理和批处理的开源分布式计算框架,它被设计用于在大规模数据集上进行高性能、低延迟的数据处理。Flink 提供了丰富的流式处理和批处理功能,可以应用于多种数据处理场景,如实时数据分析、流式ETL(Extract, Transform, Load)、数据流处理、复杂事件处理(CEP)、机器学习等。
与其他流框架对比
计算引擎的发展经历了几个过程,从第 1 代的 MapReduce,到第 2 代基于有向无环图的 Tez,第 3 代基于内存计算的 Spark,再到第 4 代的 Flink,各框架对比如下:
[图片]
1)模型: Storm 和 Flink 是真正的一条一条处理数据;而 Trident(Storm 的封装框架)和 Spark Streaming 其实都是小批处理,一次处理一批数据(小批量)。
2)API : Storm 和 Trident 都使用基础 API 进行开发,操作相对复杂;而 Spark Streaming 和 Flink 中都提供封装后的高阶函数,可以直接拿来使用,这样就比较方便了。
3)保证次数: 在数据处理方面,Storm 可以实现至少处理一次,但不能保证仅处理一次,这样就会导致数据重复处理问题,所以针对计数类的需求,可能会产生一些误差;Trident 通过事务可以保证对数据实现仅一次的处理,Spark Streaming 和 Flink 也是如此。
4)容错机制: Storm和Trident可以通过ACK机制实现数据的容错机制,而Spark Streaming和 Flink 可以通过 CheckPoint 机制实现容错。
5)状态管理: Storm 中没有实现状态管理,Spark Streaming 实现了基于 DStream 的状态管理,而 Trident 和 Flink 实现了基于操作的状态管理。
6)延时: 表示数据处理的延时情况,因为 Storm 和 Flink 接收到一条数据就处理一条数据,其数据处理的延时性是很低的;而 Trident 和 Spark Streaming 都是小型批处理,它们数据处理的延时性相对会偏高。
7)吞吐量: Storm 的吞吐量其实也不低,只是相对于其他几个框架而言较低;Trident 属于中等;而 Spark Streaming 和 Flink 的吞吐量是比较高的。
Flink四大基石
事件时间(Event Time)
事件时间是数据实际发生的时间。在流式数据处理中,数据可能是乱序的,即事件时间的顺序可能与数据到达 Flink 的顺序不一致。事件时间通常由数据中的时间戳字段表示。Flink 使用事件时间来进行窗口操作、处理延迟数据以及处理乱序数据等。
处理时间(Processing Time)
处理时间是 Flink 接收数据的时间。当数据进入 Flink 时,Flink 使用处理时间作为数据的时间戳,因此处理时间是相对于 Flink 所在的机器的本地时间。处理时间操作通常是最简单和最高效的,但在处理延迟或乱序数据时可能导致结果不准确
水位线(Watermark)
水位线是 Flink 中用于处理事件时间乱序性的重要机制。水位线表示事件时间的进度,Flink 根据水位线决定何时触发窗口计算和处理迟到的数据。水位线的正确设置对于保证事件时间处理的准确性和效率至关重要。
窗口操作
Flink 提供了丰富的窗口操作,如滚动窗口、滑动窗口和会话窗口。通过窗口操作,可以对数据流进行分组和分割,使得数据按照时间或其他属性进行聚合和计算。
窗口(Window)是处理无界流的关键所在。窗口可以将数据流装入大小有限的“桶”中,再对每个“桶”加以处理。 本文的重心将放在 Flink 如何进行窗口操作以及开发者如何尽可能地利用 Flink 所提供的功能。
状态管理
在流式处理中,需要跨事件保持状态以便进行计算和统计。Flink 提供了可扩展的状态管理机制,可以高效地处理大规模状态,并支持容错机制以确保状态数据不丢失。
容错性
Flink 具有高度的容错性,能够自动恢复节点故障,保证数据处理的可靠性。
API
Flink 提供了丰富的API,包括 Java、Scala 和 Python API,使得开发人员可以用自己熟悉的编程语言来开发应用程序。
丰富的库
集成多种数据源
Flink 支持多种数据源,包括文件系统、消息队列(如Kafka、RabbitMQ)、数据库等,可以自定义source和sink请查看官网
Demo
//FlinkKafkaConsumer创建一个 Kafka数据源。然后使用flatMap函数处理从 Kafka 获取到的数据,并使用collector处理后的数据到下游。最后我们将处理后的数据存储到HDFS 中
//导入必要的 Flink 库:
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.sink.filesystem.TextOutputFormat;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import java.util.Properties;
//设置 Flink 环境并创建 Kafka 数据源:
public class KafkaToHDFSDemo {
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "flink-consumer-group");
String topic = "your-kafka-topic";
FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), properties);
DataStream<String> kafkaStream = env.addSource(kafkaConsumer);
//使用 flatMap 函数处理数据并将结果存储到 HDFS:
String hdfsPath = "hdfs://localhost:9000/path/to/output/folder";
TextOutputFormat<String> outputFormat = new TextOutputFormat<>(new org.apache.flink.core.fs.Path(hdfsPath));
DataStream<String> dataStream = kafkaStream.flatMap((String data, Collector<String> out) -> {
//处理这里传入的数据,并向收集器发出单个字符串
String[] words = data.split("\\s+");
for (String word : words) {
out.collect(word);
}
});
dataStream.writeUsingOutputFormat(outputFormat).setParallelism(1);
env.execute("Kafka to HDFS");
}
}
//使用FlinkTable Api
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.core.fs.FileSystem;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import java.util.Properties;
public class KafkaToHDFSDemo {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
EnvironmentSettings settings = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build();
StreamTableEnvironment tEnv = StreamTableEnvironment.create(env, settings);
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("group.id", "flink-consumer-group");
String topic = "your-kafka-topic";
FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<>(topic, new SimpleStringSchema(), properties);
DataStream<String> kafkaStream = env.addSource(kafkaConsumer);
// 定义kafka的schema
String schema = "message STRING";
// Kafka流创建一个临时表
tEnv.createTemporaryView("kafka_table", kafkaStream, schema);
// 插入HDFS sink表的查询
String hdfsSinkPath = "hdfs://localhost:9000/path/to/output/folder";
String insertQuery = "INSERT INTO hdfs_sink SELECT message FROM kafka_table";
// 查询将数据从Kafka同步到HDFS
tEnv.executeSql("CREATE TABLE hdfs_sink (message STRING) " +
"PARTITIONED BY (message) " +
"WITH ('connector' = 'filesystem', 'path' = '" + hdfsSinkPath + "', 'format' = 'csv')");
tEnv.executeSql(insertQuery);
}
}
//自定义UDF
import org.apache.flink.table.functions.ScalarFunction;
public class UpperCaseUDF extends ScalarFunction {
//字符串转大些
public String eval(String input) {
if (input == null) {
return null;
}
return input.toUpperCase();
}
}
//使用 KeyedState 在 DataStream 中对特定键的状态进行管理和维护
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class KeyedStateExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 数据源
DataStream<Tuple2<String, Integer>> dataStream = env.fromElements(
Tuple2.of("key1", 1),
Tuple2.of("key2", 2),
Tuple2.of("key1", 3),
Tuple2.of("key2", 4),
Tuple2.of("key1", 5)
);
// 对key进行聚合
DataStream<Tuple2<String, Integer>> resultStream = dataStream
.keyBy(tuple -> tuple.f0)
.flatMap(new SumFunction());
//输出
resultStream.print();
//执行
env.execute("Keyed State Example");
}
public static class SumFunction extends RichFlatMapFunction<Tuple2<String, Integer>, Tuple2<String, Integer>> {
private transient ValueState<Integer> sumState;
@Override
public void open(Configuration parameters) throws Exception {
// 使用ValueStateDescriptor初始化ValueState
ValueStateDescriptor<Integer> descriptor =
new ValueStateDescriptor<>("sumState", Integer.class, 0);
sumState = getRuntimeContext().getState(descriptor);
}
@Override
public void flatMap(Tuple2<String, Integer> value, Collector<Tuple2<String, Integer>> out) throws Exception {
// 从ValueState中检索当前值
Integer currentSum = sumState.value();
// 将当前值与总和相加
currentSum += value.f1;
// 更新ValueState中的总和
sumState.update(currentSum);
// 发出更新后的总和作为输出
out.collect(Tuple2.of(value.f0, currentSum));
}
}
}
//生产案例
//需求: abtest平台数据支持
//实现步骤:
//1)与后端商定好前台操作行为json传入到kafka
//2)根据规则解析json生成sql并去查找到结果集
//3)把返回结果集写入到mysql中间表库,并在写好通知程序
//4)监控flink程序异常行为(kafka数据挤压,ads层输出数据是否异常等等)
//主代码如下(涉及到一些方法代码请忽略)
public class Parse {
// 创建Logger对象
private static final Logger log = LoggerFactory.getLogger(Parse.class);
public static void main(String[] args) throws Exception {
//配置信息
Properties conf = getProperties("/data/source/data_warehouse/ga/jar/abtestbackground/conf/conf.properties");
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(300000);
env.getCheckpointConfig().setCheckpointTimeout(300000);
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(10, Time.seconds(60)));
//kafka
String bootstrap_server = conf.getProperty("***");
String kafka_topic = conf.getProperty("***");
String group_id = conf.getProperty("***");
String offset_reset = conf.getProperty("***");
//接口url
String msg_url = conf.getProperty("***");
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers(bootstrap_server)
.setTopics(kafka_topic)
.setGroupId(group_id)
.setStartingOffsets(offset_reset.equals("earliest") ? OffsetsInitializer.earliest() : OffsetsInitializer.latest())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
env.setParallelism(2);
DataStreamSource<String> ABTest_Source = env.fromSource(source, WatermarkStrategy.noWatermarks(), "ABTest_Source");
SingleOutputStreamOperator<String> flatMap = ABTest_Source.flatMap(new RichFlatMapFunction<String, String>() {
@Override
public void flatMap(String s, Collector<String> collector) throws Exception {
String str = "";
String pid = "";
try {
str = s;
JSONObject jsonObject = JSON.parseObject(str);
pid = jsonObject.getString("pid");
String is_only_performance_data = jsonObject.getString("is_only_performance_data");
String result = "";
//"is_only_performance_data":"0",//是否只要效果数据(0:返回全部数据;1:只返回实验效果详细数据)
//全部数据:is_only_performance_data=0
if (is_only_performance_data.equals("0")) {
//实验效果基础数据
ArrayList<Object> arrayList = create_Base(str, conf);
//实验效果效果详细数据(单天)--,含覆盖用户数(已废弃)
ArrayList detail_data = create_Detail(str, conf);
arrayList.addAll(detail_data);
//用户覆盖数
ArrayList user_covered = create_User_Covered(str, conf);
arrayList.addAll(user_covered);
Return_Data return_data = new Return_Data();
return_data.setPid(pid);
return_data.setResults(arrayList);
result = JSON.toJSON(return_data).toString();
System.out.println(result);
}
if (is_only_performance_data.equals("1")) {
//实验效果基础数据
ArrayList<Object> arrayList = create_Base(str, conf);
Return_Data return_data = new Return_Data();
return_data.setPid(pid);
return_data.setResults(arrayList);
result = JSON.toJSON(return_data).toString();
System.out.println(result);
/*//实验效果效果详细数据(多天),不含覆盖用户数
ArrayList detail_data = create_Detail(str, conf);
Return_Data return_data = new Return_Data();
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.addAll(detail_data);
return_data.setPid(pid);
return_data.setResults(arrayList);
result = JSON.toJSON(return_data).toString();
System.out.println(result);*/
}
to_MySQL(str, result, conf);
//查询完成,调用接口通知
//线上
//String msgUrl = getMsgUrl(msg_url, "pid=" + pid + "&status=1&msg=写入MySQL成功");
//测试
getTokent_Url(msg_url, "pid=" + pid + "&status=1&msg=写入MySQL成功", "");
} catch (Exception e) {
log.error("JSON 解析失败: " + str);
//任务失败,调用接口通知
//线上
String msgUrl = getMsgUrl(msg_url, "pid=" + pid + "&status=2&msg=JSON 解析失败: " + str);
//测试
getTokent_Url(msg_url, "pid=" + pid + "&status=2&msg=JSON 解析失败: " + str, "");
e.printStackTrace();
System.out.println("JSON 解析失败: " + str);
}
}
});
flatMap.print();
env.execute("ABTest");
}
}
问题解答
1 使用flinkapi实时消费神策上报kafka埋点数据处理
2 618和双11的时候,会提前对数据激增flink程序进行资源调整和并发调整
3 合理使用flink的checkpoint,水位线加窗口等等特性实现容错
4 flink提供一套restapi或者自己写一些代码,当发生异常时报警或自动处理