从WordCount开始

类似于学习任何变成语言的Hello World一样,大数据框架的Demo通常从Word Count开始,看一看Flink 是怎么做Word Count的吧~

//DataStrem Api Word Count
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;

public class WindowWordCount {

    public static void main(String[] args) throws Exception {
				//获取Stream执行环境,通常只需要getExcutionEnviroment()方法内部,根据运行环境自动选择Local或Remote
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); 

        DataStream<String> socketStream = env
                .socketTextStream("localhost", 9999);
      					
        DataStream<Tuple2<String,Integet>> countStream = socketStram
          			.flatMap(new Splitter()) 
        				.keyBy(value -> value.f0)  
        				.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
        				.sum(1);

        dataStream.print();

        env.execute("Window WordCount");
    }

  	//自定义Spliter实现FlatMapFunction
    public static class Splitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String sentence, Collector<Tuple2<String, Integer>> out) throws Exception {
            for (String word: sentence.split(" ")) {
                out.collect(new Tuple2<String, Integer>(word, 1));
            }
        }
    }

}

Flink api 编程一般可以分为三个部分:

  1. 数据来源
  2. Transaction算子,转换逻辑
  3. 输出(落地)

数据源

源是flink程序处理的数据来源;

内置数据源

基于文件
  • readTextFile(path): 读取符合TextInputFormat规范的文件,每行一条,生成String类型的数据源;
  • readFile(fileInputFormat, path): 按照指定格式读取数据源;
基于socket
  • socketTextStream: 从socket接口读取数据,元素可以被定界符分隔
从集合生成
  • fromCollection(Collection): 从Java的集合java.util.Collection类生成,所有元素必须类型相同;
  • fromCollection(Iterator,Class): 从迭代器生成,class用来指定迭代器返回的类型;
  • fromElements(T …): 直接从元素队列生成,元素必须类型相同
  • fromParallelConllection(SplitableIterator, class): 从并行的迭代器生成数据流,class用来指定迭代器返回的类型
  • generateSequence(from,to) - 并行的从给定区间创建数据队列

自定义数据源

非并行的数据源-- implements SourceFunction

实现SourceFunction接口,需要重载里面的run方法和cancel 方法,自定义好的接口通过env.addSource()方法接入Flink;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;

import java.util.Random;

public class MyFlinkSource implements SourceFunction<Tuple2<Long,Integer>> {
    private boolean run = true;
    @Override
    public void run(SourceContext ctx) throws Exception {
        while(run){
            ctx.collect(new Tuple2<Long,Integer>(System.currentTimeMillis(),new Random().nextInt(100)));
            Thread.sleep(1000);
        }
    }

    @Override
    public void cancel() {
        this.run = false;
    }
  
  	public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Tuple2<Long, Integer>> tuple2DataStreamSource = env.addSource(new MyFlinkSource());

        tuple2DataStreamSource.print();
        env.execute();

    }
}

ETL

flink 流控限制sink速度 flink keystate_apache

上图是flink官方对于DataSteam之间的转换关系图,通过对数据流的一系列转换,打到计算目的:

无状态转换

map

map是基本转换算子,对数据流中的每条数据进行操作,不改变数据流的条数;

flatmap

map方法只适用于一对转换,flatmap可以实现一对多转换,比如wordcount里面的将一行word按照空格进行拆分;

Keyed Streams

flink 流控限制sink速度 flink keystate_大数据_02

keyBy

将一个流,根据其中的一些属性进行分区,不同类别的流会被放到不同的slot里面去执行,可以物理的理解成对数据流水平切分,每个slot处理不同的类别;

滚动更新 max/min/sum

max跟min(int filedPosition,String fieldName),max会更新指定position的字段信息,其他同key下的保留第一个流的信息不变,比如:

文本流:

1,Alice,Chinese,60
1,Alice,Math,77
1,Alice,History,89
2,Bob,Chinese,78
2,Bob,English,100
3,Tom,English,88
3,Tom,Chinese,66
4,Jerry,Chinese,67
4,Jerry,Math,29
根据cid分组keyby,滚动更新grade的最高值:
public class KeyedStreamTest {
 public static void main(String[] args) throws Exception {
     StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
     env.setParallelism(1);

     DataStreamSource<String> source = env.readTextFile("/Users/dinl/Data/code/personal/flink/try-flink/src/main/resources/my_text_source.txt");

     DataStream stream = source
             .map(Person::new)
             .keyBy(Person::getCid)
             .max("grade");
     stream.print();
     env.execute();
 }
}

输出:

Person{cid=1, name='Alice', subject='Chinese', grade=60}
Person{cid=1, name='Alice', subject='Chinese', grade=77}
Person{cid=1, name='Alice', subject='Chinese', grade=89}
Person{cid=2, name='Bob', subject='Chinese', grade=78}
Person{cid=2, name='Bob', subject='Chinese', grade=100}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}

sum同理,只会滚动更新grade的总值;

滚动取值 maxBy/minBy

maxBy跟max的区别是: max是在每个keyedStream里面取第一条流,更新max指定字段的最大值,而maxBy则会以整条流为单位,保留该字段最大的那整条流的信息,相当于滚动取值,上述例子maxby运行结果是:

Person{cid=1, name='Alice', subject='Chinese', grade=60}
Person{cid=1, name='Alice', subject='Math', grade=77}
Person{cid=1, name='Alice', subject='History', grade=89}
Person{cid=2, name='Bob', subject='Chinese', grade=78}
Person{cid=2, name='Bob', subject='English', grade=100}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}
滚动更新算子 reduce

reduce可以实现自定义的聚合,但并非MapReduce那种多条流聚合成一条流,而是会计算与上一条流的聚合方式,1+2-> update(2), 2+3->update(3)…

把第一条跟第二条流的聚合结果,更新到第二条流上,然后用新的第二条流跟第三条流聚合,结果更新到第三条上…

见例子:

public class KeyedStreamTest {
 public static void main(String[] args) throws Exception {
     StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
     env.setParallelism(1);

     DataStreamSource<String> source = env.readTextFile("/Users/dinl/Data/code/personal/flink/try-		      flink/src/main/resources/my_text_source.txt");

     DataStream stream = source
             .map(Person::new)
             .keyBy(Person::getCid)
             .reduce((o1, o2) ->
                     new Person(o1.getCid(),
                             o1.getName(),
                             o1.getSubject() + "," + o2.getSubject(), //把两个科目用逗号连起来
                             o1.getGrade() + o2.getGrade())); //分数直接相加
     stream.print();
     env.execute();
 }

}

上述reduce的运行结果是:

Person{cid=1, name='Alice', subject='Chinese', grade=60}
Person{cid=1, name='Alice', subject='Chinese,Math', grade=137}
Person{cid=1, name='Alice', subject='Chinese,Math,History', grade=226}
Person{cid=2, name='Bob', subject='Chinese', grade=78}
Person{cid=2, name='Bob', subject='Chinese,English', grade=178}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=3, name='Tom', subject='English,Chinese', grade=154}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}
Person{cid=4, name='Jerry', subject='Chinese,Math', grade=96}

有状态转换

Flink为什么要参与状态管理?

flink为管理状态提供了一些引人注目的特性:

  • 本地性 : 为了保证访问速度,flink把状态存储在本地内存中;
  • 持久性 : flink状态是容错的,可以按照一定时间间隔产生checkpoint,并且在任务失败后进行恢复;
  • 纵向扩展性: flink状态可以存储在RocksDB实例中,可以通过增加本地磁盘来扩展存储空间;
  • 横向扩展性: flink状态可以随着集群扩展重新分布;
  • 可查询性: flink状态可以通过状态查询API给外界反馈;
keyed

为了保证状态初始化,flink对于上述MapFunction/FilterFunction/FlatMapFunction提供了"rich"变体;

如RichFlatMapFunction,其中增加了以下方法,包括

  • open(Configuration c) :仅在算子初始化时调用一次,可以用来加载一些静态数据,或者建立外部服务的链接;
  • close() : 是生命周期中的最后一个调用的方法,清理状态。
  • getRuntimeContext() : 是程序创建和访问flink状态的途径;

example: 保留每个组第一个流

public class MyFlatMapper extends RichFlatMapFunction<Person, Person> {
    ValueState<Boolean> keyHasBeenSeen;

    @Override
    public void open(Configuration conf) {
        ValueStateDescriptor<Boolean> desc = new ValueStateDescriptor<>("keyHasBeenSeen", Types.BOOLEAN);
        keyHasBeenSeen = getRuntimeContext().getState(desc);
    }

    @Override
    public void flatMap(Person value, Collector<Person> out) throws IOException {
      	//当key没有见过的时候collect一条
        if(keyHasBeenSeen.value()==null){
            out.collect(value);
          	//collect完之后将状态置为true
            keyHasBeenSeen.update(true);
        }
    }
}


public class KeyedStreamTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        DataStreamSource<String> source = env.readTextFile("/Users/dinl/Data/code/personal/flink/try-flink/src/main/resources/my_text_source.txt");
        DataStream stream = source
                .map(Person::new)
                .keyBy(Person::getCid)
                .flatMap(new MyFlatMapper());

        stream.print();
        env.execute();
    }

}

使用上述例子运行该代码,输出如下:

Person{cid=1, name='Alice', subject='Chinese', grade=60}
Person{cid=2, name='Bob', subject='Chinese', grade=78}
Person{cid=3, name='Tom', subject='English', grade=88}
Person{cid=4, name='Jerry', subject='Chinese', grade=67}
No-keyed

在没有键的上下文中我们也可以使用 Flink 管理的状态。这也被称作算子的状态。它包含 的接口是很不一样的,由于对用户定义的函数来说使用 non-keyed state 是不太常见的, 所以这里就介绍了。这个特性最常用于 source 和 sink 的实现。