flink原理与工作实践

1,482 阅读27分钟

Flink简介

流处理技术概览

大数据 处理计算模式

流计算与批计算对比

  • 数据时效性不同:流计算实时,低延迟,批计算非实时,高延迟。
  • 数据特征不同:流计算的数据一般是动态的,没有边界的,而批处理的数据一般是静态数据。
  • 应用场景不同:流计算应用在实时场景,时效性要求比较高的场景,如实时推荐,业务监控。批计算一般说批处理,应用在实时性要求不高,离线计算的场景下,数据分析,离线报表等。
  • 运行方式不同:流计算的任务是持续进行的,批计算的任务是一次性完成的。

流计算框架对比

Flink 应用场景

  • 实时监控,用户行为预警,对用户行为或者相关事件进行实时监测和分析

  • 实时报表,双十一十二等活动直播大屏,数据化运营

  • 流数据分析,实时计算相关指标反馈及时调整决策

Flink核心特征

  • 支持事件时间(Event Time),接入时间(Ingestion Time),处理时间(Processing Time)等时间概念。

  • 基于轻量级分布式快照(Snapshot)实现的容错。数据一致性保障,数据不会丢失也不会重复。

  • 支持有状态的计算。Support for very large state,querable state 支持,灵活的 state-backend(HDFS,内存,RocksDB)。

  • 支持高度灵活的窗口操作。

  • 带反压的连续流模型。

  • 基于JVM实现独立的内存管理,flink在JVM中实现了自己的内存管理。应用可以超出主内存的大小限制,并且承受更少的垃圾收集的开销。对象序列化二进制存储,类似于c对内存的管理。

Bytelingo推荐系统中的Flink实践

flink主要运用于近线层的搭建,实时更新用户的兴趣特征向量。

旧离线计算逻辑

红色任务:hive -> abase

白色任务:HSQL

绿色任务:Spark

黄色任务:mysql ->hive

蓝色任务:mq -> hive (RMQ数据源与hive之间的数据实时传输。需要注意hive的数据是实时写入的,但是需要等到hive分区生成后,数据才可见,本质还是小时或者天级别)

紫色任务:flink 任务

暂时无法在飞书文档外展示此内容

新在线计算逻辑

暂时无法在飞书文档外展示此内容

数据源

字段名类型含义
user_idSTRING用户id
eventSTRING事件
album_idBIGINT内容id
timeBIGINT时间
cntBIGINT数量

动态表

暂时无法在飞书文档外展示此内容

user_ideventalbum_idtimecnt
11show12311
11click12321
22show59731
33show12341
11click78651

经过和hive维表以及abase维表的join操作,得到动态表multi_score_feature。

当我们基于这个表想做关于 user_fearture的聚合计算

CREATE  VIEW user_interest_feature AS
SELECT  user_id,
        bytelingo_group_array_sum(multi_feature) AS user_feature
FROM    multi_score_feature
GROUP BY
        user_id;

动态表的变化

user_idalbum_idmulti_feature
111231,0,0,0,0
user_idalbum_idmulti_feature
111232,0,0,0,0
user_idalbum_idmulti_feature
111232,0,0,0,0
225970,1,0,0,0
user_idalbum_idmulti_feature
111232,0,0,0,0
225970,1,0,0,0
331231,0,0,0,0
user_idalbum_idmulti_feature
111232,0,0,0,0
225970,1,0,0,0
331231,0,0,0,0
117860,0,1,0,0

Dynamic Table -> Stream 转换:

Retract 流,包含两种类型的 message: add messages 和 retract。先retract再add来体现表的update操作。

暂时无法在飞书文档外展示此内容

UDF 实现

public class GroupArraySum extends AggregateFunction<Long[], List<Long>> {

    @Override
    public Long[] getValue(List<Long> accumulator) {
        if (accumulator == null || accumulator.size() == 0) {
            return null;
        }
        Long[] res = new Long[accumulator.size()];
        for (int i=0; i<accumulator.size(); i++) {
            res[i] = accumulator.get(i);
        }
        return res;
    }

    @Override
    public List<Long> createAccumulator() {
        return new ArrayList<>();
    }

    public void accumulate(List<Long> acc, Long[] array) {
        if (acc.size()==0) {
            for (int i=0; i<array.length; i++) {
                acc.add(array[i]);
            }
        } else {
            if (acc.size() < array.length) {
                int gap = array.length - acc.size();
                // 新增维度用0补位
                for (int i=0; i<gap; i++) {
                    acc.add((long)0);
                }
            }
            for (int i=0; i<acc.size(); i++) {
                Long temp = acc.get(i) + array[i];
                acc.set(i, temp);
            }
        }
    }

    public void retract(List<Long> acc, Long[] array) {
        for (int i=0; i<acc.size(); i++) {
            Long temp = acc.get(i) - array[i];
            acc.set(i, temp);
        }
    }

}

推荐架构重构相关连接

技术方案: 【技术方案】推荐特征检索优化重构

flink sql 任务: rec20_user_interest_feature_stream

bytelingo新建udf函数库: code.byted.org/bytelingo/f…

Flink部署与应用

Flink集群架构

  • JobManager:管理节点,每个集群至少一个,管理整个集群计算资源,Job管理与调度执行,以及Checkpoint协调。
  • TaskManager: 每个集群有多个TM,负责计算资源提供。
  • Client: 本地执行应用 main() 方法解析 JobGraph对象,并最终将 JobGraph提交到JobManager运行,同时监控Job执行的状态。

JobManager

  • Checkpoint Coordinator
  • JobGraph -> Execetion Graph
  • Task 部署与调度
  • RPC 通信 (Actor System)
  • Job接收 (Job Dispatch 分发task)
  • 集群资源管理 (ResourceManager) 基于不同的集群k8s或者Yarn,会有不同的RM实现
  • Task注册和管理

TaskManager

  • TaskExecution
  • Network Manager
  • Shuffle Environment 管理 (shuffle进行数据交互的组件)
  • Rpc 通信 (Actor system)
  • Heartbeat with JobManager and ResourceManager
  • Data Exchange
  • Memory Management 序列化和反序列化的一个操作
  • Register to RM
  • Offer Slots to JobManager

Client

  • Application's main() method 执行
  • JobGraph Generate
  • Execution Environment 管理
  • Job提交与运行
  • Dependency Jar Ship
  • RPC with JobManager
  • 集群部署 (Cluster Deploy)

JobGraph

  • 通过有向无环图(Dag)方式表达用户程序
  • 不同接口程序的抽象表达
  • 客户端和集群之间的Job描述载体
  • 节点 Vertices,Result 参数
  • Flink 1.11 之前只能在Client中生成

stream graph -> job graph

stream graph没有并发这样的描述,

Flink集群部署模式

根据以下两种条件将集群部署模式分为3种类型:

  1. 集群的生命周期和资源隔离
  2. 根据程序main()方法执行在client还是jobManager
  • Session Mode

    • 共享JobManager和TaskManager,所有提交的Job都在一个Runtime中运行
  • Per-Job Mode

    • 独享JobManager与TaskManager,好比为每个Job单独启动一个Runtime
  • Application Mode

    • Application的main()运行在Cluster上,而不在客户端
    • 每个Application对应一个Runtime,Application中可以含有多个Job

Session 集群运行模式

  • Session集群运行模式:

    • JobManager与TaskManager共享
    • 客户端通过RPC或Rest API 连接集群的管理节点
    • Deployer 需要上传依赖的 Dependences jar
    • Deployer 需要生成 JobGraph,并提交到管理节点
    • JobManager 的生命周期不受提交的Job影响,会长期运行
  • Session 运行模式优点:

    • 资源充分共享,提升资源利用率
    • Job在 Flink Session集群中管理,运维简单
  • Session运行模式缺点:

    • 资源隔离相对较差
    • 非Native类型部署(即TM启动时task数目已经固定),TM不易拓展,Slot计算资源伸缩性较差

Per-Job 运行模式

  • Per-Job 类型集群

    • 单个Job独享JobManager与TaskManager
    • TM中Slot资源根据Job指定
    • Deployer需要上传依赖的Dependences Jar
    • 客户端生成 JobGraph,并提交到管理节点
    • JobManager的生命周期和Job生命周期棒绑定
  • Per-Job部署模式优势

    • Job和Job之间资源隔离充分
    • 资源根据Job需要进行申请,TM Slots数量可以不同
  • Per-Job 部署模式劣势

    • 资源相对比较浪费,JobManager需要消耗资源
    • Job管理完全交给ClusterManagement,管理复杂

Session 集群和Per-Job 类型集群问题

Application Mode 集群运行模式

  • Application Mode 类型集群 (1.11版本)

    • 每个Application对应一个JobManager,且可以运行多个Job
    • 客户端无需将Dependencies上传到JobManager,仅负责管理Job的提交与管理
    • main()方法运行JobManager中,将JobGraph的生成放在集群上运行,客户端压力降低
  • Application Mode 优点:

    • 有效降低带宽消耗和客户端负载
    • Application实现资源隔离,Application中实现资源共享
  • Application Mode 缺点:

    • 功能太新,还未经过生产验证
    • 仅支持Yarn和Kubunetes

集群资源管理器支持

Flink状态管理与容错

有状态计算概念

传统的数据处理框架比如storm是无状态的,会将计算层与存储层进行分离,中间结果会存储到外部的存储层。flink会把中间产生的结果存储到本地的内存或文件系统,不需要每次都通过跨网络的方式对数据进行更新。

传统的 流处理 框架

问题:每次有数据进入,都会从外部的存储比如redis进行数据的获取和更新,然后emit出去,这个时间延迟会非常高,频繁的网络io。一旦出现redis集群不稳定,就会影响map算子的性能和效率,进而导致整个任务阻塞。

有状态 流处理 架构

每个算子会维系一个状态的存储空间在内存里面,update和emit等所有的操作并没有跨网络传输。但是这样就会出现一些问题,一旦算子task线程挂了存储的中间状态的数据一致性无法保证,如果系统宕机中间状态数据就会丢失,版本升级代码逻辑发生改变,无法处理状态的变化,以及内存大小有限,无法存储过多数据等。

flink会通过异步的snapshot的操作,也就是checkpoint机制,来恢复数据的中间状态,提高了容错的能力。

有几个问题需要注意,也是重点关注的内容:

  • 端到端的数据一致性如何实现?
  • 状态数据如何恢复?
  • 状态如何存储获取以及更新?
  • 状态后端存储如何选择?
  • snapshot操作如何实现?

带着这些问题,继续往下。

状态类型及应用

状态应用场景

  • 去重计算
  • 窗口计算
  • 机器学习深度学习
  • 历史数据获取

原生状态&托管状态

Keyed State & Operator State

Keyed StateOperator State
使用算子类型只能被用于 KeyedStream 中的 Operator 上可以被用于所有的 Operator (例如 FlinkKafkaConsumer 中)
状态分配每个 Key 对应一个状态,单个 Operator 中可能涵盖多个 Keys单个 Operator 对应一个 State
创建和访问方式重写 RichFunction,通过访问 RuntimeContext 对象获取实现 CheckpointedFunction 或 ListCheckpointed 接口
横向拓展状态随着 Key 自动在多个算子 Task 上迁移有多种状态重新分配的方式:- 均匀分配
  • 将所有状态合并,再分发给每个实例上。 | | 支持数据类型 | ValueState,ListState, ReducingState,AggregatingState, MapState | ListStateUnionListStateBroadcastState |

Keyed State

使用实例

Operator State

Operator State 定义

  • 单 Operator 具有一个状态,不区分 Key
  • State 需要支持重新分布
  • 不常用,主要用于 Source 和 Sink 节点,像 KafkaConsumer 中,维护 Offset,Topic 等信息;和业务处理关系不大,更多的是一种系统层面的信息维护。
  • 实例:BufferSink

三种状态类型:

  • ListState
  • UnionListState
  • BroadcastState

两种定义方式:

  • 实现 CheckpointedFunction 接口定义
  • 实现 ListCheckpointed 接口定义 (Deprecated)

Operator State resizing modes

使用实例

class BufferingSinkFunction implements SinkFunction<Tuple2<String, Integer>>, CheckpointedFunction {

    private final int threshold;

    private transient ListState<Tuple2<String, Integer>> checkpointedState;

    private List<Tuple2<String, Integer>> bufferedElements;

    public BufferingSinkFunction(int threshold) {
        this.threshold = threshold; // 达到阈值sink数据
        this.bufferedElements = new ArrayList<>();
    }

    @Override
    public void invoke(Tuple2<String, Integer> value, Context contex) throws Exception {
        bufferedElements.add(value);
        if (bufferedElements.size() == threshold) {
            for (Tuple2<String, Integer> element : bufferedElements) {
                // send it to the sink
            }
            bufferedElements.clear();
        }
    }

    @Override
    public void snapshotState(FunctionSnapshotContext context) throws Exception {
        checkpointedState.clear();
        for (Tuple2<String, Integer> element : bufferedElements) {
            checkpointedState.add(element);
        }
    }

    @Override
    public void initializeState(FunctionInitializationContext context) throws Exception {
        ListStateDescriptor<Tuple2<String, Integer>> descriptor =
                new ListStateDescriptor<>(
                        "buffered-elements",
                        TypeInformation.of(new TypeHint<Tuple2<String, Integer>>() {
                        }));

        checkpointedState = context.getOperatorStateStore().getListState(descriptor);

        if (context.isRestored()) {
            for (Tuple2<String, Integer> element : checkpointedState.get()) {
                // 从checkpoint中恢复数据到内存
                bufferedElements.add(element);
            }
        }
    }
}

Broadcast State(跳过)

zhuanlan.zhihu.com/p/105600352

Broadcast State 使得 Flink 用户能够以容错、一致、可扩缩容地将来自广播的低吞吐的事件流数据存储下来,被广播到某个 operator 的所有并发实例中,然后与另一条流数据连接进行计算。

广播状态与其他 operator state 之间有三个主要区别:

  1. Map 格式类型
  2. 需要有一条广播的输入流
  3. operator 可以有多个不同名称的广播状态

Broadcast 应用场景

  • 动态规则:动态规则是一条事件流,要求吞吐量不能太高。例如,当一个报警规则时触发报警信息等,将规 则广播到算子的所有并发实例中;
  • 数据丰富:例如,将用户的详细信息作业广播状态进行广播,对包含用户 ID 的交易数据流进行数据丰富;

Dynamic Pattern Evaluation with Broadcast State

Broadcast State 实现

// 用户行为流
DataStream<Action> actions = env.addSource(new KafkaConsumer<>()); 
// pattern流
DataStream<Pattern> patterns = env.addSource(new KafkaConsumer<>()); 

KeyedStream<Action, Long> actionsByUser = 
actions.keyBy((KeySelector<Action, Long>) action -> action.userId); 

MapStateDescriptor<Void, Pattern> bcStateDescriptor =
new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(Pattern.class)); 

// pattern流转化为broadcast stream
BroadcastStream<Pattern> bcedPatterns = patterns.broadcast(bcStateDescriptor); 

DataStream<Tuple2<Long, Pattern>> matches = 
actionsByUser
.connect(bcedPatterns)
.process(new PatternEvaluator());

购物车实例

public class Action {

    public Long userId;
    public String action;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }
}

public class Pattern {

    public String firstAction;
    public String secondAction;

    public String getFirstAction() {
        return firstAction;
    }

    public void setFirstAction(String firstAction) {
        this.firstAction = firstAction;
    }

    public String getSecondAction() {
        return secondAction;
    }

    public void setSecondAction(String secondAction) {
        this.secondAction = secondAction;
    }
}

public class CartDetectPatternEvaluatorExample {

    public static void main(String[] args) throws Exception {

        final ParameterTool parameterTool = ParameterTool.fromArgs(args);
        StreamExecutionEnvironment env = KafkaExampleUtil.prepareExecutionEnv(parameterTool);
        // 行为流
        DataStream<Action> actions = env
                .addSource(
                        new FlinkKafkaConsumer010<>(
                                parameterTool.getRequired("action-topic"),
                                new ActionEventSchema(),
                                parameterTool.getProperties()));
        // pattern流
        DataStream<Pattern> patterns = env
                .addSource(
                        new FlinkKafkaConsumer010<>(
                                parameterTool.getRequired("pattern-topic"),
                                new PatternEventSchema(),
                                parameterTool.getProperties()));

        KeyedStream<Action, Long> actionsByUser = actions
                .keyBy((KeySelector<Action, Long>) action -> action.userId);

        MapStateDescriptor<Void, Pattern> bcStateDescriptor =
                new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(Pattern.class));
        // pattern流转化为broadcast stream
        BroadcastStream<Pattern> bcedPatterns = patterns.broadcast(bcStateDescriptor);

        DataStream<Tuple2<Long, Pattern>> matches = actionsByUser
                .connect(bcedPatterns)
                .process(new PatternEvaluator());

        matches.print();

        env.execute("CartDetectPatternEvaluatorExample");

    }

    public static class PatternEvaluator
            extends KeyedBroadcastProcessFunction<Long, Action, Pattern, Tuple2<Long, Pattern>> {

        // handle for keyed state (per user) 当前事件前一个事件的行为
        ValueState<String> prevActionState;
        // broadcast state descriptor 和上面定义的map state格式是一样的
        MapStateDescriptor<Void, Pattern> patternDesc;

        @Override
        public void open(Configuration conf) {
            // initialize keyed state 初始化操作
            prevActionState = getRuntimeContext().getState(
                    new ValueStateDescriptor<>("lastAction", Types.STRING));
            patternDesc =
                    new MapStateDescriptor<>("patterns", Types.VOID, Types.POJO(Pattern.class));
        }

        /**
         * Called for each user action.
         * Evaluates the current pattern against the previous and
         * current action of the user.
         */
        @Override
        public void processElement(
                Action action,
                ReadOnlyContext ctx,
                Collector<Tuple2<Long, Pattern>> out) throws Exception {
            // get current pattern from broadcast state
            Pattern pattern = ctx
                    .getBroadcastState(this.patternDesc)
                    // access MapState with null as VOID default value
                    .get(null);
            // get previous action of current user from keyed state
            String prevAction = prevActionState.value();
            if (pattern != null && prevAction != null) {
                // user had an action before, check if pattern matches
                if (pattern.firstAction.equals(prevAction) &&
                        pattern.secondAction.equals(action.action)) {
                    // MATCH
                    out.collect(new Tuple2<>(ctx.getCurrentKey(), pattern));
                }
            }
            // update keyed state and remember action for next pattern evaluation
            prevActionState.update(action.action);
        }

        /**
         * Called for each new pattern.
         * Overwrites the current pattern with the new pattern.
         */
        @Override
        public void processBroadcastElement(
                Pattern pattern,
                Context ctx,
                Collector<Tuple2<Long, Pattern>> out) throws Exception {
            // store the new pattern by updating the broadcast state
            BroadcastState<Void, Pattern> bcState = ctx.getBroadcastState(patternDesc);
            // storing in MapState with null as VOID default value
            // 把pattern存储到 bcstate里
            bcState.put(null, pattern);
        }
    }
}

Broadcast State 使用注意事项

  • 同一个 operator 的各个 task 之间没有通信,广播流侧(processBroadcastElement)可以能修改 broadcast state,而数据流侧(processElement)只能读 broadcast state.;
  • 需要保证所有 Operator task 对 broadcast state 的修改逻辑是相同的,否则会导致非预期 的结果;
  • Operator tasks 之间收到的广播流元素的顺序可能不同:虽然所有元素最终都会下发给下游 tasks,但是元素到达的顺序可能不同,所以更新state时不能依赖元素到达的顺序;
  • 每个 task 对各自的 Broadcast state 都会做快照,防止热点问题;
  • 目前不支持 RocksDB 保存 Broadcast state:Broadcast state 目前只保存在内存中,需要 为其预留合适的内存;

Checkpoint实现原理

www.zhihu.com/search?type…

exactly once

实现 exactly once 的分布式快照/状态检查点方法受到 Chandy-Lamport 分布式快照算法的启发。通过这种机制,流应用程序中每个算子的所有状态都会定期做 checkpoint。如果是在系统中的任何地方发生失败,每个算子的所有状态都回滚到最新的全局一致 checkpoint 点。在回滚期间,将暂停所有处理。源也会重置为与最近 checkpoint 相对应的正确偏移量。整个流应用程序基本上是回到最近一次的一致状态,然后程序可以从该状态重新启动。

juejin.cn/post/723134…

State fault tolerance

Checkpoint 过程将算子中的状态数据异步持久化到文件系统中。flink使用异步屏障快照算法,实现了全局状态快照的一致。

Restore State

每个 Operator 都会分配相应的 File Handles,然后通过文件恢复状态数据。

Checkpoint 执行过程

所有checkpoint完结后,chechpoint coordinator会把元数据持久化到外围的数据库里面。

总体过程

Checkpoint Barrier

zhuanlan.zhihu.com/p/161801025

barrier是一种特殊的事件。Flink 通过在 DAG 数据源定时向数据流注入名为 Barrier 的特殊元素,将连续的数据流切分为多个有限序列,对应多个 Checkpoint 周期。

Checkpoint Barrier Align

在有多个输入 Channel 的情况下,为了数据准确性,算子会等待所有流的 Barrier 都到达之后才会开始本地的快照,这种机制被称为 Barrier 对齐。对齐后触发operator的checkpoint写入。

在对齐的过程中,算子只会继续处理的来自未出现 Barrier Channel 的数据,而其余 Channel 的数据会被写入输入队列,直至在队列满后被阻塞。当所有 Barrier 到达后,算子进行本地快照,输出 Barrier 到下游并恢复正常处理。

  • 图 a: 输入 Channel 1 存在 3 个元素,其中 2 在 Barrier 前面;Channel 2 存在 4 个元素,其中 2、9、7在 Barrier 前面。
  • 图 b: 算子分别读取 Channel 一个元素,输出 2。随后接收到 Channel 1 的 Barrier,停止处理 Channel 1 后续的数据,只处理 Channel 2 的数据。
  • 图 c: 算子再消费 2 个自 Channel 2 的元素,接收到 Barrier,开始本地快照并输出 Barrier。

但是这样在operator里面会有一个block从而系统性能下降,就是先到的数据流需要等待还未到的数据流,对齐之后再发之后的数据流给下游。一旦checkpoint过大,就会出现比较大的堵塞。所以从1.11版本就引入了 unaligned checkpointing。

Unaligned Checkpointing

blog.csdn.net/bigdatakena…

Unaligned Checkpoint 允许算子优先摄入并优先输出 Barrier。如此一来,第一个到达 Barrier 会在算子的缓存数据队列(包括输入 Channel 和输出 Channel)中往前跳跃一段距离,而被”插队”的数据和其他输入 Channel 在其 Barrier 之前的数据会被写入快照中(图中黄色部分)。

这样的主要好处是,如果本身算子的处理就是瓶颈,Unaligned Checkpoint 则可以在 Barrier 进入输入 Channel 就马上开始快照。这可以从很大程度上加快 Barrier 流经整个 DAG 的速度,从而降低 Checkpoint 整体时长。

  • 图 a: 输入 Channel 1 存在 3 个元素,其中 2 在 Barrier 前面;Channel 2 存在 4 个元素,其中 2、9、7 在 Barrier 前面。输出 Channel 已存在结果数据 1。
  • 图 b: 算子优先处理输入 Channel 1 的 Barrier,开始本地快照记录自己的状态,并将 Barrier 插到输出 Channel 末端。
  • 图 c: 算子继续正常处理两个 Channel 的输入,输出 2、9。同时算子会将 Barrier 越过的数据(即输入 Channel 1 的 2 和输出 Channel 的 1)写入 Checkpoint,并将输入 Channel 2 后续早于 Barrier 的数据(即 2、9、7)持续写入 Checkpoint。

比起 Aligned Checkpoint 中不同 Checkpoint 周期的数据以算子快照为界限分隔得很清晰,Unaligned Checkpoint 进行快照和输出 Barrier 时,部分本属于当前 Checkpoint 的输入数据还未计算(因此未反映到当前算子状态中),而部分属于当前 Checkpoint 的输出数据却落到 Barrier 之后(因此未反映到下游算子的状态中)。这也正是 Unaligned 的含义: 不同 Checkpoint 周期的数据没有对齐,包括不同输入 Channel 之间的不对齐,以及输入和输出间的不对齐。而这部分不对齐的数据会被快照记录下来,以在恢复状态时重放。换句话说,从 Checkpoint 恢复时,不对齐的数据并不能由 Source 端重放的数据计算得出,同时也没有反映到算子状态中,但因为它们会被 Checkpoint 恢复到对应 Channel 中,所以依然能提供只计算一次的准确结果。

当然,Unaligned Checkpoint 并不是百分百优于 Aligned Checkpoint,它会带来的已知问题就有:由于要持久化缓存数据,State Size 会有比较大的增长,磁盘负载会加重。 随着 State Size 增长,作业恢复时间可能增长,运维管理难度增加。 目前看来,Unaligned Checkpoint 更适合容易产生高反压同时又比较重要的复杂作业。对于像数据 ETL 同步等简单作业,更轻量级的 Aligned Checkpoint 显然是更好的选择。

Flink 1.11 的 Unaligned Checkpoint 主要解决在高反压情况下作业难以完成 Checkpoint 的问题,同时它以磁盘资源为代价,避免了 Checkpoint 可能带来的阻塞,有利于提升 Flink 的资源利用率。随着流计算的普及,未来的 Flink 应用大概会越来越复杂,在未来经过实战打磨完善后 Unaligned Checkpoint 很有可能会取代 Aligned Checkpoint 成为 Flink 的默认 Checkpoint 策略。

Savepoint

save point 是一种特殊类型的checkpiont,是由用户这边主动去控制的。比如我们需要停机运维,或者更改代码,我们需要对系统进行人工干预和停止。save point可以让人工控制作业的起停。

check pointsave point
触发管理方式Flink Runtime 自动管理和触发用户手动触发
主要功能- Task 发生异常时恢复状态数据
  • Application 异常恢复
  • 数据一致性保证 | - 有计划备份状态数据,使作业停止后能够再次 恢复原有计算状态
  • 作业升级,备份
  • 修改代码,调整并发等 | | 特点 | - 轻量,支持增量更新
  • 作业停止后默认清除 | - 持久化存储
  • 标准化格式存储,允许代码逻辑及配置发生改 变,作业可以人为地从 Savepoint 中恢复 |

StateBackends 状态管理器(重点)

主要类别

JVM Heap state backend

RocksDB State backend

对比

MemoryStateBackendFsStateBackendRocksDBStateBackend
构造方法env.setStateBackend(new MemoryStateBackend( "file://" + baseCheckpointPath, null).configure(conf, classLoader))默认后端状态管理器env.setStateBackend( new FsStateBackend(tmpPath))env.setStateBackend(new RocksDBStateBackend("file://" + baseCheckpointPath).configure(conf, classLoader))
数据存储- State 数据存储在 TaskManager 内存中
  • Checkpoint 数据数据存储在 JobManager 内存 | - State:TaskManager 内存
  • Checkpoint:外部文件系统(本地或 HDFS) | - State:TaskManager 中的 KV 数据库(实际使用内存+磁盘)
  • Checkpoint:外部文件系统(本地或 HDFS) | | 容量限制 | - 单个 State maxStateSize 默认为5M
  • maxStateSize <= akka.framesize 默认10M
  • 总大小不能超过 JobMananger 的内存 | - 单个 TaskManager上State 总量不能超过TM内存
  • 总数据大小不超过文件系统容量 | - 单 TaskManager 上 State 总量不超过其内存 + 磁盘大小
  • 单 Key 最大容量2G
  • 总大小不超过配置的文件系统容量 | | 推荐场景 | - 本地测试
  • 状态比较少的作业 | - 常规状态作业
  • 窗口时间比较长,如分钟级别窗口聚合,Join 等
  • 需要开启 HA 的作业 | - 超大状态作业
  • 需要开启 HA 的作业
  • 对状态读写性能要求不高的作业 | | 生产环境可用性 | 不推荐生产环境中使用 | 可在生产环境中使用 | 可在生产环境中使用 |

State Schema Evolution

应用升级带来的问题

Code Update

  • 业务计算逻辑发生变化
  • State 发生改变,无法直接从 Old State 中恢复作业

Pipeline topology 发生改变

  • 增加和删除 Operators

Job 重新配置

  • Rescale job/ Operator Parallelism
  • Swapping state backend

状态数据结构更新

状态数据类型支持

POJO types

  • 删除字段

    • 字段删除后,删除字段对应的原有值将在新的检查点和保存点中删除
  • 添加新字段

    • 新字段将初始化为其类型的默认值,如 Java 所定义的那样
  • 定义的字段其类型不能更改

  • POJO 类型的 Class Name 不能更改,包括类的命名空间

Avro types

  • Flink 完全支持 Avro 类型状态的模式演进,只要 Avro 的模式解析规则认为模式更改是兼容的
  • 不支持键的模式演化
  • Kryo 不能用于模式演化

非內建类型状态数据结构更新

需要通过custom state serializers实现。

State 序列化与反序列化

State serialization in heap-based backends

local state backends 堆内内存。

在 local state backends 和 persisted state backends之间进行序列化和反序列化。

State serialization in off-heap backends

local state backends 堆外内存。

序列化的形式储存在local state backends中。触发save point的时候,只是 file transfer。

使用的过程中才会进行反序列化,如果某个key对应的状态没有使用到,那么并不会进行v2序列化的更新。

Querable State 介绍与使用

外围可以通过rpc的方式获取operator的state。

Flink Runtime 设计与实现

Flink Runtime 整体架构

整体架构包含了三个部分:

  • JobManager(Master)
  • TaskManager(Worker)
  • Client

集群的执行流程:

Runtime 核心组件

Dispatcher

  • 集群 Job 的调度分发
  • 根据 JobGraph 启动 JobManager(JobMaster)

ResourceManager

  • 集群层面资源管理
  • 适配不同的资源管理,eg Yarn,Kubernetes 等
  • 核心组件:SlotManager

TaskManager

  • Slot 计算资源提供者

JobManager

  • 负责管理一个具体的 Job

  • Scheduler

    • 调度和执行该 Job 的所有 Task
    • 发出 Slots 资源请求

集群运行模式

Session 模式

  • Runtime 集群组件共享
  • 资源复用
  • Runtime 中有多个 JobManager

Per-Job 模式

  • Runtime 集群组件仅为单个 Job 服务
  • 资源相对独立
  • 不支持提交 JobGraph

Flink Client 实现原理

Session 集群创建流程

Application Code 运行

ResourceManager 资源管理

双层资源调度

  • Cluster->Job

    • SlotManager

集群会通过slot manager 给job进行资源的分配。

拿到资源后,然后就是把 job 里面的 task 调度起来。task是更细粒度的线程或者计算节点,会在slot里面完成作业的运行。

  • Job->Task

    • Scheduler调度器,job manager 会用调度器把slot pool 里面的slot资源调度给task
    • 单个Slot可以用于一个或多个Task执行
    • 但相同的Task不能在一个Slot中运行

Slot 计算 资源管理

Slot 资源组成

  • TM有固定数量的 Slot 资源
  • Slot 数量由配置决定
  • Slot 资源由 TM 资源及 Slot 数量决定
  • 同一 TM 上的 Slot 之间无差别

TaskManager 资源管理

资源类型

  • 内存

  • CPU

  • 其他拓展资源

    • GPU(待支持)

蓝色:堆内内存

黄色:堆外内存

TaskManager 管理

  • Standalone 部署模式

  • ActiveResourceManager 部署模式

    • Kubernetes,Yarn,Mesos
    • Slot 数量按需分配,根据 Slot Request 请求数量启动 TaskManager
    • TaskManager 空闲一段时间后,超时释放
    • On-Yarn 部署模式不再支持固定数量的 TaskManager

Job 资源调度

  1. taskmanager register phase
  2. slot allocate phase
  3. task submit phase

Task 调度执行

  • SlotRequest:

    • Task + Slot -> Allocate TaskSlot 调度器里面把task和slot绑定
  • Slot Sharing:

    • Slot Sharing Group 任务共享 Slot计算资源
    • 单个 Slots 中相同任务只能有一个

Dispatcher 任务分发器

  • 集群重要组件之一,主要负责 JobGraph 的接收
  • 根据 JobGraph 启动 JobManager (使用JobManagerRunnerFactory创建JobManager)
  • RpcEndpoint 服务,客户端和Web UI 都可以调用rpc接口
  • 通过 DispatcherGateway Rpc 对外 提供服务
  • 从 ZK 中恢复 JobGraph (HA high available 模式)
  • 保存整个集群的 Job 运行状态

job graph的信息需要持久化到zk高可用介质里面,recoveredJobs从zk里面恢复job graph。

会和RM进行交互,为JobManager提供启动资源。

Dispatcher 组件核心成员

Dispatcher 启动流程

Dispatcher 接收任务

JobGraph 提交与运行 (跳过)

Flink Graph 转换位置

cluster client 和 runtime这边进行对应的网络交互。

Flink 四种 Graph 转换

第⼀层: Program -> StreamGraph

第⼆层:StreamGraph -> JobGraph

第三层:JobGraph -> ExecutionGraph

第四层:Execution -> 物理执⾏计划,最终物理执行图会和task manager 进行交互执行任务

Program -> StreamGraph

StreamGraph 组成

StreamGraph -> JobGraph

把能合并在一起的节点合并在一起,生成一个节点。会进行一个重构和优化,并额外添加作业执行的信息。

JobGraph 组成

JobGraph->ExecutionGraph

并行度。

ExecutionGraph 组成

Task 调度和执行(跳过)

ExecutionGraph-> 物理执⾏计划

ExecutionGraph 调度器

ExecutionGraph 调度器分类

  • DefaultScheduler

    • 默认调度器
    • 外部调度 ExecutionGraph
  • LegacyScheduler

    • 基于 ExecutionGraph 内部调度

统一实现了 SchedulerNG的接口。

Task 调度策略

Eager:所有的资源一次性启动。

Lazy From Resource:block 形式,下游等待上游处理完毕后,才去启动对应的资源。

Task 设计和实现

StreamTask 触发与执行

AbstractInvokable 抽象类。

Task 运行状态

Task 重启策略与容错(跳过)

Task Failover 情况

  • 单个 Task 执行失败
  • TaskManager 出错退出
  • 支持多种恢复策略

Task 容错恢复

  • Task Restart 策略:

    • Fixed Delay Restart Strategy 如果任务失败,固定时延后重启
    • Failure Rate Restart Strategy 根据容错率进行重启
    • No Restart Strategy 如果出现错误,不重启
  • Failover Strategies:

    • Restart all
    •   一个task失败,重启所有task。HA 模式下从 Checkpoint 中恢复 状态数据。
    • Restart pipelined region
    •   一个task失败,仅仅重启相关联task。
      • Blocking 数据落盘,可直接读取。

      • 仅重启 Pipeline 关联的 Task

      • 两种错误类型:

        • 作业自身执行失败
        • 作业读取上游数据失败

Pipelined Region Failover Strategy

需要重启的 Region 的判断逻辑如下:

  • 出错 Task 所在 Region 需要重启。
  • 如果要重启的 Region 需要消费的数据有部分无法访问(丢失或损坏),产出该部分数据的 Region 也需要重启。
  • 需要重启的 Region 的下游 Region 也需要重启。这是出于保障数据一致性的考虑,因 为一些非确定性的计算或者分发会导致同一个 Result Partition 每次产生时包含的数据都不相同。

(重点)Flink 内存管理

JVM 内存管理带来的问题

  • Java 对象存储密度低: 一个只包含 boolean 属性的对象占用了16个字节内存:对象头占了8个, boolean 属性占了1个,对齐填充占了7个。而实际上只需要一个 bit(1/8字节)就够了。
  • Full GC 会极大地影响性能 ,尤其是为了处理更大数据而开了很大内存空间的 JVM 来说,GC 会达到秒 级甚至分钟级。
  • OOM 问题影响稳定性 :OutOfMemoryError 是分布式计算框架经常会遇到的问题,当 JVM 中所有对象大小超过分配给 JVM 的内存大小时,就会发生 OutOfMemoryError 错误,导致 JVM 崩溃,分布式 框架的健壮性和性能都会受到影响。

Flink有一套自己的内存管理,从而脱离了jvm内存管理带来的问题。

Flink 内存模型

对于容器部署来说,更注重进程内存。

TypeInformation 支持

提供数据序列化和反序列化的能力。

对象 序列化 实例

MemorySegment 内存块

  • 节约内存空间: 启动超大内存(上百GB)的JVM需要很长时间,GC停留时间也会很长(分钟级)。使用堆外内存的话,可以极大地减小堆内存(只需要分配Remaining Heap那一块),使得 TaskManager 扩展到上百GB内存不是问题。
  • 高效的 IO 操作: 堆外内存在写磁盘或网络传输时是 zero-copy,而堆内存的话,至少需要 copy 一次。
  • 故障恢复:堆外内存是进程间共享的。也就是说,即使JVM进程崩溃也不会丢失数据。这可以用来做故障恢复(Flink暂时没有利用起这个,不过未来很可能会去做)。
  • Flink用通过ByteBuffer.allocateDirect(numBytes)来申请堆外内存(hybridMemorySegment),用 sun.misc.Unsafe 来操作堆外内存 (heapMemorySegment)。

Flink SQL 监控与性能优化

反压监控与原理

TCP 自带反压的局限性

subtask 4 buffer pool 里面的空间耗尽,出现堵塞。

flink 5.0 之前是基于tcp自带的天然反压的能力,tcp管道可以感知堵塞。

但是这样的问题是,tcp是多路复用的,一旦有一个算子出现堵塞(比如subtask 4),其他算子也无法继续接受数据了。

而且这种反压机制是被动的,不能提前预知,不灵活。

基于 Credit 反压机制

credit是下游的节点向上游节点发送的指标,也叫信用值。credit值越大,说明下游处理数据的能力越强。上游会根据credit值调整发送。

  1. 在接收端,每个 Channel 在初始阶段会被分配固定数量的 Exclusive Buffer,这些 Buffer 会被用于存储接受到的数据,交给 Operator 使用后再次被释放。Channel 接收端空闲的 Buffer 数量称为 Credit,Credit 会被定时同步给发送端被后者用于决定发送多少个 Buffer 的数据。在流量较大时,Channel 的 Exclusive Buffer 可能会被写满,此时 Flink 会向 Buffer Pool 申请剩余的 Floating Buffer。这些 Floating Buffer 属于备用 Buffer,哪个 Channel 需要就去哪里。

  2. 在发送端,一个 Subtask 所有的 Channel 会共享同一个 Buffer Pool,这边就没有区分 Exclusive Buffer 和 Floating Buffer。发送端也会同步backlog的信息给下游。所以信息的同步是双向的。

反压采样

分析具体原因及处理

zhuanlan.zhihu.com/p/92743373

在实践中,很多情况下的反压是由于数据倾斜造成的,这点我们可以通过 Web UI 各个 SubTask 的 Records Sent 和 Record Received 来确认,另外 Checkpoint detail 里不同 SubTask 的 State size 也是一个分析数据倾斜的有用指标。

此外,最常见的问题可能是用户代码的执行效率问题(频繁被阻塞或者性能问题)。最有用的办法就是对 TaskManager 进行 CPU profile,从中我们可以分析到 Task Thread 是否跑满一个 CPU 核:如果是的话要分析 CPU 主要花费在哪些函数里面,比如我们生产环境中就偶尔遇到卡在 Regex 的用户函数(ReDoS);如果不是的话要看 Task Thread 阻塞在哪里,可能是用户函数本身有些同步的调用,可能是 checkpoint 或者 GC 等系统活动导致的暂时系统暂停。

当然,性能分析的结果也可能是正常的,只是作业申请的资源不足而导致了反压,这就通常要求拓展并行度。值得一提的,在未来的版本 Flink 将会直接在 WebUI 提供 JVM 的 CPU 火焰图,这将大大简化性能瓶颈的分析。

另外 TaskManager 的内存以及 GC 问题也可能会导致反压,包括 TaskManager JVM 各区内存不合理导致的频繁 Full GC 甚至失联。推荐可以通过给 TaskManager 启用 G1 垃圾回收器来优化 GC,并加上 -XX:+PrintGCDetails 来打印 GC 日志的方式来观察 GC 的问题。

Flink 内存配置与调优

TaskManager 内存指标监控

JVM

Network

TaskManager 内存模型

Framework vs Task Memory

  • 区别:是否计入 Slot 资源。

    • 如果计入slot资源,就是task memory。
  • 总用量受限:

    • -Xmx (堆内内存)= Framework Heap + Task Heap
    • -XX:MaxDirectMemorySize (堆外内存)= Framewok Off-Heap + Task OffHeap
  • 无隔离

    • 续社区会实现动态资源隔离(flip-56)

Heap VS Off-Heap Memory

  • Heap

    • 堆内存,Java 对象数据
    • HeapStateBackend
  • Off-Heap

    • Direct

      • DirectByteBuffer

        • ByteBuffer.allocateDirect()
      • MappedBytebuffer

        • FileChannel.map()
    • Native

      • JNI,C/C++,Python
    • flink里面不区分 Direct 和 Native,统一叫做off heap

Network Memory

  • Direct Memory

  • 主要用于网络数据传输

  • 特点:

    • TaskManager 的各个 Slot 之间 没有隔离
    • 根据作业的拓扑确定 Network Memory
    • 主要决定于 Buffer数量

Managed Memory

  • Native Memory 类型

  • 主要用于

    • RocksDBStateBackend
    • Batch Operator
  • 特点:

    • 同一 TaskExecutor 的各个 Slot 之间严格隔离
    • 多点少点都能跑,与性能挂钩
  • RocksDB 内存限制

    • state.backend.rocksdb.memory.managed(default:true)
    • 设定RocksDB使用内存为Managed Memory 大小
    • 目的:防止容器内存超限
    • Standalone 可关闭限制

JVM ****Metaspace & Overhead

  • JVM Metaspace

    • 存放 JVM 加载类的元数据
    • 加载的类越多需要的内存空间越大
  • JVM Overhead

    • Native Memory

    • 用于其他 JVM 内存开销

      • Code Cache
      • Thread Stack

内存模型

Flink SQL 实践(跳过)

Dynamic Table -> Stream 转换

  • Append-only 流: 仅通过 INSERT 操作修改的动态表可以通过输出插入的行转换为流

  • Retract 流: retract 流包含两种类型的 message: add messages 和 retract

  • Upsert 流: upsert 流包含两种类型的 message: upsert messages 和delete messages。会根据具体的key,需要动态表有唯一主键。与retract流区别是用单个message进行编码,因此效率会更高。