此处使用docker构建运行环境,当前flink最新版本为1.18.1。若有问题尝试使用指定版本sudo docker pull flink:1.18
//创建网络
sudo docker network create flink-network
//启动主节点
sudo docker run -idt --name=jobmanager -e TZ=Asia/Shanghai --network flink-network -p 8081:8081 --env FLINK_PROPERTIES="jobmanager.rpc.address: jobmanager" flink jobmanager
查看主节点ip
ubuntu@ubuntu:~$ sudo docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(sudo docker ps -aq)
/jobmanager - 172.18.0.2
启动子节点,可启动多个,改name就行,hosts的ip填入上述获取的ip
sudo docker run -idt --name=taskmanager1 -e TZ=Asia/Shanghai --network flink-network --env FLINK_PROPERTIES="jobmanager.rpc.address: jobmanager" --add-host jobmanager:172.18.0.2 flink taskmanager
访问8081,此处启动了两个task
其他常用的配置属性
jobmanager.memory.process.size:JobManager进程可使用到的全部内存
taskmanager.memory.process.size:TaskManager进程可使用到的全部内存
taskmanager.numberOfTaskSlots:TaskManager能够分配的Slot数量,默认为1
parallelism.default:Flink任务执行的并行度,默认为1
maven构建项目
mvn archetype:generate \
-DarchetypeGroupId=org.apache.flink \
-DarchetypeArtifactId=flink-quickstart-java \
-DarchetypeVersion=1.18.1
项目生成后pom文件中mainCalss用于指定默认的启动作业类。上传作业时也可手动指定覆盖
执行环境、数据读入与数据转换
public static void main(String[] args) throws Exception {
/**
* 执行环境与执行模式
* 环境
* getExecutionEnvironment 获取当前环境,自动根据当前运行环境获取,常用
* createLocalEnvironment 获取本地环境
* createRemoteEnvironment 获取集群环境
* 处理模式
* Streaming 流,默认常用
* Batch 批
* AutoMatic 自动
*
*/
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置处理模式,不设置默认也是STREAMING
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
/**
* 源算子(数据源),返回DataStreamSource<T>
* *从集合获取
* List<String> data = Arrays.asList("1", "@", "3");
* env.fromCollection(data);
*
* *直接生成
* env.fromElements("a", "b", "c");
*
* *从文件获取,pom需要额外引入flink-connector-files
* FileSource<String> fileSource = FileSource.forRecordStreamFormat(new TextLineInputFormat(), new Path("input/word.txt")).build();
* env.fromSource(fileSource, WatermarkStrategy.noWatermarks(), "file");
*
* *从socket读入
* env.socketTextStream("172.0.0.5", 6789);
*
* 从kafka读入,需要额外引入flink-connector-kafka
* KafkaSource<String> kafkaSource = KafkaSource.<String>builder()
* .setBootstrapServers("127.0.0.1:9092")
* .setTopics("topic")
* .setGroupId("group")
* .setStartingOffsets(OffsetsInitializer.latest())
* .setValueOnlyDeserializer(new SimpleStringSchema())
* .build();
*
* env.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "kafka");
*
* 从数据生成器读入,flink-connector-datagen
* 略
*/
DataStreamSource<String> source = env.fromElements("a", "b", "c");
/**
* ↑ 合流
* union 合流,只能合并相同类型的流,可一次性合并多条
* source.union(source1);
*
* connect流连接,只能两条连接,返回值变更为ConnectedStreams
* source.connect(source);
* 流连接可连接不同类型流,两条流数据分开处理,处理后返回一条流,
* 双流数据匹配用集合存储,再反向匹配对向流。为保证连接流相同key在相同的slot中处理
* 多并行度下需要根据匹配条件进行keyby,保证相同key数据到一起去才能匹配上
*
*/
/**
* 转换算子(相当于sql体系处理数据而入库而后执行sum count group等操作)
* 返回SingleOutputStreamOperator<T>
* map/filter/flatMap为三个常用的转换算子,大致等价于数据入库。怎样入库,入库那些
*
* map(映射,全部入库)
* SingleOutputStreamOperator<Object> streamOperator = source.map(new MapFunction<String, Object>() {
* @Override
* public Object map(String value) throws Exception {
* return value;
* }
* });
* 上一段可lamb写法简化为 SingleOutputStreamOperator<Object> streamOperator = source.map((String v) -> v);
* 依次输出a,b,c
*
* filter(过滤,选择那些入库)
* source.filter((String v) -> v.equals("a"));
* 只会输出a
*
* flatMap(扁平映射)
* 就是map的增强,map一次只能返回一个值,flatMap使用收集器Collector可以返回多个
* source.flatMap(((String value, Collector<String> out) -> {
* if ("c".equals(value)) {
* out.collect("d");
* out.collect("e");
* out.collect("fgh");
* out.collect("a");
* out.collect("b");
* } else {
* out.collect(value);
* }
* })).returns(String.class);
* 依次输出a,b,d,e,fgh,a,b。使用lamb写法有泛型擦除问题,需要指定returns声明收集器返回类型
*
* process 的侧输出流使用,使用tag对数据进行分流
*/
OutputTag<String> a = new OutputTag<>("a", Types.STRING);
OutputTag<String> b = new OutputTag<>("b", Types.STRING);
SingleOutputStreamOperator<String> streamOperator = source.process(new ProcessFunction<String, String>() {
@Override
public void processElement(String s, Context context, Collector<String> collector) throws Exception {
//将a和b分流,其他的进入主流
if ("a".equals(s)) {
context.output(a, s);
} else if ("b".equals(s)) {
context.output(b, s);
} else {
collector.collect(s);
}
}
});
//获取侧流
streamOperator.getSideOutput(a).print();
streamOperator.getSideOutput(b).print();
//打印输出结果
streamOperator.print();
//启动执行,相当于thread.start();
env.execute("Options Job");
}
数据聚合、函数自定义等
public static void main(String[] args) throws Exception {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//虚拟一组数据,三列
DataStreamSource<Tuple3<String, Integer, Integer>> source = env.fromElements(
Tuple3.of("1", 3, 3),
Tuple3.of("1", 1, 1),
Tuple3.of("2", 2, 2),
Tuple3.of("4", 4, 4)
);
//转换
SingleOutputStreamOperator<Tuple3<String, Integer, Integer>> streamOperator = source.map((Tuple3<String, Integer, Integer> value) -> value)
.returns(Types.TUPLE(Types.STRING, Types.INT, Types.INT));
/**
* 聚合算子
* keyBy 等价于sql group by ?
* 以第一列为key
*/
KeyedStream<Tuple3<String, Integer, Integer>, String> keyedStream = streamOperator.keyBy((Tuple3<String, Integer, Integer> value) -> value.f0);
/**
* 聚合算子,keyBy之后可用
* min,max,sum 最小,最大,求和。入参int为下标,就是数组下标,从0开始为第一位,等价于sql table第一列。string为参数名,传入pojo时可使用,等价于sql table列名
* keyedStream.min(1).print();
* 输出值为
* (1,3,3)
* (1,1,3)
* (2,2,2)
* (4,4,4)
* 上述方法按照key group,只会识别指定的min字段,其他字段保留第一次的值
* keyedStream.minBy(1).print();
* 输出值为
* (1,3,3)
* (1,1,1)
* (2,2,2)
* (4,4,4)
* maxBy,minBy,带by的方法同时也会取其他字段的值
*
* reduce(两两聚合)。一条一条处理,有相同key时触发进入操作
*
* keyedStream.reduce(new ReduceFunction<Tuple3<String, Integer, Integer>>() {
* @Override
* public Tuple3<String, Integer, Integer> reduce(Tuple3<String, Integer, Integer> value1, Tuple3<String, Integer, Integer> value2) throws Exception {
* System.out.println("value1:" + value1);
* System.out.println("value2:" + value2);
* return Tuple3.of(value1.f0, value1.f1 + value2.f1, value1.f2 + value2.f2);
* }
* }).print();
* 输出结果
* (1,3,3)
* value1:(1,3,3)
* value2:(1,1,1)
* (1,4,4)
* (2,2,2)
* (4,4,4)
*
*/
SingleOutputStreamOperator<Tuple3<String, Integer, Integer>> reduce = keyedStream.reduce(new ReduceFunction<Tuple3<String, Integer, Integer>>() {
@Override
public Tuple3<String, Integer, Integer> reduce(Tuple3<String, Integer, Integer> value1, Tuple3<String, Integer, Integer> value2) throws Exception {
System.out.println("value1:" + value1);
System.out.println("value2:" + value2);
return Tuple3.of(value1.f0, value1.f1 + value2.f1, value1.f2 + value2.f2);
}
});
/**
* 自定义函数,此处使用富函数类
* open,close为生命周期管理,每个子任务开始结束时运行。非正常退出close不一定调用
* getRuntimeContext()可用于获取上下文信息,任务编号,名称等等等等
*
*/
reduce.map(new RichMapFunction<Tuple3<String, Integer, Integer>, String>() {
@Override
public void open(Configuration parameters) throws Exception {
getRuntimeContext();
super.open(parameters);
}
@Override
public void close() throws Exception {
super.close();
}
@Override
public String map(Tuple3<String, Integer, Integer> value) throws Exception {
return value.f1 + ":" + String.valueOf(value.f1 + value.f2);
}
}).print();
env.execute();
}
打包后上传作业
确认包名是要执行的作业全路径类名。无误提交
点击最后一个块。此处执行的预览根据作业的不同可能会有1~n个,点最后一个
此处表示任务由那个task节点处理,docker-flink输出会打印到控制台
查看对应的task日志获取打印结果
sudo docker logs -f taskmanager2
窗口,触发器,聚合
窗口
滚动滑动概念模型:统计某段时间地铁过闸人数
按小时统计地铁过闸人数,使用滚动窗口,滚动窗口时长为1小时
统计最近一小时地铁过闸人数,5秒更新一次,使用滑动窗口。窗口长度一小时,步长5秒
某人17:59:55过闸,因为网络不好数据发送到flink服务器时已经18:00:10
事件时间Event
:指17:59:55过闸,实际业务产生的时间
处理时间Processing
:指18:00:10,flink处理数据的时间
窗口在keyby之后获取了keyedStream,调用keyedStream.window()传入
keyedStream.window(SlidingProcessingTimeWindows.of(Time.days(1), Time.seconds(3)))
窗口类型:滑动(时间),滚动(时间),会话,计数,全局
窗口创建(区分事件时间与处理时间)
滑动 TumblingProcessingTimeWindows.of(Time.hours(1))/TumblingEventTimeWindows.of()
滚动 SlidingProcessingTimeWindows.of(Time.hours(1), Time.seconds(5))/SlidingEventTimeWindows.of()
会话 ProcessingTimeSessionWindows.withGap()/EventTimeSessionWindows.withGap()
全局 GlobalWindows.create()
计数窗口不需要显示创建,直接keyedStream.countWindow
会话窗口:首次接收数据时为开始时间,持续多久结束
计数窗口:接收多少个数据后结束
全局窗口:持续统计所有数据
聚合,实现AggregateFunction
implements AggregateFunction<传入数据,中间态数据,返回数据>
例如需要精确计算,传入string,使用bigdecimal计算,结果以string返回 implements AggregateFunction<String, BigDecimal,String>
createAccumulator初始化数据
add聚合操作
getResult返回数据
merge合并操作
触发器,继承Trigger
extends Trigger<入参类型, 窗口类型> -> extends Trigger<String, TimeWindow>
onElement数据进入触发
onProcessingTime处理时间触发
onEventTime事件时间触发
clear清理
非自定义的时间窗口自带了触发器
以org.apache.flink.streaming.api.windowing.triggers.ProcessingTimeTrigger
为例,数据每次进入处理时注册onProcessingTime的触发时间为窗口关闭时间
onProcessingTime内调用输出结果。业务逻辑就是窗口生存期内持续收集数据,结束时输出结果
返回值主要关注TriggerResult.CONTINUE 继续执行,TriggerResult.FIRE 输出结果(调用聚合方法的getResult)
自定义窗口,此处为时间窗口:extends WindowAssigner<入参, 窗口类型> -> extends WindowAssigner<Object, TimeWindow>
assignWindows创建窗口
getDefaultTrigger默认的触发器类型
getWindowSerializer窗口序列化
isEventTime事件窗口
主要关注assignWindows的返回值 -> return Collections.singletonList(new TimeWindow(开始时间, 结束时间));
时间为毫秒时间戳,其他方法可参照其他实现好的窗口复制↓,需要注意窗口类型(Processing,Event)
org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows