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_id | STRING | 用户id |
| event | STRING | 事件 |
| album_id | BIGINT | 内容id |
| time | BIGINT | 时间 |
| cnt | BIGINT | 数量 |
动态表
暂时无法在飞书文档外展示此内容
| user_id | event | album_id | time | cnt |
|---|---|---|---|---|
| 11 | show | 123 | 1 | 1 |
| 11 | click | 123 | 2 | 1 |
| 22 | show | 597 | 3 | 1 |
| 33 | show | 123 | 4 | 1 |
| 11 | click | 786 | 5 | 1 |
经过和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_id | album_id | multi_feature |
|---|---|---|
| 11 | 123 | 1,0,0,0,0 |
| user_id | album_id | multi_feature |
|---|---|---|
| 11 | 123 | 2,0,0,0,0 |
| user_id | album_id | multi_feature |
|---|---|---|
| 11 | 123 | 2,0,0,0,0 |
| 22 | 597 | 0,1,0,0,0 |
| user_id | album_id | multi_feature |
|---|---|---|
| 11 | 123 | 2,0,0,0,0 |
| 22 | 597 | 0,1,0,0,0 |
| 33 | 123 | 1,0,0,0,0 |
| user_id | album_id | multi_feature |
|---|---|---|
| 11 | 123 | 2,0,0,0,0 |
| 22 | 597 | 0,1,0,0,0 |
| 33 | 123 | 1,0,0,0,0 |
| 11 | 786 | 0,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种类型:
- 集群的生命周期和资源隔离
- 根据程序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 State | Operator 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 之间有三个主要区别:
- Map 格式类型
- 需要有一条广播的输入流
- 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实现原理
exactly once
实现 exactly once 的分布式快照/状态检查点方法受到 Chandy-Lamport 分布式快照算法的启发。通过这种机制,流应用程序中每个算子的所有状态都会定期做 checkpoint。如果是在系统中的任何地方发生失败,每个算子的所有状态都回滚到最新的全局一致 checkpoint 点。在回滚期间,将暂停所有处理。源也会重置为与最近 checkpoint 相对应的正确偏移量。整个流应用程序基本上是回到最近一次的一致状态,然后程序可以从该状态重新启动。
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
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 point | save point | |
|---|---|---|
| 触发管理方式 | Flink Runtime 自动管理和触发 | 用户手动触发 |
| 主要功能 | - Task 发生异常时恢复状态数据 |
- Application 异常恢复
- 数据一致性保证 | - 有计划备份状态数据,使作业停止后能够再次 恢复原有计算状态
- 作业升级,备份
- 修改代码,调整并发等 | | 特点 | - 轻量,支持增量更新
- 作业停止后默认清除 | - 持久化存储
- 标准化格式存储,允许代码逻辑及配置发生改 变,作业可以人为地从 Savepoint 中恢复 |
StateBackends 状态管理器(重点)
主要类别
JVM Heap state backend
RocksDB State backend
对比
| MemoryStateBackend | FsStateBackend | RocksDBStateBackend | |
|---|---|---|---|
| 构造方法 | 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 资源调度
- taskmanager register phase
- slot allocate phase
- 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值调整发送。
-
在接收端,每个 Channel 在初始阶段会被分配固定数量的 Exclusive Buffer,这些 Buffer 会被用于存储接受到的数据,交给 Operator 使用后再次被释放。Channel 接收端空闲的 Buffer 数量称为 Credit,Credit 会被定时同步给发送端被后者用于决定发送多少个 Buffer 的数据。在流量较大时,Channel 的 Exclusive Buffer 可能会被写满,此时 Flink 会向 Buffer Pool 申请剩余的 Floating Buffer。这些 Floating Buffer 属于备用 Buffer,哪个 Channel 需要就去哪里。
-
在发送端,一个 Subtask 所有的 Channel 会共享同一个 Buffer Pool,这边就没有区分 Exclusive Buffer 和 Floating Buffer。发送端也会同步backlog的信息给下游。所以信息的同步是双向的。
反压采样
分析具体原因及处理
在实践中,很多情况下的反压是由于数据倾斜造成的,这点我们可以通过 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进行编码,因此效率会更高。