Flink 入门篇之 写个WordCount

4,441 阅读3分钟

Word Count 是数据处理框架、平台的Hello World。程序作用很简单,就是数有多少英文的单词。我们今天要使用 Flink 1.10 制作一个流版本的 Word Count。在这个流版本的 Word Count 中呢,字符串会以流的方式从输入到Flink中,并且我们会观察在处理过程中,字符统计的情况。

状态!状态!

在 Flink 的官网中,我们可以看到 Flink 社区对于 Flink 的定义是 Apache Flink® — Stateful Computations over Data Streams, 一个在数据流之上的有状态计算引擎。在正式开工之前,我们需要先对于状态有一个很初步很初步的了解。 Flink 官网上对于 State 有非常多专业的解释,我今天先初步在本文里面先谈谈简单的一些理解,感兴趣的朋友呢可以去读读 Flink 官方的解释。

在 Flink 中,任务是使用有向无环图进行描述的,每个图的节点实际上是一个操作函数或算子(Operator),而这些函数和算子在 Flink 处理单一的元素和事件时会存储数据(官网使用的是 remember 记住),因此我们可以说 Flink 的算子和函数是有状态的(Stateful)。状态在Flink中非常重要,我会在后面单独谈谈 Flink 的状态。

何为流

我们在学习程序设计的过程中,会接触到各种各样的流,文件流,网络流,Java 的流 API 等等等等。在 Flink 的世界里,数据可以形成事件流,日志记录,传感器数据,用户的行为等等都可以形成数据流。Flink 官网 中把流分为两类:有界流和无界流。

Flink 官网的解释图

简单来说,无界流有流的起点但是没有终点,数据源源不断地到达,因此我们也需要持续地处理数据。而有界流是有流的结束点的,且数据可以在全部到达后进行处理,因此我们可以把有界流理解为批处理(Batch)。与真正的批处理有所不同的是,Flink是有状态的,每个状态中的数据可能会随着数据的不断到达而发生变化。值得注意的是,由于无界流会源源不断地到达,一般我们会使用时间的特征作为无界流的处理顺序。

搭建 Maven 项目

首先创建一个 Maven 项目,并添加以下关键的依赖:

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <flink.version>1.10.0</flink.version>
    <java.version>1.8</java.version>
    <scala.binary.version>2.11</scala.binary.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-java</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.15</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.7</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

编写 Word Count Job

完成 Maven 后我们需要编写 Word Count 的 任务代码。和普通的 Java 程序一样,Flink 会找到并执行 main 方法中所定义的处理逻辑。首先我们需要在 main 方法的第一句就使用 getExecutionEnvironment() 方法获得程序运行所需要的流环境

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

然后我们需要向流处理的环境注册一个数据源,以源源不断地读入字符串:

DataStream<String> source = env.addSource(new WordCountSource()).name("SteveJobs-Word");

在获取到数据源后,我们需要对数据进行处理。首先是需要使用 flatMap 配合分词工具对输入的字符串进行切分,得到多个二元组。这些二元组的第一个元素是所得的单词,而第二个元素是 1 。为什么是 1 呢,因为我们会根据单词进行分组,每个单词都会拥有一个独立的 状态 ,每个状态都会存储当前处理过的数据信息。在这个样例中,flink 会对每个单词的状态中所有元组的第二个元素进行求和操作。由于第二个元素都是1,可知其求和的结果等于单词的个数。

DataStream<Tuple2<String,Integer>> proc = source.flatMap(new TextTokenizer())
                .keyBy(0)
                .sum(1)
                .name("Process-Word-Count");

在 Flink 中,一切的数据都需要输出到 Sink 中。Sink 可以输出数据到数据库,到 Elasticsearch 中,到 HDFS 中,或是到 Kafka MQ 中。因此,我们需要指定 Flink 的计算结果输出位置。如在本样例中,我们会把数据输出到

proc.addSink(new WordCountSink())
    .name("Word-Count-Sink");

Flink 的任务需要在本地打包,并上传到服务器加载后才可以在 Flink 集群上运行。因此我们需要在 main 方法的末尾,指定任务的名称,并调用环境的 execute 方法启动任务。

env.execute("Word Count");

最终,我们可以得到 WordCountJob 的写法:

public class WordCountJob  {

    public static void main(String[] args) throws Exception {

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> source = env.addSource(new WordCountSource())
                .name("SteveJobs-Word");

        DataStream<Tuple2<String,Integer>> proc = source.flatMap(new TextTokenizer())
                .keyBy(0)
                .sum(1)
                .name("Process-Word-Count");

        proc.addSink(new WordCountSink())
                .name("Word-Count-Sink");

        env.execute("Word Count");
    }

}

数据的 Source 和 Sink

在这个项目中,我们先做一个有界流的试验。在 Flink 中,Source 和 Sink 是数据输入和数据输出的算子,即数据通过 Source 算子输入, 通过 Sink 算子输出。根据刚刚所介绍的,有界流实质上是指批处理,所处理的数据是有限的。因此我们的输入数据源部分我们实现了简单的遍历器数据源 (JavaDoc),直接上代码:

public class WordCountSource extends FromIteratorFunction<String> implements Serializable {

    private static final long serialVersionUID = 0L;

    public WordCountSource() {
        super(new WordIterator());
    }

    private static class WordIterator implements Iterator<String>,Serializable{

        private static final long serialVersionUID = 4L;

        private int index = 0;
        private int length = -1;

        private WordIterator(){
            length = StaticWordData.STEVE_JOBS_WORD.length;
        }

        @Override
        public boolean hasNext() {
            return (index < length);
        }

        @Override
        public String next() {
            return StaticWordData.STEVE_JOBS_WORD[index ++];
        }
    }

}

Sink 作为输出,我们参照了 Flink 官方 Code Walkthroughs 的写法,直接输出到日志中。 具体写法可以参考 Sink 的 JavaDoc ,以下是我的写法:

public class WordCountSink implements SinkFunction<Tuple2<String,Integer>> {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = LoggerFactory.getLogger(WordCountSink.class);

    @Override
    public void invoke(Tuple2<String, Integer> value, Context context) throws Exception {
        logger.info("{ Word: \""+ value.f0 + "\", Cnt:" + value.f1 +"}");
    }
}

测试

在编写好代码以后,使用 Maven 的指令进行打包:

$ mvn clean package

我在之前的更新中介绍了如何搭建单机版 Flink , 传送门 。 我们把单机版 Flink 启动起来,然后选择 Submit New Job ,点击 Add new 通过网页版上传刚刚打包好的包。

上传好的效果图

我们可以点击 Show Plan 查看 我们在程序中定义的执行图。

执行图

点击 Submit,即可提交任务到 Flink 并执行。我们可以在 Task Manager 中,选择并查看当前执行任务的 Task Manager 的 Log。即可看到我们在 Sink 中输出的日志:

Log

可以看到,单词的统计会随着流数据的输入而不断增长。

每次更新啰里啰嗦的话

我提供了本实验的源代码,请在我的 Github 获取:ousheobin/flink-word-count

欢迎大家在上面折腾各种玩法,比如实现无界的数据源,或者把你的 word count 结果输出到 Kafka MQ。