跳到主要内容大数据架构面试核心问题解析:Flink、Spark 与算法 | 极客日志Javajava算法
大数据架构面试核心问题解析:Flink、Spark 与算法
本文涵盖大数据架构面试核心内容,包括 Flink SQL 解析流程、Ranger 鉴权机制、Checkpoint 失败原因及解决方案、Spark 3.0 AQE 优化特性、窗口计算原理以及常见算法题解。重点解析了 Flink 从 SQL 到 Operation 的转换步骤,分区裁剪策略,Join 类型实现,以及动态合并 Shuffle 分区和倾斜处理。针对 Checkpoint 超时问题,介绍了 FLIP-183 动态缓冲区和 FLIP-76 非对齐快照方案。此外还包含旋转数组最小值查找及 LRU 缓存实现的代码示例与复杂度分析。适合准备大数据开发岗位的技术人员参考。
一、项目经验介绍
面试官您好!我叫 xxx,xxxx 年 x 月毕业于 xxx 学校,xx 学历,目前就职于 xxx 公司 xxx 部门,职位为大数据开发工程师。主要从事 Flink、Hadoop 等组件及平台的开发工作。
工作以来,我先后参与了 xxx 项目、xxx 项目以及 xxx 项目,积累了丰富的项目经验,这些项目均获得了团队和领导的高度评价。
我对 Flink 组件有着浓厚的兴趣,工作之余经常钻研技术,例如 Flink 四大基石、Flink 内核应用提交流程、Flink 调度策略等。
入职 x 年,曾荣获优秀员工称号。以上是我的自我介绍,请面试官提问。
好的,那我重点介绍一下流计算平台。该平台对标阿里云的实时计算 Flink,是一个一站式、高性能的大数据计算与分析平台。底层基于 Flink 实现,平台提供多种核心功能,支持多种 Source、Sink 插件,内置统一的元数据管理,支持一键提交、应用管理、断点调试、监控告警、Ranger 鉴权等多个核心模块。
我主要负责对该平台的 Flink 版本升级(从原先的 Flink 1.11.0 升级到 1.14.0),同时对平台进行架构重构及代码优化,并参与核心模块应用管理、Ranger 鉴权模块的开发工作。
主要解决了多部门提交 Flink 任务需要大量开关配置问题,版本升级后的 SQL 语法校验、应用提交报错问题,以及 Ranger 鉴权问题。
二、Flink 核心机制
1. Ranger 鉴权机制
面试官:ranger 鉴权能介绍一下吗?是对哪方面进行鉴权?
Ranger 鉴权主要是对表级别的读写权限进行控制。
通过 Flink SQL 调用 parser 解析后获得 operation,然后判断 operation 的表类型是 DDL、DML 还是 DQL 的哪种。通过自研的 flink-ranger 插件获取 operation,从 operation 提取关键信息,组成 ranger 格式的约定进行鉴权。如果鉴权成功,就根据 Flink 原生的执行逻辑继续往下执行;反之报出鉴权异常。
面试官:为什么要用 Flink SQL 鉴权,而不用 Hive SQL 鉴权或者 HDFS 本身的鉴权?
首先该流计算平台底层基于 Flink 实现。在鉴权方面,由于编写的 SQL 在提交过程中需要走 Flink SQL 提交流程,所以在鉴权时直接通过 SQL 解析,校验拿到对应的 operation 类型。同时为了和流计算平台更适配,满足更多业务场景需求,才最终选用 Flink SQL 鉴权。其实用 Hive SQL 也是可以鉴权的,但在流式场景下 Flink SQL 解析更为直接高效。
2. Flink SQL 解析流程
面试官:Flink SQL operation 之前的解析流程清楚吗?可以详细介绍一下
Flink SQL 调用 parser() 方法,将 sqlText 转为 Flink 内部的 operation。在这其中主要经历了 4 大步骤:
- parse() 方法:将 SQL 转为未经校验的 AST 抽象语法树(sqlNode)。在解析阶段主要用到词法解析和语法解析。词法解析将 SQL 语句转为一组 token,语法解析对 token 进行递归下降语法分析。
- validate() 方法:将 AST 抽象语法树转为经过校验的抽象语法树(SqlNode)。在校验阶段主要校验两部分:
- 校验表名、字段名、函数名是否正确。
- 校验特殊的类型是否正确,如 join 操作、groupby 是否有嵌套等。
- rel() 方法:将抽象语法树 SqlNode 转为关系代数树 RelNode(关系表达式)和 RexNode(行表达式)。在这个步骤中,DDL 不执行 rel 方法,因为 DDL 实际是对元数据的修改,不涉及复杂查询。
- convert() 方法:将 RelNode 转为 operation。operation 包含多种类型,但最终都会生成根节点 ModifyOperation。
面试官:那在 operation 之后又做了哪些操作?
在 Flink 内部的 operation 之后,会调用 translate 方法将 operation 转为 transformation。在这中间也经历了四大步骤:
- translateToRel() 方法:先将 ModifyOperation 转换成 Calcite RelNode 逻辑计划树,再对应转换成 FlinkLogicalRel(RelNode 逻辑计划树)。
- optimize() 方法:将 FlinkLogicalRel 优化成 FlinkPhysicalRel。在这中间的优化规则包含基于规则优化 RBO 和基于代价优化 CBO。
- TranslateToExecNodeGraph() 方法:将物理计划转为 execGraph。
- TranslateToPlan() 方法:将 execGraph 转为 transformation。
3. 优化策略
RBO 规则优化中包含谓词下推、Join 优化、列裁剪、分区裁剪等等。
分区剪裁就是对于分区表或者分区索引来说,优化器可以自动从 from 和 where 中根据分区键直接提取出需要访问的分区,从而避免扫描所有的分区,降低了 IO 请求。
分区剪裁可以细分为静态分区剪裁和动态分区剪裁。其中静态分区剪裁发生在 SQL 语句编译阶段,而动态分区剪裁则发生在 SQL 语句执行阶段。对于分区键是常量值优化器会走静态分区剪裁的,如果分区键是变量形式优化器只会走动态分区剪裁。
4. Join 类型
面试官:那在 Flink SQL 中,join 都包含哪些类型?(引擎层的实现)
在 Join 中包含 Regular Join、Interval Join、Temporal Join、Lookup Join。
- Regular Join:包含 Left Join、Right Join、Inner Join、Full Join。
- Interval Join:时间区间 Join,表示两条流之间一段时间的 Join。
三、Spark 优化特性
了解 Spark 3.0 AQE(Adaptive Query Execution)自适应查询优化。
Spark 3.0 AQE 自适应查询里面包含 3 种优化,如动态合并 Shuffle 分区、动态调整 Join 策略、动态优化数据倾斜 Join 等。
(1) 动态合并 Shuffle 分区
在 Spark 中,Shuffle 前后的分区不同。如果分区数太少,那么每个分区处理的数据大小可能非常大,导致大分区处理时需要落盘,查询效率太低;如果分区过多,导致每个分区处理数据较少,这也会导致 IO 请求增多降低查询效率。
动态合并 Shuffle 的含义就是当 Map 端的两个分区经过 Shuffle 操作后,本来产生五个分区的,但因为有两个分区数据过小,所以直接对其进行合并,最终输出 3 个分区。
(2) 动态调整 Join 策略
总共包含 3 种 Join 策略:Broadcast Hash Join、Hash Join、SortMergeJoin。
(3) 动态优化数据倾斜 Join
针对数据倾斜场景,AQE 能够识别出倾斜的 Key,并将倾斜的 Task 单独拆分或广播小表来优化。
面试官:假如两张表 Join,但目前达不到 Broadcast Hash Join 的要求,Spark 3.0 是怎么处理的可以让其达到要求?
在 Spark 3.0 AQE 中通过动态调整 Join 策略,其中 Broadcast Hash Join 性能是最好的,前提是参加 Join 的一张表的数据能够装入内存。由于这个原因,当 Spark 估计参加 Join 的表数据量小于广播大小的阈值时,其会将 Join 策略调整为 Broadcast Hash Join。
所以当两张表 Join 时,如果 A 表的数据量大于广播大小的阈值,此时不能选择 Broadcast Hash Join,但恰好可以通过 Filter 条件将 A 表无用数据过滤掉,且 B 表不包含无用数据,这时候过滤掉后的 A 表数据量小于广播大小的阈值,此时就可以选择 Broadcast Hash Join。
四、Checkpoint 与容错
面试官:Checkpoint 失败有遇到过吗,什么原因导致的?
有遇到过,Checkpoint 失败一般都和反压相结合。导致 Checkpoint 失败的原因主要有两个:
-
数据流动缓慢,Checkpoint 执行时间过长
我们知道,Flink Checkpoint 机制是基于 Barrier 的。在数据处理过程中,Barrier 也需要像普通数据一样,在 Buffer 中排队,等待被处理。当 Buffer 较大或者数据处理较慢时,Barrier 需要很久才能够到达算子,触发 Checkpoint。尤其是当存在反压时,Barrier 需要在 Buffer 中流动数个小时,从而导致 Checkpoint 执行时间过长,超过了 Timeout 还没有完成,从而导致失败。
当算子需要 Barrier 对齐时,如果一个输入的 Barrier 已经到达,那么该输入的 Barrier 后面的数据会被阻塞住,不能被处理,需要等到其他输入 Barrier 到达之后,才能继续处理。在 Barrier 对齐过程中,其他输入数据处理都要暂停,将严重导致应用实时性,从而让 Checkpoint 执行时间过长,超过了 Timeout 还没有完成,导致执行失败。
-
状态数据过大
当状态数据过大,会影响每次 Checkpoint 的时间,并且在 Checkpoint 时 IO 压力也会很大,执行时间过长,导致超时还没有执行成功,从而导致执行失败。
对于数据流动缓慢解决思路是:
让 Buffer 中的数据变少,让 Barrier 能跳过 Buffer 中存储的数据。
这对应社区提出的 FLIP-183 Dynamic buffer size adjustment,其解决思路是只缓存配置时间内可以处理的数据量,这可以很好的控制 Checkpoint。
对于 Barrier 对齐问题,社区提出 FLIP-76 Unaligned Checkpoint。其解决思路是对于实时性要求很好,但数据重复性要求低的,可采用 Barrier 不对齐模式。当还有其他流的 Barrier 还没到达时,为了不影响性能,不用理会,直接处理 Barrier 之后的数据。等到所有流的 Barrier 的都到达后,就可以对该 Operator 做 CheckPoint 了。
对于状态数据过大问题:
FLIP-158 提出通用的增量快照方案,其核心思想是基于 State Changelog。Changelog 能够细粒度地记录状态数据的变化。具体描述如下:
- 有状态算子除了将状态变化写入状态后端外,再写一份到预写日志中。
- 预写日志上传到持久化存储后,Operator 确认 Checkpoint 完成。
- 独立于 Checkpoint 之外,State Table 周期性上传,这些上传到持久存储中的数据被称为物化状态。
- State Stable 上传后,之前部分预写日志就没用了,可以被裁剪。
五、窗口计算
Flink 支持的窗口包含两个重要属性(窗口长度 Size 和滑动间隔 Interval),通过窗口长度和滑动间隔来区分滚动窗口和滑动窗口。
滑动窗口(Sliding Window)数据有重叠,即 Size(1min) > Interval(30s)。
timeWindow(Time.seconds(10), Time.seconds(5)) --- 基于时间的滑动窗口
countWindow(10, 5) --- 基于数量的滑动窗口
六、常见算法题
面试官:我们写两道算法吧,先看看第一道
给定一个有序数组,前 n 位往后移,例如 {1,2,3,4,5} -> {3,4,5,1,2},求其中的最小值。
该题其实就是让你用最优解找一个数组中的最小值,可以使用二分查找法。
时间复杂度 O(log n),空间复杂度 O(1)
public class Main {
public static void main(String[] args) {
int[] nums = {4, 5, 6, 7, 1, 2, 3};
System.out.println(test(nums));
}
public static int test(int[] nums) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int mid = (low + high) / 2;
if (nums[mid] < nums[high]) {
high = mid;
} else {
low = mid + 1;
}
}
return nums[low];
}
}
面试官:LRU 算法,先说一下原理,然后介绍一下实现思路
LRU 被称作最近最少使用算法,是一种页面置换算法。其核心思想是将最近最久未使用的页面予以淘汰。就是一种缓存淘汰算法。
实现思路:
LRU 缓存机制可以通过哈希表 + 双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1) 的时间内完成 get 或者 put 操作。
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) { key = _key; value = _value; }
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
++size;
if (size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
--size;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
时间复杂度 O(1)
空间复杂度 O(capacity)
七、架构设计补充
数据倾斜通常指在分布式计算中,某些节点处理的数据量远大于其他节点,导致整体任务进度受限于慢节点。
解决方案包括:
- Key 加盐:给倾斜的 Key 加上随机前缀,打散到不同分区,聚合时再去掉前缀。
- 广播小表:如果是 Join 倾斜,尝试将小表广播到所有节点。
- 调整并行度:增加倾斜任务的并行度,分担负载。
- 自定义 Partitioner:根据数据特征自定义分区策略。
- Producer 端:设置
acks=all,确保所有副本确认收到;开启重试机制 retries。
- Broker 端:设置
min.insync.replicas 至少为 2,确保至少有一个副本存活;关闭 unclean.leader.election.enable。
- Consumer 端:关闭自动提交 Offset,手动提交 Offset,确保消费成功后再提交。
八、总结
本文涵盖了大数据架构面试的核心知识点,包括 Flink SQL 解析、Ranger 鉴权、Checkpoint 机制、Spark AQE 优化、窗口计算以及常见算法题解。建议面试者在准备时不仅关注 API 的使用,更要深入理解底层原理和优化策略,结合项目经验进行阐述。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online