此篇文章主要目的是用Flink的流迅速开发一个应用,一些需要注意的问题,但不会发散,有兴趣的自己去网上查资料,也不会介绍Flink相关的基础与原理。
partitioning。
这个网上看到一篇关于Watermarks不触发问题(原文:https://www.jianshu.com/p/753e8cf803bb),自己虽然没有遇到,但却发现忽视这个问题,用storm进行bolt组合时平时都有显示指示。
在DataStream文件里面有个种分流接口,同时也可以自定义。
public <K> DataStream<T> partitionCustom(Partitioner<K> partitioner, int field) {
ExpressionKeys<T> outExpressionKeys = new ExpressionKeys(new int[]{field}, this.getType());
return this.partitionCustom(partitioner, (Keys)outExpressionKeys);
}
@PublicEvolving
public DataStream<T> shuffle() {
return this.setConnectionType(new ShufflePartitioner());
}
public DataStream<T> forward() {
return this.setConnectionType(new ForwardPartitioner());
}
public DataStream<T> rebalance() {
return this.setConnectionType(new RebalancePartitioner());
}
注意每种分流情况下数据拷贝传输性能及一些特殊情况,上面Watermarks就是一种特殊情况。
这种设置在实现上也是形成一个新的流:
protected DataStream<T> setConnectionType(StreamPartitioner<T> partitioner) {
return new DataStream(this.getExecutionEnvironment(), new PartitionTransformation(this.getTransformation(), partitioner));
}
Flink有三个时间:事件时间,摄取时间和处理时间。Flink默认使用处理时间,而事实上业务中用的最多的是事件时间,设置接口。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
水位线(Watermarks)是一个非常不错的功能,有两种模式:
AssignerWithPeriodicWatermarks:定时提取更新wartermark
AssignerWithPunctuatedWatermarks:每一个event到来的时候,就会提取一次Watermark
数据量大的时候,频繁的更新wartermark会比较影响性能通,常情况下采用定时提取就足够了。
在存在多个流及多个并发进程时,每个的水位线是独立产生的,但最终的水位线是取其中最小的,也就是上面的坑。
重点介绍下AssignerWithPeriodicWatermarks
public interface TimestampAssigner<T> extends Function {
long extractTimestamp(T var1, long var2);
}
public interface AssignerWithPeriodicWatermarks<T> extends TimestampAssigner<T> {
@Nullable
Watermark getCurrentWatermark();
}
public class TimestampsAndPeriodicWatermarksOperator<T> extends AbstractUdfStreamOperator<T, AssignerWithPeriodicWatermarks<T>> implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {
……
……
public void processElement(StreamRecord<T> element) throws Exception {
long newTimestamp = ((AssignerWithPeriodicWatermarks)this.userFunction).extractTimestamp(element.getValue(), element.hasTimestamp() ? element.getTimestamp() : -9223372036854775808L);
this.output.collect(element.replace(element.getValue(), newTimestamp));
}
}
每一条消息都会通过extractTimestamp获取消息的真实时间。
看一下BoundeOutOfOrdernessTimestampExtractor代码,后续就模仿写自定义的水位线函数。
public abstract class BoundedOutOfOrdernessTimestampExtractor<T> implements AssignerWithPeriodicWatermarks<T> {
private long currentMaxTimestamp;
private long lastEmittedWatermark = -9223372036854775808L;
private final long maxOutOfOrderness;
……
……
public abstract long extractTimestamp(T var1);
public final Watermark getCurrentWatermark() {
long potentialWM = this.currentMaxTimestamp - this.maxOutOfOrderness;
if (potentialWM >= this.lastEmittedWatermark) {
this.lastEmittedWatermark = potentialWM;
}
return new Watermark(this.lastEmittedWatermark);
}
public final long extractTimestamp(T element, long previousElementTimestamp) {
long timestamp = this.extractTimestamp(element);
if (timestamp > this.currentMaxTimestamp) {
this.currentMaxTimestamp = timestamp;
}
return timestamp;
}
……
……
}
BoundeOutOfOrdernessTimestampExtractor保存经过消息最大时间,在取水位线的时候返回,用户需要实现extractTimestamp(T var1)接口返回每条数据真实时间,因为我的数据是秒存储的,所以需要乘以1000
public class ReportTimestamp extends BoundedOutOfOrdernessTimestampExtractor<ReportRecord> {
public ReportTimestamp(Time maxOutOfOrderness) {
super(maxOutOfOrderness);
}
@Override
public long extractTimestamp(ReportRecord record) {
return record.getTimeStamp()*1000;
}
}
因为每一条消息都会通过extractTimestamp(T element, long previousElementTimestamp),所以在这个地方做很多事情,看实现应该每个并发度内不存在多线程并发问题。
使用上面的类,在使用中因为上报数据携带时间是正常时间数小时后,造成水位线异常,所有窗口被purge,后续来的消息也全部认为过期,个人简单加个Filter函数,也可以自定义实现新的函数,因为消息量大且每分钟一定产生,可以让水位线推移速度小于60s这种方式。
Flink的filter进行数据过滤,注意的是返回false数据被过滤掉,返回true为正常通过。
@Public
public interface FilterFunction<T> extends Function, Serializable {
boolean filter(T var1) throws Exception;
}
本以为过滤只是一个函数,但从实现来看是转换成了一个新的流。
@PublicEvolving
public <R> SingleOutputStreamOperator<R> transform(String operatorName, TypeInformation<R> outTypeInfo, OneInputStreamOperator<T, R> operator) {
this.transformation.getOutputType();
OneInputTransformation<T, R> resultTransform = new OneInputTransformation(this.transformation, operatorName, operator, outTypeInfo, this.environment.getParallelism());
SingleOutputStreamOperator<R> returnStream = new SingleOutputStreamOperator(this.environment, resultTransform);
this.getExecutionEnvironment().addOperator(resultTransform);
return returnStream;
}
窗口可以通过时间与数量进行划分,又可以分为滚动与滑动,组合起来就有四种类型,还有Session窗口和全局窗口。虽然也可以定义窗口,但感觉这几个已经足够,特殊场景更多是靠定时器来实现。
定时器是窗口的函数接口,是定制各种特殊场景的核心。通过模仿ContinuousEventTimeTrigger写了个根据时间间隔与次数来激活窗口出数场景的定时器。
public class TimeCountTrigger<W extends Window> extends Trigger<Object, W> {
private static final long serialVersionUID = 1L;
private final ReducingStateDescriptor<Long> timeStateDesc;
private final ReducingStateDescriptor<Long> countStateDesc;
private final long maxcount;
private final long interval;
public TimeCountTrigger(long maxcount, long interval) {
this.maxcount = maxcount;
this.interval = interval;
timeStateDesc = new ReducingStateDescriptor("time-fire", new TimeCountTrigger.Min(), LongSerializer.INSTANCE);
countStateDesc = new ReducingStateDescriptor("count-fire", new TimeCountTrigger.Sum(), LongSerializer.INSTANCE);
}
@Override
public TriggerResult onElement(Object object, long timestamp, W w, TriggerContext triggerContext) throws Exception {
ReducingState<Long> fireTimestamp = (ReducingState)triggerContext.getPartitionedState(this.timeStateDesc);
timestamp = triggerContext.getCurrentProcessingTime();
if (fireTimestamp.get() == null) {
long start = timestamp - timestamp % this.interval;
long nextFireTimestamp = start + this.interval;
triggerContext.registerProcessingTimeTimer(nextFireTimestamp);
fireTimestamp.add(nextFireTimestamp);
}
ReducingState<Long> count = (ReducingState)triggerContext.getPartitionedState(this.countStateDesc);
count.add((long)tpAccumulator.getAccumulatorMap().size());
if ((Long)count.get() >= this.maxcount) {
count.clear();
return TriggerResult.FIRE;
}
return TriggerResult.CONTINUE;
}
@Override
public TriggerResult onProcessingTime(long timestamp, W w, TriggerContext triggerContext) throws Exception {
ReducingState<Long> fireTimestamp = (ReducingState)triggerContext.getPartitionedState(this.timeStateDesc);
if (((Long)fireTimestamp.get()).equals(timestamp)) {
fireTimestamp.clear();
fireTimestamp.add(timestamp + this.interval);
triggerContext.registerProcessingTimeTimer(timestamp + this.interval);
ReducingState<Long> count = (ReducingState)triggerContext.getPartitionedState(this.countStateDesc);
if (count.get() != null && (Long)count.get() > 0) {
count.clear();
return TriggerResult.FIRE;
}
}
return TriggerResult.CONTINUE;
}
@Override
public TriggerResult onEventTime(long l, W w, TriggerContext triggerContext) throws Exception {
return TriggerResult.CONTINUE;
}
@Override
public void clear(W w, TriggerContext triggerContext) throws Exception {
ReducingState<Long> fireTimestamp = (ReducingState)triggerContext.getPartitionedState(this.timeStateDesc);
long timestamp = (Long)fireTimestamp.get();
triggerContext.deleteProcessingTimeTimer(timestamp);
fireTimestamp.clear();
((ReducingState)triggerContext.getPartitionedState(this.countStateDesc)).clear();
}
private static class Min implements ReduceFunction<Long> {
private static final long serialVersionUID = 1L;
private Min() {
}
public Long reduce(Long value1, Long value2) throws Exception {
return Math.min(value1, value2);
}
}
private static class Sum implements ReduceFunction<Long> {
private static final long serialVersionUID = 1L;
private Sum() {
}
public Long reduce(Long value1, Long value2) throws Exception {
return value1 + value2;
}
}
}
根据定时器返回窗口动作:continue(不做任何操作),fire(处理窗口数据),purge(移除窗口和窗口中数据)
onElement是每一条此窗口的消息都需要经过的函数,在这里设置定时器,同时记录次数。发现次数到达限制就返回fire即可。
用processtime就能达到定时多少时间触发onProcessingTime函数,然后重新注册下一周期定时器,同时进行想要的操作。
注意:
ReducingStateDescriptor在创建时名字要不一样,不然好像对拿到相同数据。
定时器不要想每个窗口不同,从效果来看,定时器被激活后会遍历所有窗口,每个窗口通过触发时间进行判断此定时器是不是自己的,所以注册的时候将时间用mode,让所有窗口在同一时间触发定时器,因为触发时间相同而只被触发一次。(为什么定时器没有采用回调应该是照顾checkpoint机制吧,回调这种和内存地址相关的操作会让重启现场恢复变得困难)
窗口操作库自带有有reduce,fold和aggregate,说下aggregate,因为原理都是一样的,不过库函数提供了功能便例。
@PublicEvolving
public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable {
ACC createAccumulator();
ACC add(IN var1, ACC var2);
OUT getResult(ACC var1);
ACC merge(ACC var1, ACC var2);
}
ACC是聚合中间数据缓存,IN是输入数据,OUT也就是窗口最后输出数据。
createAccumulator创建缓存中间数据类,因为用keyby和timewindows偏多,一般希望初使化时便清楚Key与窗口时间,但此为了通用性没有提供。
add是每一条窗口内消息都经过的函数,聚合操作就在这里进行,返回的ACC会被系统保存。
getResult则是在定时触发Fire或者窗口结束前被调用。
个人喜欢aggregate,提供中间状态保存,同时通过定时器可以按固定时间,固定条数,甚至每个消息都触发Fire的方式来调用getResult函数,在ACC中记录在这个周期内数据是否被更新,更新则返回数据,没有更新则返回NULL的方式控制数据输出。
除了上面几个外,还有一个WindowFunction与Evictor,感觉这两个都触发了原始数据的缓存功能,接口中能获取窗口全部数据。
@Public
public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, Serializable {
void apply(KEY var1, W var2, Iterable<IN> var3, Collector<OUT> var4) throws Exception;
}
map与flatmap属于类型转换函数,map是一对一输出,而flatmap则是一对多,通过collector提交就行。
public interface FlatMapFunction<T, O> extends Function, Serializable {
void flatMap(T var1, Collector<O> var2) throws Exception;
}
public interface MapFunction<T, O> extends Function, Serializable {
O map(T var1) throws Exception;
}
Selector能将流进行分拣成多个流,一个消息可以重复出现在多个流中
public class OneTwoSelector implements OutputSelector<DataType> {
public static final String select_1 = "1";
public static final String select_2 = "2";
@Override
public Iterable<String> select(DataType data) {
List<String> typeList = new ArrayList<>();
if (data is select_1_type)
typeList.add(select_1);
if (data is select_2_type)
typeList.add(select_2);
return typeList;
}
}
resultStream.select(OneTwoSelector.select_1)
resultStream.select(OneTwoSelector.select_2)
另一个能达到分流功能的是Side Output,同时还能将flatmap的事一起做了,在window窗口中的allowedLateness便是用了此功能。
最后就是ProcessFunction,也是最底层的接口,处理函数与定时器,然后再加上自定义缓存,基本可以实现上说的接口。
注意:KeyState以及定时器功能都只能在KeyStream中使用
public abstract class ProcessFunction<I, O> extends AbstractRichFunction {
private static final long serialVersionUID = 1L;
public ProcessFunction() {
}
public abstract void processElement(I var1, ProcessFunction<I, O>.Context var2, Collector<O> var3) throws Exception;
public void onTimer(long timestamp, ProcessFunction<I, O>.OnTimerContext ctx, Collector<O> out) throws Exception {
}
public abstract class OnTimerContext extends ProcessFunction<I, O>.Context {
public OnTimerContext() {
super();
}
public abstract TimeDomain timeDomain();
}
public abstract class Context {
public Context() {
}
public abstract Long timestamp();
public abstract TimerService timerService();
public abstract <X> void output(OutputTag<X> var1, X var2);
}
}
flink就是一连串的DataStream连接,每一个DataStream都只做一件事情,Window也是一种特殊的DataStream,相对于其他多了数据缓存,定时器及调度管理。