实时分析是当前一个比较热门的数据处理技术,因为许多不同领域的数据都需要进行实时处理、计算。到目前为止,有很多技术提供实时的解决方案,包括Storm、Spark Streaming等。这些需求源自于物联网的应用程序需要存储、处理和实时或近实时分析,为了满足这种需求,Flink提供了数据流处理API即DataStream API。
在总结DataStream API之前,我们先简单的了解一下Flink程序的基本运行流程,不论是Data Streaming还是DataSet,Flink程序都主要由以下几个部分组成:1、获取运行时环境(getExecutionEnvironment());2、绑定数据源(addSource);3、对接收的书进行transformation操作;4、dataSink,指定数据计算的输出结果方式(输出到文件或直接打印);5、程序触发执行(execute(jobName))。从上述步骤中,可以看出,我们真正需要账务及操作的就三个过程:Source、Transformation、Sink。DataStream API,即对数据流进行流处理操作,将流式的数据抽象成分布式的数据流,用户可以方便地对分布式数据流进行各种操作,支持Java和Scala。
Flink中的DataStream程序是实现在数据流上的transformation(如filtering, updating state, defining windows, aggregating)的普通程序。创建数据流的来源多种多样(如消息队列,Socket流、文件等)。程序通过Data Sink返回结果,如将数据写入文件,或标准输出。
Flink作为一款优秀的流式计算框架,可以实时的处理实时产生的数据流,只要数据不断,Flink就可以一直计算下去,Data Source就是数据源输入,你可以通过StreamExecutionEnvironment.addSource(sourceFunction)来为你的程序绑定一个数据源。flink提供了大量的已经实现好的source方法,此外我们也可以实现sourcefunction来自定义并行度为1的Source,也可以实现 ParallelSourceFunction 接口或者扩展 RichParallelSourceFunction 来自定义并行的 source。
1、基于socket
env.socketTextStream(hostname, port, delimiter) 从socker指定的ip地址和端口号处读取数据,元素可以通过一个分隔符切开。
package com.bigdata.flink.Stream;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.common.functions.ReduceFunction;
import org.apache.flink.api.java.utils.ParameterTool;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
/**
* 滑动窗口的计算
*
* 通过socket模拟产生单词数据 flink对其进行统计计数
* 实现时间窗口:
* 每隔1秒统计前两秒的数据
*/
public class SocketWindowWordCount {
public static void main(String[] args) throws Exception{
//定义端口号,通过cli接收
int port;
try{
ParameterTool parameterTool = ParameterTool.fromArgs(args);
port = parameterTool.getInt("port");
}catch(Exception e){
System.err.println("No port Set, use default port---java");
port = 9000;
}
//获取运行时环境,必须要
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//绑定Source,通过master的nc -l 900 产生单词
String hostname = "192.168.83.129";
String delimiter = "\n";
//连接socket 绑定数据源
DataStreamSource socketWord = env.socketTextStream(hostname, port, delimiter);
DataStream windowcounts = socketWord.flatMap(new FlatMapFunction() {
public void flatMap(String value, Collector out) throws Exception {
String[] splits = value.split("\\s");
for (String word : splits) {
out.collect(new WordWithCount(word, 1));
}
}
}).keyBy("word")
.timeWindow(Time.seconds(2), Time.seconds(1))
//.sum("count");//这里求聚合 可以用reduce和sum两种方式
.reduce(new ReduceFunction() {
public WordWithCount reduce(WordWithCount a, WordWithCount b) throws Exception {
return new WordWithCount(a.word, a.count + b.count);
}
});
windowcounts.print().setParallelism(1);
env.execute("socketWindow");
}
public static class WordWithCount{
public String word;
public int count;
//无参的构造函数
public WordWithCount(){
}
//有参的构造函数
public WordWithCount(String word, int count){
this.count = count;
this.word = word;
}
@Override
public String toString() {
return "WordWithCount{" +
"word='" + word + '\'' +
", count=" + count +
'}';
}
}
}
2、基于文件
readTextFile(path) , 读取指定路径文本文件,即符合 TextInputFormat 规范的文件,逐行读取并返回。这个方法一般用于测试环境。
3、基于集合
fromCollection(Collection) ,从 Java 的 Java.util.Collection 创建数据流。集合中的所有元素类型必须相同。
4、自定义数据源
addSource 可以实现读取第三方数据源的数据,目前个人觉得最重要的还是读取Kafka数据,这个在之后的章节再写。这里实现两种,第一种实现sourcefunction来自定义并行度为1的Source,第二种实现 ParallelSourceFunction 接口来自定义并行的 source。注意:这两种方法都需要指定数据类型,都在会报类型不匹配。
SourceFunction作为Flink中Source的根接口,定义了两种方法run()和cancel()。run (): 启动一个 source,即对接一个外部数据源然后 emit 元素形成 stream(大部分情况下会通过在该方法里运行一个 while 循环的形式来产生 stream);cancel(): 取消一个 source,也即将 run 中的循环 emit 元素的行为终止。
第一种:无并行度的source,编写数据源,实现run()和cancel()方法;
package com.bigdata.flink.CustormSource;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
/**
* 自定义实现并行度为1的source
* 模拟产生从1开始的递增数字
*
* 注意:
* sourceFunction 和 SourceContext都需要指定数据类型,如果不指定
* 代码运行时会报类型不匹配
*/
public class MyNoParalleSource implements SourceFunction {
private Long count = 0L;
private boolean isRunning = true;
/**
* 主要的方法
* 启动一个Source
* 大部分情况下,都需要在这个run方法里实现一个循环,这样就可以循环产生数据了
* @param ctx
* @throws Exception
*/
@Override
public void run(SourceContext ctx) throws Exception {
while (isRunning){
ctx.collect(count);
count++;
Thread.sleep(1000);
}
}
/**
* 取消一个cancel的时候会调用的方法
*/
@Override
public void cancel() {
isRunning = false;
}
}
获取运行时环境,绑定自定义的数据源MyNoParalleSource()
package com.bigdata.flink.CustormSource;
import com.bigdata.flink.CustormSource.MyNoParalleSource;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
/**
* 使用并行度为1的Source
* Author ambrose
* @Date 2019/3/12 16:07
*/
public class StreamingDemoWithMyNoParalleSource {
public static void main(String[] args) throws Exception {
//获取Flink运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//绑定数据源
DataStreamSource text = env.addSource(new MyNoParalleSource());
//map操作
DataStream num = text.map(new MapFunction() {
@Override
public Long map(Long value) throws Exception {
System.out.println("接收数据:" + value);
return value;
}
});
//每两秒处理一次数据
DataStream sum = num.timeWindowAll(Time.seconds(2)).sum(0);
//打印到控制台,并行度为1
sum.print().setParallelism(2);
env.execute( "StreamingDemoWithMyNoParalleSource");
}
}
第二种:实现 ParallelSourceFunction 接口
package com.bigdata.flink.CustormSource;
import org.apache.flink.streaming.api.functions.source.ParallelSourceFunction;
/**
* 与并行度为1的实现方式区别在于 标志位不一样
* Author ambrose
* @Date 2019/3/12 16:32
*/
public class MyParalleSource implements ParallelSourceFunction {
private Long count = 0L;
private boolean isRunning = true;
/**
* 主要的方法
* 启动一个Source
* 大部分情况下,都需要在这个run方法里实现一个循环,这样就可以循环产生数据了
* @param ctx
* @throws Exception
*/
@Override
public void run(SourceContext ctx) throws Exception {
while (isRunning){
ctx.collect(count);
count++;
Thread.sleep(1000);
}
}
/**
* 取消一个cancel的时候会调用的方法
*/
@Override
public void cancel() {
isRunning = false;
}
}
获取运行时环境,绑定数据源 new MyParalleSource(),设置并行度
package com.bigdata.flink.CustormSource;
import com.bigdata.flink.CustormSource.MyParalleSource;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
/**
* 使用并行度为1的Source
* Author ambrose
* @Date 2019/3/12 16:07
*/
public class StreamingDemoWithMyParalleSource {
public static void main(String[] args) throws Exception {
//获取Flink运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//绑定数据源
DataStreamSource text = env.addSource(new MyParalleSource()).setParallelism(2);
//map操作
DataStream num = text.map(new MapFunction() {
@Override
public Long map(Long value) throws Exception {
System.out.println("接收数据为:" + value);
return value;
}
});
//每两秒处理一次数据
DataStream sum = num.timeWindowAll(Time.seconds(2)).sum(0);
//打印到控制台,并行度为1
sum.print().setParallelism(1);
env.execute( "StreamingDemoWithMyNoParalleSource");
}
}
Data transformation会将一或多个DataStream转换成一个新的DataStream。程序可以将多个transformation结合形成复杂的拓扑结构(topology)。Transformation其实在flink中是处于核心位置,所有数据的转换、处理都是依靠这个部分。
Transformation | 描述 |
Map |
输入一个element,返回一个element。中间可以做一些清洗转换等操作 |
FlapMap | 输入一个element,可以返回出0、1或多个element。 |
Filter | 过滤函数,对传入的数据进行判断,符合条件的数据会被留下 |
KeyBy DataStream -> KeyedStream |
根据指定的key进行分组,相同key的数据会进入同一个分区 dataStream.keyBy("someKey") // Key by field "someKey" |
Reduce | 对数据进行聚合操作,结合当前元素和上一次reduce返回的值进行聚合操作,然后返回一个新的值 |
Aggregations | sum(),min(),max()等聚合操作 |
Window KeyedStream - > WindowedStream |
Window可以定义在已经分区的KeyedStream上。窗口将根据一些特征(如最近5秒到达的数据)将数据按其各自的key集合在一起。 dataStream.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(5))); |
WindowAll DataStream -> AllWindowedStream |
Window可以定义在普通的DataStream上。窗口将根据一些特征(如最近5秒到达的数据)将所有Stream事件集合在一起。 dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5))); |
Union | 将2个或多个data stream合并创建出一个新的包含所有stream的element的stream ,要求合并的两个流类型必须一致 |
Connect | 和union类似,但是只能连接两个流,两个流的数据类型可以不同,会对两个流中的数据应用不同的处理方法。 |
Split | 根据某些标准将Stream分割成2个或更多的stream |
Select SplitStream -> DataStream |
从SplitStream中选择1个或多个stream SplitStream<Integer> split; |
Sink是Flink处理的最后一步,即将实时transformation后的计算结果“落地”到某个地方,可以是Mysql、ES、Kafka、redis等。对应Source的SourceFunction(),Sink也对应有SinkFunction(),SinkFunction()也是sink的根接口,在自定义SinkFunction时要继承RichSinkFunction抽象类,实现其中的方法,包括open()、invoke()、close(),其中最重要的实现就是invoke方法。
下面我们通过平时经常用的.print()方法的源码,简单认识一下SinkFunction()的使用
@PublicEvolving
public class PrintSinkFunction extends RichSinkFunction {
private static final long serialVersionUID = 1L;
private static final boolean STD_OUT = false;
private static final boolean STD_ERR = true;
private boolean target;
private transient PrintStream stream;
private transient String prefix;
//实例化标准输出的sink方法
public PrintSinkFunction() {}
//如果格式化打印为stdErr而不是stdOut,置stderr true
public PrintSinkFunction(boolean stdErr) {
target = stdErr;
}
public void setTargetToStandardOut() {
target = STD_OUT;
}
public void setTargetToStandardErr() {
target = STD_ERR;
}
//重写open方法
@Override
public void open(Configuration parameters) throws Exception {
super.open(parameters);
StreamingRuntimeContext context = (StreamingRuntimeContext) getRuntimeContext();
// get the target stream
stream = target == STD_OUT ? System.out : System.err;
// set the prefix if we have a >1 parallelism
prefix = (context.getNumberOfParallelSubtasks() > 1) ?
((context.getIndexOfThisSubtask() + 1) + "> ") : null;
}
@Override
public void invoke(IN record) {
if (prefix != null) {
stream.println(prefix + record.toString());
}
else {
stream.println(record.toString());
}
}
@Override
public void close() {
this.stream = null;
this.prefix = null;
}
@Override
public String toString() {
return "Print to " + (target == STD_OUT ? "System.out" : "System.err");
}
}
我们可以通过使用result.addSink(new PrintSinkFunction<>());的方式将结果集直接答应在控制台上,也可以使用result.print();打印结果集,这两种方法的效果是一样的。