优化Flink应用的4种方式

原文:4 Ways to Optimize Your Flink Applications 

作者:Ivan Mushketyk 

翻译:Diwei

译者注:Apache Flink是一个面向分布式数据流处理和批量数据处理的开源计算平台。作者在本文介绍了一些如何优化Flink应用速度的方式。以下为译文。

Flink框架非常复杂,并提供了许多方法来调整其执行方式。本文我将介绍提高Flink应用程序性能的四种不同方法。

如果不熟悉Flink,你可以阅读一些介绍性的文章,比如这篇这篇,还有这篇。但是如果已经非常熟悉Apache Flink了,本文描述的内容可以帮助你如何提高应用程序的运行速度。

使用Flink Tuples

当使用类似于groupBy、join或keyBy这些操作时,Flink提供了多种方式以便用户在数据集中选择主键。用户可以使用主键选择函数:

// Join movies and ratings datasets

movies.join(ratings)

        // Use movie id as a key in both cases

        .where(new KeySelector<Movie, String>() {

            @Override

            public String getKey(Movie m) throws Exception {

                return m.getId();

            }

        })

        .equalTo(new KeySelector<Rating, String>() {

            @Override

            public String getKey(Rating r) throws Exception {

                return r.getMovieId();

            }

        })


也可以在POJO类型中指定字段名称:

movies.join(ratings)

// Use same fields as in the previous example

.where("id")

.equalTo("movieId")


但是如果现在使用的是Flink tuple类型,那么只要简单地指定字段元组的位置,就可以被用作主键了:

DataSet<Tuple2<String, String>> movies = ...

DataSet<Tuple3<String, String, Double>> ratings = ...

movies.join(ratings)

    // Specify fields positions in tuples

    .where(0)

    .equalTo(1)


可见最后一种方式的性能是最好的,但是可读性怎么样呢?代码现在看起来是不是就像下面这样?

DataSet<Tuple3<Integer, String, Double>> result = movies.join(ratings)

    .where(0)

    .equalTo(0)

    .with(new JoinFunction<Tuple2<Integer,String>, Tuple2<Integer,Double>, Tuple3<Integer, String, Double>>() {

        // What is happening here?

        @Override

        public Tuple3<Integer, String, Double> join(Tuple2<Integer, String> first, Tuple2<Integer, Double> second) throws Exception {

            // Some tuples are joined with some other tuples and some fields are returned???

            return new Tuple3<>(first.f0, first.f1, second.f1);

        }

    });


在本例中,想要提高可读性,最常见的做法就是创建一个类,该类需要继承TupleX类,并为类里面的这些字段实现getter和setter。下面是Flink Gelly库的Edge类,继承了Tuple3类:

public class Edge<K, V> extends Tuple3<K, K, V> {

    public Edge(K source, K target, V value) {

        this.f0 = source;

        this.f1 = target;

        this.f2 = value;

    }

    // Getters and setters for readability

    public void setSource(K source) {

        this.f0 = source;

    }

    public K getSource() {

        return this.f0;

    }

    // Also has getters and setters for other fields

    ...

}


复用Flink对象

另一个可以用来提高Flink应用程序性能的选项是,当从用户定义的函数返回数据时,最好使用可变对象。看看下面这个例子:

stream

    .apply(new WindowFunction<WikipediaEditEvent, Tuple2<String, Long>, String, TimeWindow>() {

        @Override

        public void apply(String userName, TimeWindow timeWindow, Iterable<WikipediaEditEvent> iterable, Collector<Tuple2<String, Long>> collector) throws Exception {

            long changesCount = ...

            // A new Tuple instance is created on every execution

            collector.collect(new Tuple2<>(userName, changesCount));

        }

    }


可以看出,apply函数每执行一次,都会新建一个Tuple2类的实例,因此增加了对垃圾收集器的压力。解决这个问题的一种方法是反复使用相同的实例:

stream

    .apply(new WindowFunction<WikipediaEditEvent, Tuple2<String, Long>, String, TimeWindow>() {

        // Create an instance that we will reuse on every call

        private Tuple2<String, Long> result = new Tuple<>();

        @Override

        public void apply(String userName, TimeWindow timeWindow, Iterable<WikipediaEditEvent> iterable, Collector<Tuple2<String, Long>> collector) throws Exception {

            long changesCount = ...

            // Set fields on an existing object instead of creating a new one

            result.f0 = userName;

            // Auto-boxing!! A new Long value may be created

            result.f1 = changesCount;

            // Reuse the same Tuple2 object

            collector.collect(result);

        }

    }


这种做法更好一点。虽然每次调用时都新建一个Tuple2的实例,但是其实还间接创建了Long类的实例。为了解决这个问题,Flink有许多所谓的value class:IntValue、LongValue、StringValue、FloatValue等。下面介绍一下如何使用它们:

stream

    .apply(new WindowFunction<WikipediaEditEvent, Tuple2<String, Long>, String, TimeWindow>() {

        // Create a mutable count instance

        private LongValue count = new IntValue();

        // Assign mutable count to the tuple

        private Tuple2<String, LongValue> result = new Tuple<>("", count);

        @Override

        // Notice that now we have a different return type

        public void apply(String userName, TimeWindow timeWindow, Iterable<WikipediaEditEvent> iterable, Collector<Tuple2<String, LongValue>> collector) throws Exception {

            long changesCount = ...

            // Set fields on an existing object instead of creating a new one

            result.f0 = userName;

            // Update mutable count value

            count.setValue(changesCount);

            // Reuse the same tuple and the same LongValue instance

            collector.collect(result);

        }

    }


这种做法经常用在Flink库里面,如Flink Gelly。

使用注解功能

优化Flink应用程序的另一种方法是提供一些关于用户自定义的函数会对输入数据做哪些操作的信息。由于Flink无法解析和理解代码,所以可以提供一些有利于构建更有效执行计划的重要信息。可以使用以下三个注解:

@ForwardedFields:指定输入值中哪些字段保持不变,哪些字段是用于输出的。

@NotForwardedFields:指定在输出中未保留相同位置的字段。

@ReadFields:指定用来计算结果值的字段。指定的字段应该只在计算中使用,而不仅仅是复制到输出参数中。

看一下如何使用ForwardedFields注释:

// Specify that the first element is copied without any changes

@ForwardedFields("0")

class MyFunction implements MapFunction<Tuple2<Long, Double>, Tuple2<Long, Double>> {

    @Override

    public Tuple2<Long, Double> map(Tuple2<Long, Double> value) {

      // Copy first field without change

        return new Tuple2<>(value.f0, value.f1 + 123);

    }

}


这意味着输入元组中的第一个元素没有被更改,它将返回到相同的位置。

如果不更改字段,但只需将其移动到另一个位置,那么也可以使用ForwardedFields。在下一个示例中,我们在输入tuple中互换一下字段,并通知Flink:

// 1st element goes into the 2nd position, and 2nd element goes into the 1st position

@ForwardedFields("0->1; 1->0")

class SwapArguments implements MapFunction<Tuple2<Long, Double>, Tuple2<Double, Long>> {

    @Override

    public Tuple2<Double, Long> map(Tuple2<Long, Double> value) {

      // Swap elements in a tuple

        return new Tuple2<>(value.f1, value.f0);

    }

}


上面提到的注解只能应用于只有一个输入参数的函数,例如map或flatMap。如果函数有两个输入参数,则可以使用ForwardedFieldsFirst和ForwardedFieldsSecond,分别提供关于第一个参数和第二个参数的信息。

下面是如何在JoinFunction接口的实现中使用这些注释:

// Two fields from the input tuple are copied to the first and second positions of the output tuple

@ForwardedFieldsFirst("0; 1")

// The third field from the input tuple is copied to the third position of the output tuple

@ForwardedFieldsSecond("2")

class MyJoin implements JoinFunction<Tuple2<Integer,String>, Tuple2<Integer,Double>, Tuple3<Integer, String, Double>>() {

    @Override

    public Tuple3<Integer, String, Double> join(Tuple2<Integer, String> first, Tuple2<Integer, Double> second) throws Exception {

        return new Tuple3<>(first.f0, first.f1, second.f1);

    }

})


Flink还提供NotForwardedFieldsFirst、NotForwardedFieldsSecond、ReadFieldsFirst ReadFirldsSecond注释,这些注释都可以达到类似目的。

Select Join Type

如果给Flink另一个提示,那么就可以让joins速度更快,但是在讨论它的工作原理之前,先讨论一下Flink是如何执行joins的。

当Flink处理批量数据时,集群中的每台机器都存储了部分数据。要执行join,Apache Flink需要找到满足连接条件的两个数据集。为了做到这一点,Flink首先必须将两个数据集的项目放在同一台机器上。这里有两种策略:

Repartition-分配策略:在这种情况下,两个数据集都被各自的主键分离了,并通过网络发送。这意味着如果数据集很大,可能需要大量的时间才能通过网络完成复制。

广播转发策略:在这种情况下,一个数据集不受影响,但是第二个数据集被复制到集群中的每台机器上,它们都有第一个数据集的一部分。

如果是将某个小数据集join到更大的数据集,那么可以使用广播转发策略,这样也可以避免第一个数据集的分区付出的昂贵代价。这很容易做到:

ds1.join(ds2, JoinHint.BROADCAST_HASH_FIRST)


这就表示第一个数据集比第二个数据集小得多。

你也可以使用其他连接提示:

BROADCAST_HASH_SECOND:第二个数据集要小得多

REPARTITION_HASH_FIRST:第一个数据集稍微小一些

REPARTITION_HASH_SECOND:第二个数据集要小一点

REPARTITION_SORT_MERGE:使用排序和合并策略对数据集进行重新分配

**OPTIMIZER_CHOOSES:**Flink优化器将决定如何join数据集

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容