Flink基本架构
jobmanage
资源管理,任务管理
管理整个资源,负责任务的调度,checkpoint(对taskmanage计算的结果进行保存)
taskmanage
jobmanage 相当于一个master taskmanage 相当于一个 slave 集群启动时,taskmanage会向taskmange注册,注册taskmanage所能管理的资源,jobmanage就通过管理taskmange进而管理整个集群的资源。taskmanage在管理资源的时候会对资源进行划分,划分成一个个slot,每一个slot代表一个资源,每个slot的内存是隔离的。taskmanage会接受jobmanage发过来的task,然后放到slot中去执行,slot代表资源,task代表一个个线程
Flink安装启动
修改配置文件conf/flink-conf.yaml
# JobManager地址
jobmanager.rpc.address: node1
# JobManagerRPC通信端口
jobmanager.rpc.port: 6123
# JobManager所能使用的堆内存大小
jobmanager.heap.size: 1024m
# TaskManager所能使用的堆内存大小
taskmanager.heap.size: 1024m
# taskmanager配置多少个槽,把资源分成多少组依据当前物理机的核心数来配置,一般预留出一部分核心(25%)给系统及其他进程使用
# 一个slot对应一个core,如果core支持超线程,那么slot个数*2
taskmanager.numberOfTaskSlots: 2
# 指定WebUI的访问端口
rest.port: 8081
修改配置文件slaves 用于配置从节点
node02
node03
node04
启动和关闭
start-cluster.sh
stop-cluster.sh
添加任务
- 通过命令添加
flink run -c com.msb.stream.WordCount StudyFlink-1.0-SNAPSHOT.jar- -c:指定主类
- -d:独立运行、后台运行
- -p:指定并行度
- 通过web界面添加: web页面提交任务,可以通过flink-conf.yaml中的web.submit.enable: false来禁用,默认开启
- 关闭任务,可以通过web界面来关闭,也可以通过命令
flink list flink cancel id号
scala-shell测试
# hostname为jobmanage的ip
start-scala-shell.sh remote <hostname> <portnumber>
注意:只有调用了execute才会执行任务
jobManage 容错方案
由于jobManage负责的事情很多,他也很重要,如果jobmanage宕机,整个集群将会瘫痪,所以有两个解决方案,一个是搭建HA高可用集群,一个是分担JobManage的压力
HA高可用集群
概念
部署多台jobmanage,其中一个处于运行(active),其余都处于候补状态(standby)
搭建
-
修改配置文件flink-conf.yaml, master
# flink-conf.yaml # 将其注释打开,使用zookeeper来做选举 high-availability: zookeeper # 将其注释打开,存储目录放到hdfs上,保存JobManage恢复所需要的元数据 # spark是将数据放到zookeeper上,但是flink擅长做有状态的计算,所以选择hdfs high-availability.storageDir: hdfs://centosa:9000/flink/ha/ # 配置zookeeper的集群地址,写一个两个都行,最好是全部写上 high-availability.zookeeper.quorum: centosa:2181,centosb:2181,centosc:2181# masters # 配置所有的jobmanage centosa:8081 centosb:8081如果需要将数据放到hdfs上,需要下载一个hadoop插件,并且拷贝到各个节点flink安装包的lib目录下
下载地址:repo.maven.apache.org/maven2/org/… -
单独启动一个JobManage,TaskManage
# 会根据flink-conf.yaml找到jobmanage的地址,然后向jobmanage注册 jobmanage.sh taskmanage.sh
flink on yarn(分担JobManage的压力)
概念
此时flink从资源管理,任务管理变成仅需进行任务管理,资源管理则交给yarn。实际上向yarn上提交一个任务就相当于在yarn所管理的资源上启动一个flink集群。
优点:
- 分担jobmanage压力
- 降低维护成本
运行流程图
两种提交任务的方式
yarn-session
在提交之前需要先yarn中启动一个flink集群(yarn-session);启动成功后通过flink run向集群中提交任务。当job执行完毕,yarn-session集群并不会关闭,等待下个job的提交。JobManager会一直占用资源,只有当提交了任务后TaskManager才会启动,由于yarn是细粒度的,所以需要多少启动多少,不会多启动TaskManager。当任务被关闭,TaskManager也会随之被关闭
run a flink job on yarn
直接向yarn中提交一个flink job,在job执行之前,先去启动一个flink集群,集群启动成功后,job再执行,当job执行完毕,flink集群一同被关闭,释放了资源。
配置
-
Flink on Yarn依赖Yarn集群和HDFS集群,启动Yarn、HDFS集群 start-all.sh
-
下载支持Hadoop插件并且拷贝到各个节点的安装包的lib目录下
下载地址:repo.maven.apache.org/maven2/org/… -
yarn-session
-
在yarn中启动Flink集群
# 启动 yarn-session.sh -n 3 -s 3 -nm flink-session -d -q # 关闭 yarn application -kill applicationId yarn-session选项: -n,--container <arg>:在yarn中启动container的个数,实质就是TaskManager的个数 -s,--slots <arg>:每个TaskManager管理的Slot个数 -nm,--name <arg>:给当前的yarn-session(Flink集群)起一个名字 -d,--detached:后台独立模式启动,守护进程 -tm,--taskManagerMemory <arg>:TaskManager的内存大小 单位:MB -jm,--jobManagerMemory <arg>:JobManager的内存大小 单位:MB -q,--query:显示yarn集群可用资源(内存、core) -
提交Flink Job到yarn-session集群中运行
flink run -c com.msb.stream.WordCount -yid application_1586794520478_0007 ~/StudyFlink-1.0-SNAPSHOT.jar yid:指定yarn-session的ApplicationID 不使用yid也可以,因为在启动yarn-session的时候,在tmp临时目录下已经产生了一个隐藏小文件 [root@node01 bin]# vim /tmp/.yarn-properties-root #Generated YARN properties file #Mon Apr 13 23:39:43 CST 2020 parallelism=9 dynamicPropertiesString= applicationID=application_1586791887356_0001
-
-
Run a Flink job on YARN模式配置
flink run -m yarn-cluster -yn 3 -ys 3 -ynm flink-job -c com.msb.stream.WordCount ~/StudyFlink-1.0-SNAPSHOT.jar -yn,--container <arg> 表示分配容器的数量,也就是TaskManager的数量。 -d,--detached:设置在后台运行。 -yjm,--jobManagerMemory<arg>:设置JobManager的内存,单位是MB。 -ytm,--taskManagerMemory<arg>:设置每个TaskManager的内存,单位是MB。 -ynm,--name:给当前Flink application在Yarn上指定名称。 -yq,--query:显示yarn中可用的资源(内存、cpu核数) -yqu,--queue<arg> :指定yarn资源队列 -ys,--slots<arg> :每个TaskManager使用的Slot数量。
flink on yarn HA
-
修改Hadoop安装包下的yarn-site.xml文件,记得同步到所有节点并重启yarn
<property> <name>yarn.resourcemanager.am.max-attempts</name> <value>10</value> <description> The maximum number of application master execution attempts AppMaster最大重试次数 </description> </property> -
修改Flink安装包下的flin-conf.yaml文件
high-availability: zookeeper high-availability.storageDir: hdfs://node01:9000/flink/ha/ high-availability.zookeeper.quorum: node01:2181,node02:2181,node03:2181
API 编程
导包
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.11</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.11</artifactId>
<version>1.10.0</version>
</dependency>
示例
public class JavaWordCount {
public static void main(String[] args) {
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> initStream = env.socketTextStream("CentOSA", 8888);
SingleOutputStreamOperator<String> wordStream = initStream.flatMap((FlatMapFunction<String, String>) (value, out) -> {
for (String s : value.split(" ")) {
out.collect(s);
}
}).returns(String.class);
SingleOutputStreamOperator<Tuple2<String, Integer>> pairStream = wordStream.map((value) -> new Tuple2<>(value, 1)).returns(Types.TUPLE(Types.STRING, Types.INT));
KeyedStream<Tuple2<String, Integer>, String> keyByStream = pairStream.keyBy((value) -> value.f0);
SingleOutputStreamOperator<Tuple2<String, Integer>> resStream = keyByStream.sum(1);
resStream.print();
try {
env.execute("java flink");
} catch (Exception e) {
e.printStackTrace();
}
}
}
- setParallelism
- disableChaining
- startNewChain
taskslot 个数 决定的并行度,taskslot为n,并行度最多为n
各种流之间的关系
DataStreamSource
DataStream
SingleOutputStreamOperator
KeyedStream
DataStream Transformations(数据流转换)
Map
DataStream → DataStream
遍历数据流中的每一个元素,产生一个新的元素
DataStream<Integer> dataStream = //...
dataStream.map(new MapFunction<Integer, Integer>() {
@Override
public Integer map(Integer value) throws Exception {
return 2 * value;
}
});
FlatMap
DataStream → DataStream
遍历数据流中的每一个元素,产生N个元素 N=0,1,2,......
dataStream.flatMap(new FlatMapFunction<String, String>() {
@Override
public void flatMap(String value, Collector<String> out)
throws Exception {
for(String word: value.split(" ")){
out.collect(word);
}
}
});
Filter
DataStream → DataStream
过滤算子,根据数据流的元素计算出一个boolean类型的值,true代表保留,false代表过滤掉
dataStream.filter(new FilterFunction<Integer>() {
@Override
public boolean filter(Integer value) throws Exception {
return value != 0;
}
});
KeyBy
DataStream → KeyedStream
根据数据流中指定的字段来分区,相同指定字段值的数据一定是在同一个分区中,内部分区使用的是HashPartitioner
dataStream.keyBy(value -> value.getSomeKey());
dataStream.keyBy(value -> value.f0);
Reduce
KeyedStream → DataStream
将当前元素与上一个reduce的值组合,并发出新值。对组内的所有值连续使用reduce,直到留下最后一个值!
keyedStream.reduce(new ReduceFunction<Integer>() {
@Override
public Integer reduce(Integer value1, Integer value2)
throws Exception {
return value1 + value2;
}
});
Window
KeyedStream → WindowedStream
Windows可以在已经分区的KeyedStreams上定义。Windows根据某些特征(例如,最近5秒内到达的数据)对每个键中的数据进行分组。
dataStream
.keyBy(value -> value.f0)
.window(TumblingEventTimeWindows.of(Time.seconds(5)));
WindowAll
DataStream → AllWindowedStream
Windows可以在常规的数据流上定义。Windows根据某些特征(例如,最近5秒内到达的数据)对所有流事件进行分组。
dataStream
.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));
Window Apply
WindowedStream → DataStream
AllWindowedStream → DataStream
将通用函数应用于整个窗口。
windowedStream.apply(new WindowFunction<Tuple2<String,Integer>, Integer, Tuple, Window>() {
public void apply (Tuple tuple,
Window window,
Iterable<Tuple2<String, Integer>> values,
Collector<Integer> out) throws Exception {
int sum = 0;
for (value t: values) {
sum += t.f1;
}
out.collect (new Integer(sum));
}
});
// applying an AllWindowFunction on non-keyed window stream
allWindowedStream.apply (new AllWindowFunction<Tuple2<String,Integer>, Integer, Window>() {
public void apply (Window window,
Iterable<Tuple2<String, Integer>> values,
Collector<Integer> out) throws Exception {
int sum = 0;
for (value t: values) {
sum += t.f1;
}
out.collect (new Integer(sum));
}
});
Window Reduce
WindowedStream → DataStream
将一个函数reduce函数应用到窗口并返回减少的值。
windowedStream.reduce (new ReduceFunction<Tuple2<String,Integer>>() {
public Tuple2<String, Integer> reduce(Tuple2<String, Integer> value1, Tuple2<String, Integer> value2) throws Exception {
return new Tuple2<String,Integer>(value1.f0, value1.f1 + value2.f1);
}
});
Union
DataStream* → DataStream
两个或多个数据流的合并,创建包含所有流中的所有元素的新流。注意:如果你将一个数据流与它本身合并,你将在结果流中获得每个元素两次。
dataStream.union(otherStream1, otherStream2, ...);
Window Join
DataStream,DataStream → DataStream
在给定键和公共窗口上连接两个数据流。
dataStream.join(otherStream)
.where(<key selector>).equalTo(<key selector>)
.window(TumblingEventTimeWindows.of(Time.seconds(3)))
.apply (new JoinFunction () {...});
Connect
DataStream,DataStream → ConnectedStream
连接两个保持其类型的数据流。连接允许在两个流之间共享状态。
DataStream<Integer> someStream = //...
DataStream<String> otherStream = //...
ConnectedStreams<Integer, String> connectedStreams = someStream.connect(otherStream);
CoMap, CoFlatMap
ConnectedStream → DataStream
类似于连接数据流上的map和flatMap
connectedStreams.map(new CoMapFunction<Integer, String, Boolean>() {
@Override
public Boolean map1(Integer value) {
return true;
}
@Override
public Boolean map2(String value) {
return false;
}
});
connectedStreams.flatMap(new CoFlatMapFunction<Integer, String, String>() {
@Override
public void flatMap1(Integer value, Collector<String> out) {
out.collect(value.toString());
}
@Override
public void flatMap2(String value, Collector<String> out) {
for (String word: value.split(" ")) {
out.collect(word);
}
}
});
Iterate
DataStream → IterativeStream → ConnectedStream
通过将一个操作符的输出重定向到之前的某个操作符,在流中创建一个反馈循环。这对于定义不断更新模型的算法尤其有用。下面的代码从一个流开始,并不断地应用迭代体。大于0的元素被发送回反馈通道,其余的元素被向下转发。
IterativeStream<Long> iteration = initialStream.iterate();
DataStream<Long> iterationBody = iteration.map (/*do something*/);
DataStream<Long> feedback = iterationBody.filter(new FilterFunction<Long>(){
@Override
public boolean filter(Long value) throws Exception {
return value > 0;
}
});
iteration.closeWith(feedback);
DataStream<Long> output = iterationBody.filter(new FilterFunction<Long>(){
@Override
public boolean filter(Long value) throws Exception {
return value <= 0;
}
});
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Long> someIntegers = env.generateSequence(0, 1000);
// 创建一个迭代流
IterativeStream<Long> iteration = someIntegers.iterate();
// 将迭代流iteration中的数据减1
DataStream<Long> minusOne = iteration.map((MapFunction<Long, Long>) value -> value - 1);
// 在减1过后的数据流中过滤出大于0的数据
DataStream<Long> stillGreaterThanZero = minusOne.filter((FilterFunction<Long>) value -> (value > 0));
// 将stillGreaterThanZero数据流再放入iteration迭代流的开始部分
iteration.closeWith(stillGreaterThanZero);
// 在减1过后的数据流中过滤出小于0的数据
DataStream<Long> lessThanZero = minusOne.filter((FilterFunction<Long>) value -> (value <= 0));
// 对小于0的数据流进行相应处理
Flink概念
在 Flink 中,应用程序由用户自定义算子转换而来的流式 dataflows 所组成。这些流式 dataflows 形成了有向图,以一个或多个源(source)开始,并以一个或多个汇(sink)结束。
- 源(source):
- 转换(transformation):
- 汇(sink):
一些名词的理解
- 任务(task):多个功能相同的子任务的集合;
- 子任务(subtask):对应的是Thread,子任务从算子角度看,就是算子链;
- Operator Chains(算子链):没有 shuffle 的多个算子合并在一个 subTask 中,就形成了 Operator Chains
- 操作/算子:每一个操作都可以说是一个算子(socketTextStream, flatMap, map, keyBy ...), 正常每个算子应该启动一个线程去运行,当sockTextStream, flatMap, map的并行度都是1的时候,可以将他们合成一个线程来处理, map(flatMap(socketTextStream))
备注:官方的解释是:将多个操作/算子链接在一起的功能称为链。Flink 称一个调度单位的一组或多个(链接在一起的)算子为一个 任务。通常,子任务 用来指代在多个 TaskManager 上并行运行的单个任务实例,但我们在这里只使用 任务(task)一词。
会导致shuffle的操作
keyBy
设置并行度的方式
- 配置文件设置,全局设置 parallelism.default: 1
- 在提交job的时候通过-p选项来设置
- 在代码中通过环境变量来设置 env.setParallelism(1);
- 直接在算子上设置 .setParallelism(1); 优先级由上到下依次提高,最下面的优先级最高
Kafka连接器
依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.11</artifactId>
<version>1.14.0</version>
</dependency>
从Kafka中读取数据
读取带key的数据,大部分情况不需要读取key
// Kafka的一些配置
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "CentOSA:9092,CentOSB:9092,CentOSC:9092");
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
properties.setProperty("group.id", "test2");
// 创建流并建立和Kafka的连接
// new FlinkKafkaConsumer<>(...) 参数解析
// 1. Topic 名称或者名称列表
// 2. 用于反序列化 Kafka 数据的 DeserializationSchema 或者 KafkaDeserializationSchema
// 3. Kafka 消费者的属性
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
SingleOutputStreamOperator<Tuple2<String, String>> stream = env
.addSource(new FlinkKafkaConsumer<>("topic", new KafkaDeserializationSchema<Tuple2<String, String>>() {
// 何时结束流
@Override
public boolean isEndOfStream(Tuple2<String, String> nextElement) {
return false;
}
// 要进行序列化的字节流,这里对其做了一些转换, 表明输出哪些数据到流中
@Override
public Tuple2<String, String> deserialize(ConsumerRecord<byte[], byte[]> record) throws Exception {
// Tuple2<String, String> tuple = new Tuple2<>(String.valueOf(record.key() == null ? 1 : 0), new String(record.value()));
// return tuple;
return null;
}
@Override
public TypeInformation<Tuple2<String, String>> getProducedType() {
return null;
}
}, properties)).returns(Types.TUPLE(Types.STRING, Types.STRING));
stream.print();
try {
env.execute("word count");
} catch (Exception e) {
e.printStackTrace();
}
只读取value的方式
new FlinkKafkaConsumer<>(...)中第二个参数使用 new SimpleStringSchema() 即可
DataStream<String> stream = env
.addSource(new FlinkKafkaConsumer<>("topic", new SimpleStringSchema(), properties));
自定义数据源
单并行度
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
/*
* 如果是基于SourceFunction接口实现自定义数据源,这种方式只支持单并行度,只有一个线程去发射数据
* 通过setParallelism强制设置为多并行度会报错
*/
SingleOutputStreamOperator<String> source = env.addSource(new SourceFunction<String>() {
boolean flag = true;
// 发射数据
@Override
public void run(SourceContext<String> ctx) throws Exception {
// 读取任何地方的数据,然后将数据发射到流中
Random random = new Random();
while (flag) {
int value = random.nextInt(10);
ctx.collect("source" + value);
if (1 == value) {
cancel();
}
}
}
// 停止
@Override
public void cancel() {
flag = false;
}
}).returns(Types.STRING);
source.print();
try {
env.execute("...");
} catch (Exception e) {
e.printStackTrace();
}
多并行度
实现 RichParallelSourceFunction 即可