flink简介

本文主要以三个角度介绍flink, 第一个是以flink的整体架构,第二个是以flink支持那些东西,第三个是flink能干什么,因为博主没有接触过flink的批处理,所以这一块省略

参考 https://www.itcodemonkey.com/article/8095.html

 

flink支持的功能

整体架构

flink组件结构

  1. flink可以以多种模式部署:如localstandaloneyarn
  2. Runtime层接受JobGraph(一种描述flinkjob结构的拓扑图),进行调度并最终将生成的task运行在Taskmanager
  3. DataStream API 根据代码的结构生成JobGraph
  4. Table Api SQL  经过优化器,编译成 DataStream API 

抽象层次

 

  1. 最低级的抽象层次仅仅提供有状态的流处理,通过DataStream  ProcessFunction 来实现,在open方法中可以通过调用
    getRuntimeContext().getxxxstate 处理状态相关,可以通过 调用ProcessFunction.Context#timerService 处理时间相关
  2. DataStream API 主要提供处理数据的通用模块,比如windowjoinagg,一些自带的sourcesink, 这些会自动帮你处理状态和时间相关的东西 
  3. Table API 和sql 都会转换成逻辑运算表达式树,已经优化后,根据表达式生成代码由Janino动态编译

程序与数据流

一个简单的flink程序如上图,它是由source,sink,transformation构成,transformation主要是把一个或多个输入流转换为一个或多个的输出流。对数据流的操作构成一个DAG,flink job的运行过程就是把代码转换为DAG提交到JM(jobmanager)上,然后有JM进行调度

并行数据流

  1. flink程序的是并行的和分布式的,每个operate可以指定parallelism  ,parallelism 可以简单理解为同一份操作运行在不同的线程中,而线程可以是分布在不同jvm或者机器(taskmanager中)
  2. 既然不同的operate可以指定不同的parallelism,那么必然会涉及到数据的分区问题,flink有两种分区模式forward和redistributing 
  3. forward模式:下游operate的subtask 的数据全部来自上游operate的subtask,及数据的分区规则没有变,数据的顺序依然是上一个operate的subtask的处理顺序,如上图source[1] - map[1]的过程
  4. redistributing模式:下游operate的subtask 的数据全部来自上游operate的不同subtask,如上图map[1] - keyby...[1]的过程,例如
  5. DataStream#broadcast 把当前operate的subtask数据传输到下游operate的所有subtask(下游每个subtask接收到一样的数据)
  6. DataStream#shuffle 每个record随机发送到下游的某个subtask
  7. DataStream#rebalance  下游的所有subtask按照顺序依此接受上游的某个subtask发来的消息
  8. DataStream#rescale 实现同DataStream#rebalance 只不过rescale的第一条记录是发送到下游的subtask[1],而rebalance第一条记录是发送到下游的subtask[n] n是随机的
  9. DataStream#global 上游所有subtask都发送到下游subtask[1]
  10. DataStream#keyBy  按照key的hash取模进行分区,可能会发生数据倾斜,他与上面的分区有一个地方不同,就是下游的的某些operate不是对每个分区的操作,而是对每个key的操作,就比如KeyedStream#reduce,是对key进行reduce,而不是整个分区

时间及watermark

  • Event Time 事件时间
  • Ingestion time 进入到flink source的时间
  • process time 进入到某个operate的时间
  • 就比如一条订单创建于8:30 ,发送到kafka,flinkjob 的kafkasource接收到这条消息的时间是8:40,中间经过一些处理,8:50进入到window operate里,那么Event Time 是8:30 ,Ingestion time是8:40 ,process time是8:50
  • 一半来说对于流式的应用,source 都是来自于mq,在某些场景下,发送到mq的消息里的时间无法保证严格递增,或者消费mq的消息里的时间也无法保证严格递增,又或者由于某些原因,消息有些延迟,由或者下游的subtask消费上游的的多个subtask,造成下游的subtask接收到的数据无法保证严格递增,基于这些原因event time和Ingestion time 才有了watermark这个概念,而process time 保证按照时间递增
  • watermark是代表之前的数据在逻辑上消费完了(实际可能没有消费完,但是不做处理)的时间戳,例如9:00的watermark到了,那么对于window end时间在9:00的窗口会触发,watermark会广播到下游的所有subtask,下游的subtask的watermark是由上游多个subtask watermark最小的那个决定
  • flink有很多种方式来生成watermark,如在source调用SourceFunction.SourceContext#collectWithTimestamp,DataStream#assignTimestampsAndWatermarks

 

状态和检查点

flink的operate都可以包含state,这个状态是用于checkpoint或者savapoint(做分布式快照,持久化state),前者用于异常恢复后者用于job迭代时可以从上次的state继续。状态分为两种,一种是operate state 一种是keyedstate

  • operate state:  主要有三种 ListState,UnionState,BroadcastState,下图很好的说明这三种状态,左边是并行度为3的情况,右边是并行度改为2的情况。我觉的没有必要对operate state进行本地备份,state实际上是存在内存中的(自己会维护List),只是在checkpoint的时候才会持久化,并没有性能的影响
public class MapFunc implements MapFunction<Integer, Integer>, CheckpointedFunction {
    /**
     * 因为没有实现序列化接口
     */
    transient ListState<Integer> listState;
    /**
     * 本地备份
     */
    List<Integer> local = new ArrayList<>();

    @Override
    public Integer map(Integer value) throws Exception {
        local.add(value);
        return value;
    }

    /**
     * 进行快照前调用
     * */
    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        listState.clear();
        listState.addAll(local);
    }

    /**
     * 初始化状态,可能是从上一次恢复
     */
    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        listState = context.getOperatorStateStore().getListState(new ListStateDescriptor("count", Integer.class));
        /*如果是从上一次状态恢复*/
        if (context.isRestored()) {
            listState.get().forEach(local::add);
        }

    }
}

  • KeyedState: 数据类型有很多,如下图所示,

/**
 * keyed state是半自动化的,也就是说在一个record来的时候会根据KeySelect 获取key,并设置当前key和namespace,然后调用processElement
 * 所以keyedState不用实现CheckpointedFunction
 *
 * */
public class MyFunc extends KeyedProcessFunction<String, String, String> {
    /**value state 实际的数据结构是 Map<N, Map<K, S>>[] state  N:namespace 一般以用于window,K:key  S:state 在本例中就是Integer*/
    transient ValueState<Integer> valueState;

    @Override
    public void open(Configuration parameters) throws Exception {
        valueState = getRuntimeContext().getState(new ValueStateDescriptor<>("state", Types.INT));
    }

    @Override
    public void processElement(String value, Context ctx, Collector<String> out) throws Exception {
        String key =  ctx.getCurrentKey();
        Integer count = valueState.value();
        valueState.update(count == null ? 1 : count++);
    }


}

每个subtask一个拥有一个KeyedStateBackend 内部结构是Map<String, StateTable<K, N, S>> key是statename,而 StateTable的结构是StateMap<K, N, S>[],数组是因为有KeyGroup这个概念,StateMap的结构是Map<N, Map<K, S>>。为什么有KeyGroup这个概念呢,keyBy只是保证同样的key在同一个分区,如果简单点像HashMap ,hash(key)%parallelism,那么如果parallelism增加,所有的key就需要重新分区,这样效率太低,所以如果我已一个常量 C(例如16)进行分区hash(key)%C 那么结果是0-15,如果有两个并行度,一个分0-7,一个分8-15,此时加一个并行度,0-5,5-10,11-15,就不需要进行重分区,而且这样更适合批量的读写。

flink有三种StateBackend

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值