Flink实战

        此篇文章主要目的是用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());
    }

         这里只是部分代码,还就是广播,keyby也算是一种,注意每种分流情况下数据拷贝传输性能及一些特殊情况,上面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,相对于其他多了数据缓存,定时器及调度管理。

        

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值