Flink学习

326 阅读9分钟

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

添加任务

  1. 通过命令添加
    flink run -c com.msb.stream.WordCount StudyFlink-1.0-SNAPSHOT.jar
    
    • -c:指定主类
    • -d:独立运行、后台运行
    • -p:指定并行度
  2. 通过web界面添加: web页面提交任务,可以通过flink-conf.yaml中的web.submit.enable: false来禁用,默认开启
  3. 关闭任务,可以通过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)

搭建

  1. 修改配置文件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/…

  2. 单独启动一个JobManage,TaskManage

    # 会根据flink-conf.yaml找到jobmanage的地址,然后向jobmanage注册
    jobmanage.sh 
    taskmanage.sh
    

flink on yarn(分担JobManage的压力)

概念

此时flink从资源管理,任务管理变成仅需进行任务管理,资源管理则交给yarn。实际上向yarn上提交一个任务就相当于在yarn所管理的资源上启动一个flink集群。

优点:

  • 分担jobmanage压力
  • 降低维护成本

运行流程图

1636944240(1).jpg

两种提交任务的方式

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集群一同被关闭,释放了资源。

配置
  1. Flink on Yarn依赖Yarn集群和HDFS集群,启动Yarn、HDFS集群 start-all.sh

  2. 下载支持Hadoop插件并且拷贝到各个节点的安装包的lib目录下
    下载地址:repo.maven.apache.org/maven2/org/…

  3. 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
      
  4. 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

  1. 修改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>
    
  2. 修改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)结束。

  1. 源(source):
  2. 转换(transformation):
  3. 汇(sink):

一些名词的理解

  1. 任务(task):多个功能相同的子任务的集合;
  2. 子任务(subtask):对应的是Thread,子任务从算子角度看,就是算子链;
  3. Operator Chains(算子链):没有 shuffle 的多个算子合并在一个 subTask 中,就形成了 Operator Chains
  4. 操作/算子:每一个操作都可以说是一个算子(socketTextStream, flatMap, map, keyBy ...), 正常每个算子应该启动一个线程去运行,当sockTextStream, flatMap, map的并行度都是1的时候,可以将他们合成一个线程来处理, map(flatMap(socketTextStream))

备注:官方的解释是:将多个操作/算子链接在一起的功能称为链。Flink 称一个调度单位的一组或多个(链接在一起的)算子为一个 任务。通常,子任务 用来指代在多个 TaskManager 上并行运行的单个任务实例,但我们在这里只使用 任务(task)一词。 image.png

会导致shuffle的操作

keyBy

设置并行度的方式

  1. 配置文件设置,全局设置 parallelism.default: 1
  2. 在提交job的时候通过-p选项来设置
  3. 在代码中通过环境变量来设置 env.setParallelism(1);
  4. 直接在算子上设置 .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 即可

向Kafka中写入数据