一.流处理
1.1流处理不同于spark的批次处理,流处理是随时数据来,随时处理是一个多线程的处理方式,例子代码如下.首先是创建流处理的环境StreamExecutionEnvironment,再通过readTextFile方法进行读取数据,再通过扁平算子flatMap进行处理,最后打印。
整个flink,就可以分为Source、Transformation、Sink三大块,Source代表着数据源也就是获取数据,一般都是从Kafka获取数据,Transform处理数据的流程就包含常用的算子map,flatMap,filter等,Sink就是将处理后的数据写入可能是用SQL写入Hbase之类的数据库,也可能是写入hdfs之类的文件系统。
public class Main {
public static void main(String[] args) throws Exception {
//创建环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//读取数据
DataStreamSource<String> stringDataStreamSource = env.readTextFile("input/word.txt");
//进行分词,转换成二元组
SingleOutputStreamOperator<Tuple2<String, Long>> WordAndOneTuple = stringDataStreamSource.flatMap((String line, Collector<Tuple2<String, Long>> out) -> {
String[] words = line.split(" ");
for (String word : words) {
out.collect(Tuple2.of(word, 1L));
}
})
.returns(Types.TUPLE(Types.STRING, Types.LONG));
//按照word进行分组
KeyedStream<Tuple2<String, Long>, Tuple> WordOneGroup = WordAndOneTuple.keyBy(0);
//分组内聚合统计
SingleOutputStreamOperator<Tuple2<String, Long>> sum = WordOneGroup.sum(1);
//打印结果
sum.print();
//执行
env.execute();
}
}
二、一些概念
2.1JobManager和TaskManager以及slots
JobManager负责接收客户端和集群的消息,直接理解为经理,拿到需求后进行资源调度,将对应的任务分发给TaskManager。
TaskManager就是工人,slots就是这个工人能够处理的事件数。
2.2并行度
一个算子可以包含一个或者多个子任务,子任务的个数就是对应的并行度。可以在Java代码中设置每一个算子的并行度,在算子后面直接setParallelism(),设置数字为几并行度就是多少。
2.3ParameterTool
flink的参数解析方式,在主程序中用来解析args传来的参数,则是使用fromArgs,返回的是ParameterTool的实例。
三.flink连接Kafka,以及输出到kafka
首先加入连接依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.11</artifactId>
<version>1.12.3</version>
</dependency>
java测试代码如下,在集群的kafka上调试时,要创建对应的生产者、消费者,topicId要一一对应。
package com.mingtong.sink;
import com.mingtong.pojo.Event;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.serialization.SimpleStringSchema;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;
import java.util.Properties;
/**
* kafka sink练习
*/
public class FlinkKafkaSink {
public static void main(String[] args) throws Exception{
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//连接kafka
Properties properties = new Properties();
properties.setProperty("bootstrap.servers","192.168.2.101:9092");
DataStreamSource<String> kafkaDS = env.addSource(new FlinkKafkaConsumer<String>("clicks", new SimpleStringSchema(), properties));
//对数据做转换处理
SingleOutputStreamOperator<String> result = kafkaDS.map(new MapFunction<String, String>() {
@Override
public String map(String s) throws Exception {
String[] fields = s.split(",");
if (fields.length < 3) {
// 如果输入字符串格式不正确,可以抛出异常或者返回一个错误消息
throw new IllegalArgumentException("Invalid input format: " + s);
}
return new Event(fields[0].trim(),fields[1].trim(), fields[2].trim()).toString();
}
});
//将数据写入kafka
result.addSink(new FlinkKafkaProducer<String>("192.168.2.101:9092", "hello", new SimpleStringSchema()));
env.execute();
}
}
四、时间和窗口
assignTimestampsAndWatermarks:DataStream下面的方法,用于给数据流分配时间和水位线。
WatermarkStrategy:设置水位线的一个大类,如果要设置最大乱序等待时间就可以他下面的方法,需要注意的是<T>forBoundedOutOfOrderness只接受Duration类。forMonotonousTimestamps() :假设时间戳是单调递增的,即每个后续元素的时间戳不会比前一个元素的时间戳小。
WatermarkStrategy.<T>forBoundedOutOfOrderness(maxOutOfOrderness)
withTimestampAssigner:这个方法用于指定指定时间戳分配器,指定如何从流中提取时间戳。
窗口:
steams.KeyBy(data->data.user)
.windows(SlidingEventTimeWindows.of(Time.hours(1))) //滑动事件窗口
.window(TumlblingEventTimeWindows.of(Time.hours(1))//事件时间滚动窗口,每小时的数据
五、状态编程ProcessFunction
5.1KeyProcessFunction:
工作中主要用的ProcessFunction,一般会跟状态一起使用,主要有三个方法,
open,processElement,onTimer方法
open:open方法主要是一些初始化,一些对应的配置,一般不写逻辑在里面。
processElement:主要的处理逻辑的地方,三个参数,第一个参数是传入的要处理的数据类型,第二个是上下文,主要是会用到上下文对应的一些方法,第三个参数想要输出的类型,基本要写的逻辑就都在这哥方法里面。
onTimer:定时器触发,也是三个参数,第一个参数是定时器的时间戳,第二个参数是上下文,第三个参数是输出的类型,如果在processElement注册了定时器,然后想要数据到达对应的时间,执行对应的逻辑
例子代码如下
public class AMFChainProcessFunction extends KeyedProcessFunction<String, AMFChangeInfo, AMFChangeInfo> {
private static final Logger logger = LoggerFactory.getLogger(Thread.currentThread().getStackTrace()[1].getClassName());
private static final long serialVersionUID = -7741877334095410870L;
private transient ValueState<AMFChangeInfo> latestAMFChangeInfo;
@Override
public void open(final Configuration parameters) throws Exception {
// 定义AMF变更信息的状态描述符,用于存储最新的AMF变更信息。
ValueStateDescriptor<AMFChangeInfo> amfChainStat = new ValueStateDescriptor<>("AMFChainStat", AMFChangeInfo.class);
// 配置状态的生命周期,设置为4小时,并且在创建和写入时更新时间戳。
// 状态的可见性设置为从不返回过期状态。
StateTtlConfig ttlConfig = StateTtlConfig
.newBuilder(Time.hours(4)) // 设置TTL为4小时
.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite) // 创建和写入时更新状态的时间戳
.setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired) //
.build();
// 启用状态的TTL配置。
amfChainStat.enableTimeToLive(ttlConfig);
// 通过运行时上下文获取状态,并将其配置为存储最新的AMF变更信息。
this.latestAMFChangeInfo = getRuntimeContext()
.getState(amfChainStat);
// 调用超类的open方法,完成额外的初始化工作。
super.open(parameters);
}
@Override
public void close() throws Exception {
super.close();
}
/**
* 我改了dealType1And2,dealType0所以传参也改了一下
* @param info
* @param ctx
* @param out
* @throws Exception
*/
@Override
public void processElement(final AMFChangeInfo info,
final Context ctx,
final Collector<AMFChangeInfo> out) throws Exception {
if (this.latestAMFChangeInfo.value() == null) {
/*
1.1 -》 开始时间存在且结束时间存在 -》 直接输出,不做状态更新
1.2 -》 开始时间存在,结束时间不存在 -》 更新状态, 输出后来数据
*/
this.latestAMFChangeInfo.update(info);
out.collect(info);
} else if (this.latestAMFChangeInfo.value() != null) {
AMFChangeInfo stat = this.latestAMFChangeInfo.value();
int type = info.getType();
switch (type){
// 类型为0 只有开始信令
case 0 :
dealType0(info, stat,ctx, out);
break;
// 类型为1 用户主动释放,2 用户切换到新mme
case 1 :
case 2 :
dealType1And2(info, stat,ctx, out);
break;
// 3 只有结束信令,n1n2结束信令无四元组信息,故无类型为3的数据
case 3 :
// dealType3(info, stat, out);
break;
case 4 :
out.collect(info);
break;
default:
}
} else {
// 初始状态处理
this.latestAMFChangeInfo.update(info);
out.collect(info);
}
}
/**
* 处理类型为1和2的AMF变更信息。
* 当遇到相同的开始时间和MSISDN的AMF变更信息时,将它们合并,并将合并后的信息输出。
* 合并时,使用stat中的结束时间(如果stat中设置了结束时间)。
* 同时,为合并后的信息注册一个定时器,并更新最新的AMF变更信息。
*
* 我注册了一个定时器,当定时器触发时,会输出合并后的AMF变更信息。
* @param info 输入的AMF变更信息。
* @param stat 用于合并的AMF变更信息。
* @param ctx 上下文信息。
* @param out 用于收集合并后的AMF变更信息的输出流。
* @throws IOException 如果在处理过程中发生I/O错误。
*/
private void dealType1And2(AMFChangeInfo info, AMFChangeInfo stat, Context ctx, Collector<AMFChangeInfo> out) throws IOException {
// 检查info和stat的开始时间和MSISDN是否相同
if (info.getStartTime() == stat.getStartTime() && stat.getMsisdn() == info.getMsisdn()) {
// 合并结束时间,优先使用stat的结束时间,如果未设置,则使用info的结束时间
info.setEndTime(stat.getEndTime() == 0 ? info.getEndTime() : stat.getEndTime());
// 将合并后的信息收集到输出流中
out.collect(info);
// 为合并后的AMF变更信息注册一个定时器
registerTimerForSignalingData(info, ctx);
// 更新最新的AMF变更信息
latestAMFChangeInfo.update(info);
}
}
/**
* 处理类型为0的AMF变更信息。
* 此方法用于处理没有信令活动的AMF变更信息。它比较当前AMF变更信息(info)和之前的AMF变更信息(stat),
* 如果发现之前的AMF变更信息没有结束时间,并且当前AMF变更信息的开始时间晚于之前的开始时间,
* 则将之前的AMF变更信息的结束时间设置为当前AMF变更信息的结束时间,并输出更新后的之前的AMF变更信息。
* 此外,无论条件如何,都会输出当前的AMF变更信息,并更新最新的AMF变更信息。
*
* @param info 当前的AMF变更信息。
* @param stat 之前的AMF变更信息。
* @param ctx 上下文信息,用于注册定时器等操作。
* @param out 用于收集和输出处理后的AMF变更信息。
* @throws IOException 如果输出过程中发生IO异常。
*/
private void dealType0(AMFChangeInfo info, AMFChangeInfo stat,Context ctx, Collector<AMFChangeInfo> out) throws IOException {
// 检查统计信息的结束时间是否未设置且当前信息的开始时间晚于统计信息的开始时间
if (stat.getEndTime() == 0 && info.getStartTime() > stat.getStartTime()){
// 如果条件满足,更新统计信息的结束时间为当前信息的结束时间
stat.setEndTime(info.getEndTime());
// 输出更新后的统计信息
out.collect(stat);
// 为没有信令数据的情况注册定时器
registerTimerForNoSignalingData(stat, ctx);
}
// 无论条件如何,都输出当前的AMF变更信息
out.collect(info);
// 更新最新的AMF变更信息
latestAMFChangeInfo.update(info);
}
/**
* 注册一个定时器,用于处理未发送信号的数据。
* 该方法在接收到特定类型的统计信息时被调用,目的是在30分钟后触发一个事件,
* 以便对未进行信号传输的数据进行处理。定时器的使用有助于延迟处理,
* 在某些情况下,等待更合适的时间点进行操作可能是有益的。
*
* @param stat 统计信息对象,包含需要处理的数据相关信息。
* @param ctx 上下文对象,提供访问定时服务等必要功能的方法。
*/
private void registerTimerForNoSignalingData(AMFChangeInfo stat, Context ctx) {
// 计算30分钟后的时间点
long thirtyMinutesLater = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(30);
// 使用上下文中的定时服务注册一个基于事件时间的定时器
ctx.timerService().registerEventTimeTimer(thirtyMinutesLater);
}
/**
* 注册一个定时器,用于在一小时后发送信号数据。
*
* 此方法通过计算当前时间加上一小时的时间戳,来设定一个一小时后的定时器。
* 定时器使用事件时间模式注册,这意味着它将在一小时后由Flink的定时服务触发。
* 主要用于在特定时间执行某些操作,例如发送统计数据或触发周期性任务。
*
* @param stat 统计信息对象,包含需要被发送的信号数据。
* @param ctx 上下文对象,提供访问定时服务和其他必要信息的方法。
*/
private void registerTimerForSignalingData(AMFChangeInfo stat, Context ctx) {
// 计算一小时后的时间戳
long oneHourLater = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
// 注册一个事件时间定时器,它将在一小时后触发
ctx.timerService().registerEventTimeTimer(oneHourLater);
}
/**
* 当定时器触发时的处理函数。
* 此函数用于处理AMF变更信息的定时任务,主要功能是为最新的AMF变更信息设置结束时间并输出。
*
* @param timestamp 定时器触发的时间戳,用于作为AMF变更信息的结束时间。
* @param ctx 定时器触发的上下文信息,包含相关配置和状态。
* @param out 用于收集和输出AMF变更信息的收集器。
* @throws Exception 如果处理过程中发生异常,则抛出。
*/
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<AMFChangeInfo> out) throws Exception {
// 获取最新的AMF变更信息
AMFChangeInfo stat = this.latestAMFChangeInfo.value();
// 检查AMF变更信息是否存在且结束时间是否未设置
if (stat != null && stat.getEndTime() == 0) {
// 为AMF变更信息设置结束时间,并收集到输出流中
stat.setEndTime(timestamp);
out.collect(stat);
}
}
}
六、checkpoint和状态后端
`Checkpoint `的产生就是为了更加可靠的数据持久化,在`Checkpoint`的时候一般把数据放在在 `HDFS` 上,这就天然的借助了 `HDFS` 天生的高容错、高可靠来实现数据最大程度上的安全,实现了 `RDD` 的容错和高可用。
状态后端:
状态后端是指 Flink 用来管理状态的组件。状态后端负责存储和管理 Flink 作业中的状态信息,这些状态可以是在流处理过程中产生的中间结果,也可以是窗口聚合的结果等
开发中如何保证数据的安全性性及读取效率:可以对频繁使用且重要的数据,先做缓存/持久化,再做 checkpint 操作。
持久化和 Checkpoint 的区别:
位置:Persist 和 Cache 只能保存在本地的磁盘和内存中(或者堆外内存–实验中) Checkpoint 可以保存数据到 HDFS 这类可靠的存储上。
生命周期:Cache 和 Persist 的 RDD 会在程序结束后会被清除或者手动调用 unpersist 方法 Checkpoint 的 RDD 在程序结束后依然存在,不会被删除。
总结
checkPoint自身不会存储数据,而是状态后端存储数据,checkponit只是按照设置来生成检查点,检查点都有状态后端备份的数据,我哪个检查点有问题就会找到对应的最近的成功的检查点,用对应的状态后端备份的数据进行恢复,不用从头恢复数据。