流/批/OLAP 一体的 Flink 引擎架构初探和浅尝实践 | 青训营笔记
这是我参与「第四届青训营」笔记创作活动的的第2天。
流式计算的概念
回顾一下大数据的发展历程。
什么是大数据?大数据架构粗略的演变?
建议阅读第3个Reference:Juicy Big Data来详细了解发展历程。
大数据的定义
无法在一定时间内用常规软件工具对其进行获取、存储、管理和处理的数据集合
大数据架构的演变
- google 三驾马车: GFS MapReduce BigTable --> Hadoop 用 Map-Reduce
- Spark:解决 MapReduce 处理性能问题(在内存迭代),并抽象算子,提供 sql 高阶 api
- Flink:对于数据实时性有更高要求,实时性有更好性能,无论是批还是流都可支持 SQL
为什么需要流式计算?
有特定的场景需要流式计算。如下面列出的三个场景:
- 监控场景:如前端监控
- 金融风控:检测异常交易行为
- 实时推荐:根据用户行为发现偏好
实时性需求,带来了模式的变化
- 批量计算(离线计算):需要等待数据一批一批到来进行处理
- 非实时
- 静态数据
- 小时/天周期性计算
- 流式计算:
- 实时计算,快速低延时
- 数据源:无限流,动态,无边界
- 持续运行
- 流批一体
不同流式计算的框架对比
Flink 整体架构
分层架构
SDK层:SQL/Table, DataStream, Python
执行引擎层(RunTime):提供同一DAG,用于描述处理Pipeline,不管是流还是批,都会转化为DAG图;调度层再把 DAG 转化为分布式环境下的Task
状态存储层(State Backend):负责存储算子状态信息
资源调度层:多种部署环境的资源调度
总体架构
一个Flink集群包括一下两个核心组件:
- JobManager(JM):负责整个任务的协调工作
- 调度task
- 触发协调Task 做 Checkpoint
- 协调容错恢复等
- TaskManager(TM): 执行一个 DataFlow 的 Graph 的各个task以及data streams 的 buffer 和数据交换
JobManager
JobManager 的构成:
- Dispatcher:接收作业;拉起JM执行左作业;在JM 挂掉后恢复作业;
- JobMaster:管理一个 job 的整个生命周期;向 ResourceManager 申请 slot,将task 调度到对应TM上;
- ResourceManager:负责slot 资源调度和管理;TaskManager 拉起之后会向 RM 注册;
以 WordCount Flink 作业为例
业务逻辑转化为 DAG
算子并发
假设作业sink算子并发配置为1, 其余算子并发配置为2。 Streamming DataFlow 会转化为 parallel DataFlow(又称为 Execution Graph)。
链接 task
为了高效分布式执行,flink尽可能将不同 operator 链接(chain)在一起形成 Task。 每个 task 在一个线程(Operator Chain)中执行。
申请资源
Task 调度到TaskManager的slot中执行,一个 slot 执行运行同一个 task 的 subtask。 一个 task 是一个单独的线程执行。
为什么 Flink 可以做到流批一体?
为什么需要流批一体?
假设有如下业务需求: 在抖音中,实时统计一个短视频的播放量、点赞量、实时观看人数; ---> 流处理 在抖音中,按天统计播放量、评论量、广告收入; ---> 批处理
旧的架构:
痛点:
- 人力成本高、数据链路冗余:流、批两套系统,相同的逻辑
- 数据口径不一致:两套算子,可能产生不同程度误差,给业务方带来困扰
解法:流批一体
Flink 如何做到流批一体
批式计算是流式计算的特例,有界数据集(批示数据)也是一种特殊的数据流。理论上可以用一套引擎解决两种场景。
Flink 有几个模块可以做到流批一体:
- SDK层统一,流、批都可用 DataStream API 开发;
- Scheduler 层统一;
- Failover Recovery 层架构统一;
- Shuffle Service 层统一,流、批场景选择不同的 Shuffle Service;
Scheduler 层
Scheduler: 主要负责将作业 DAG 转化为分布式环境中可执行的 Task
在 Flink 1.12 之前,flink scheduler 调度可以分为两种模式:EAGER 和 LAZY 模式。
- EAGER 模式:申请一个作业所需要的全部资源,同时调度这个作业的全部task,所有task 之间采用 pipeline 进行通信,适用于流场景,如下图,12 个 task 会一起调度。
- LAZY 模式:先调度上游task,等待上游task产生数据后再调度下游,类似于 Spark 中的 Stage。适用于批场景。如下图,最小调度一个task即可,集群只需有一个 slot 便可运行。
1.12 后链接的task 构成一个 Pipeline Region,统一起来后,不管是流作业还是批作业都可以按照pipeline region 粒度来申请调度:
pipeline region 可以有两种模式:
- ALL_EDGES_BLOCKING: 所有task的数据交换都是blocking模式,分为12个pipeline region。(上图中是4个task为一个pipeline region block)
- ALL_EDGES_PIPELINED: 所有 task 的数据交换 都是pipeline模式,分一个 pipeline region。
统一起来后就可以schedular就可以实现流批一体了。
Shuffle Service 层
流批架构优化
OLAP 场景
场景典型的需求是:抖音的一些推广活动(例如春节红包雨),运营同学需要实时对产出结果进行多维分析,帮助后面的活动决策。
为什么三种场景可以用一套引擎解决
Flink 如何支持OLAP场景
Flink 目前的社区现状
Flink 浅尝实践
在mac本地跑一个简单的WordCount应用,参考:# Flink 从 0 到 1 学习 —— Mac 上搭建 Flink 1.6.0 环境并构建运行简单程序入门
附上我自己的代码(Flink 1.15.1版本):
package com.wordcount;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class SocketTextStreamWordCount {
public static void main(String[] args) throws Exception {
// check that the port is set
if (args.length != 2) {
System.err.println("USAGE:\nSocketTextStreamWordCount <hostname> <port>");
return;
}
String hostname = args[0];
int port = Integer.parseInt(args[1]);
// set up the streaming execution environment
final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// get input data
// Creates a new data stream that contains the strings received infinitely from a socket. Received strings are decoded by the system's default character set, using"\n" as delimiter. The reader is terminated immediately when the socket is down.
DataStreamSource<String> stream = env.socketTextStream(hostname, port);
// parse the data, group it, window it, and aggregate the counts
// key by the first letter of the string, then count the words
// the keyBy(0) is deprecated in the new version of Flink, so we use the new keySelector() instead
SingleOutputStreamOperator<Tuple2<String, Integer>> sum = stream.flatMap(new LineSplitter()).keyBy(new KeySelect()).sum(1);
sum.print();
env.execute("Java WordCount from SocketTextStream Example");
}
public static final class KeySelect implements KeySelector<Tuple2<String, Integer>, String> {
@Override
public String getKey(Tuple2<String, Integer> in) throws Exception {
// return the first position of the tuple so that the keyBy operation can group the line by the same word
return in.f0;
}
}
public static final class LineSplitter implements FlatMapFunction<String, Tuple2<String, Integer>> {
@Override
public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
// find all words in the line
String[] tokens = s.toLowerCase().split("\W+");
for (String token : tokens) {
if (token.length() > 0) {
// emit the word and 1 as a Tuple2 for the count
collector.collect(new Tuple2<String, Integer>(token, 1));
}
}
}
}
}
在 localhost 9000端口输入流后得出结果:
可见这个程序已经保存了state,但对于update 操作是直接插入的,(如the这个单词新增1之后直接在上一个状态+1后插入log),会有一个retract机制,在落表时把之前的状态retract掉。可以详细了解下:动态表。