|
字节一面
1 面试官:简单的做个自我介绍吧
面试官,您好!我叫 xxx 。我于 xxxx 年 x 月从 xxx 学校毕业,学历为 xx 。目前我在 xxx 公司的 xxx 部门就职,职位是大数据开发工程师。我主要从事 xxx 组件以及平台的开发工作。
工作后,我参与了 xxx 项目。我还参与了 xxx 项目。并且我参与了 xxx 项目。通过这些项目,我积累了丰富的项目经验。而且,这 x 个项目都获得了领导的一致好评。
我对 Flink 组件怀有浓厚的兴趣。在工作之余,我常常钻研技术,比如 Flink 的四大基石,还有 Flink 内核应用的提交流程以及 Flink 的调度策略等。
我入职已经 x 年了,并且曾经荣获过优秀员工。这就是我的自我介绍,接下来请面试官提问。
2 面试官:介绍一下你最拿手的项目
我重点来介绍流计算平台。这个平台是对标阿里云的实时计算 Flink 的。它是一个一站式且高性能的大数据计算、分析平台。其底层是基于 Flink 来实现的。平台能提供多种核心功能,还支持多种 、sink 插件。并且内置了统一的元数据管理。同时,它支持一键提交、应用管理、断点调试、监控告警、鉴权等多个核心模块。
我主要负责该平台的 Flink 版本升级工作,将原先的 Flink 1.11.0 升级到 1.14.0。同时,我还对平台进行了架构重构以及代码优化。此外,我参与了核心模块应用管理的工作,也参与了鉴权模块的开发工作。
解决了多部门提交 Flink 任务时需要大量开关配置的问题,解决了版本升级后 SQL 语法校验的问题,解决了应用提交报错的问题,还解决了鉴权问题。
3 面试官: 鉴权能介绍一下吗?是对哪方面进行鉴权?
鉴权是对表级别的读写进行鉴权。
通过 Flink sql 进行调用并解析后获取相关内容,接着判断该表的类型属于 DDL、DML 还是 DQL 中的哪一种,利用自研的 flink-插件去获取信息,从特定的地方提取关键信息,按照约定组成特定格式来进行鉴权,若鉴权成功,就依照 Flink 原生的执行逻辑继续执行下去,若鉴权失败则报出鉴权异常。
为什么要使用 Flink sql 进行鉴权呢?为什么不使用 Hive sql 鉴权呢?又为什么不使用 HDFS 本身的鉴权呢?
该流计算平台底层是以 Flink 来实现的。在鉴权方面,因为编写的 SQL 在提交时要经过 Flink SQL 提交流程,所以在进行鉴权时直接通过 SQL 解析,对拿到的对应的类型进行校验。同时,为了让流计算平台更适配,满足更多业务场景的需求,最终选用了 Flink SQL 鉴权。其实用 Hive SQL 也是能够进行鉴权的。
面试官询问:对于 Flink sql 之前的解析流程是否清楚呢?能否详细地介绍一下。
如下图所示:
Flink sql 调用某一方法,把某种东西转为 Flink 内部的某种形式。在这个过程中主要包含 4 大步骤。
调用 parse() 方法,把 sql 转化为未经校验的 AST 抽象语法树。在解析过程中,主要运用了词法解析以及语法解析。
词法解析会把 sql 语句转变为一组 token,语法解析会对 token 进行递归下降的语法分析。
调用某个方法,把 AST 抽象语法树转化为经过校验的抽象语法树。在校验阶段主要校验两方面的内容:
校验表名、字段名、函数名是否正确,
校验特殊类型是否正确,包括判断是否有 join 操作,以及是否存在嵌套等情况。
调用 rel() 方法,把抽象语法树转变为关系代数树(关系表达式)以及行表达式。在这个过程中,DDL 不会执行 rel 方法,原因是 DDL 实际上是对元数据进行修改,而非涉及复杂查询。
调用()方法,把 进行转化,转化后的内容包含多种类型,不过最终都会生成根节点 。
6 面试官:那在 之后又做了哪些操作?
如下图所示:
在 Flink 内部进行到某个阶段之后,会调用特定的方法把某事物转为另一事物。在这个过程中,经历了以下四大步骤:
调用()方法,首先把 转换成 逻辑计划树,接着再将其对应转换成( 逻辑计划树);
调用()方法把某个东西优化成。在这期间的优化规则包含基于规则的优化 RBO 以及基于代价的优化 CBO。
(3) 调用raph() 方法将物理计划转为 。
(4) 调用() 方法将 转为 。
7 面试官:ROB 里面都了解哪些规则优化?
RBO 规则优化包含了谓词下推,包含了 Join 优化,包含了列裁剪,还包含了分区裁剪等等。
8 面试官:分区裁剪主要解决什么问题?
分区剪裁针对分区表或分区索引而言,优化器能够依据分区键,从 from 和 where 中自动提取出需要访问的分区,这样就避免了对所有分区的扫描,进而降低了 IO 请求。
分区剪裁分为静态分区剪裁和动态分区剪裁。静态分区剪裁在 sql 语句编译阶段发生,动态分区剪裁在 sql 语句执行阶段发生。若分区键是常量值,优化器会走静态分区剪裁;若分区键是变量形式,优化器只会走动态分区剪裁。
面试官询问在 flink sql 中,join 包含哪些类型(主要是引擎层的实现方面)。
在“join”中包含了“join”、“join”、“join”、“join”。
join 包含有 left join 这种连接方式,也包含 right join 这种连接方式,还包含 inner join 这种连接方式,同时包含 full join 这种连接方式。
join 所指的是在时间区间内,两条流之间存在一段时间的 join 情况。
10 面试官:Spark 3.0 优化特性了解不?
了解 Spark 3.0 AQE 自适应查询优化。
AQE 自适应查询包含 3 种优化。其中有动态合并分区。还有动态调整 join 策略。另外有动态优化数据倾斜 join 等。
(1) 动态合并 分区
在 spark 里,前后的分区存在差异。若分区数过少,那么每个分区处理的数据量可能会很大,进而致使大分区处理时需要将数据落盘,使得查询效率变得很低;倘若分区过多,就会导致每个分区处理的数据较少,这样也会使 IO 请求增多,从而降低查询效率。
动态合并的含义是,在 map 端的两个分区经过特定操作后,原本会产生五个分区。然而,由于有两个分区的数据过小,所以直接对这两个分区进行合并操作,最终输出 3 个分区。
(2) 动态调整 join 策略。
包含 3 种 join 策略,分别是 hash join、hash join。
(3) 动态优化数据倾斜 join
面试官询问:假如两张表需要进行 join 操作,但是目前无法满足 hash Join 的要求,那么应该如何处理这种情况,才能够使其达到要求呢?
在.0 AQE 里会动态调整 join 策略。其中有一种情况是 hash join 的性能最佳,而这种情况的前提是参与 join 的一张表的数据能够被装入内存。正因为如此,当 Spark 估计参与 join 的表的数据量小于广播大小的阈值时,它就会把 Join 策略调整为 hash join。
所以当两张表进行 join 操作时,如果 A 表的数据量比广播大小的阈值大,那么就不能选择 hash join 。然而,如果恰好能够通过条件把 A 表的无用数据过滤掉,并且 B 表不包含无用数据,这样过滤掉后的 A 表数据量就会小于广播大小的阈值,在这种情况下就可以选择 hash join 。
12 面试官: 失败有遇到过吗,什么原因导致的?
遇到过这种情况,失败通常与反压相互关联。导致失败的原因主要有以下两个:
1. 数据流动缓慢, 执行时间过长。
我们知道,Flink 机制是以某种方式基于……的。在数据处理期间,它也如同普通数据那般,需在……中排队,等候被处理。倘若……较大或者数据处理较为缓慢,那么它到达算子就需要很长时间,进而触发……。特别是当存在反压情况时,它得在……中流动好几个小时,这就致使……执行时间过长,即便超过了……,依然还未完成,最终导致失败。
当需要对齐算子时,如果一个输入已经到达,那么该输入后面的数据会被阻塞,不能被处理,必须等到其他输入到达之后才能继续处理。在对齐过程中,其他输入数据的处理都要暂停,这会严重影响应用的实时性,使得执行时间过长,超过了规定时间还没有完成,从而导致执行失败。
2. 状态数据过大。
当状态数据过大时,会对每次的时间产生影响。并且在进行某种操作时,IO 压力会很大,导致执行时间过长。如果执行时间过长,就可能出现超时但仍未执行成功的情况,进而导致执行失败。
13 面试官:怎么解决的上述问题?
对于数据流动缓慢 解决思路是:
让 中的数据变少
让 能跳过 中存储的数据。
这对应社区提出的 FLIP-183 的 size 。其解决思路为只缓存配置时间内能够处理的数据量,这样能够很好地进行控制。
关于对齐问题,社区提出了 FLIP-76。其解决思路为:对于实时性要求较高但数据重复性要求低的情况,可采用不对齐模式。在还有其他流尚未到达时,为不影响性能,无需理会,直接处理后续的数据。等到所有流都到达后,就可以对该流进行相关操作。
对于 状态数据过大问题:
FLIP-158 提出了一种通用的增量快照方案,其核心思想是以 state 为基础,能够对状态数据的变化进行细粒度的记录。具体情况如下:
有状态算子会把状态变化写入状态后端,同时还会另外写一份到预写日志里。
预写日志上传到持久化存储后, 确认 完成。
state table 独立于其他部分之外,它会周期性地上传。这些上传到持久存储中的数据被称作物化状态。
上传 state 后,之前的部分预写日志就失去了作用,能够被裁剪掉。
14 面试官:滑动窗口有啥特点?
Flink 支持的窗口具备两个重要属性。一个属性是窗口长度 size,另一个属性是滑动间隔。通过窗口长度和滑动间隔这两个属性,能够区分滚动窗口和滑动窗口。
滑动窗口的数据存在重叠情况,也就是说其大小(1 分钟)大于(30 秒)。
基于时间的滑动窗口,其中包含时间(10)和时间(5)
(10,5)---基于数量的滑动窗口
算法题
15 面试官:我们写两道算法吧,先看看第一道
给定一个有序数组,将其前 n 位向后移动。例如对于数组{1,2,3,4,5},移动后变为{3,4,5,1,2}。现在需要求出移动后数组中的最小值。
该题的目的是让你通过最优解来找出数组中的最小值,并且可以运用二分查找法。
时间复杂度 O(log n),空间复杂度O(1)
<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre><code>public class Main {
public 静态 void 主方法(String 数组 args){
定义一个整数数组 nums,其中包含元素 4、5、6、7、1、2、3。
输出由 nums 作为参数的 test 的结果。
}
如果循环结束后没有返回 -1 则返回 1。
int low = 0;
定义一个整数变量 high,其值为数组 nums 的长度减 1。
while (low<high){
int mid = (low+high)/2;
if(nums[mid]<nums[high]){
high = mid;
}else{
low = mid +1;
}
}
return nums[low];
}
}
</code></pre></p>
面试官询问 LRU 算法,要求先阐述其原理,接着介绍实现思路。
LRU 被叫做最近最少使用算法,它是一种页面置换算法。这种算法的核心思想是淘汰最近最久未使用的页面。它是一种缓存淘汰算法。
实现思路:
LRU 缓存机制能够借助哈希表与双向链表来实现。我们利用一个哈希表以及一个双向链表,对缓存中的所有键值对进行维护。
双向链表按照使用顺序来存储这些键值对。靠近头部的键值对是最近被使用的。靠近尾部的键值对是最久未被使用的。
哈希表就是普通的哈希映射。它通过缓存数据的键,从而能够映射到该数据在双向链表中的位置。
我们首先利用哈希表来进行定位,接着找出缓存项在双向链表中的具体位置,然后把它移动到双向链表的头部,这样就能在 O(1) 的时间内完成 get 或者 put 操作。
<p style='margin-bottom:15px;color:#555555;font-size:15px;line-height:200%;text-indent:2em;'> <pre><code>public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
创建一个公共的双向链表节点类。该类没有参数的构造函数。
创建一个公共的双向链表节点,其包含一个整数类型的键(key)和一个整数类型的值(value)。在构造函数中,将传入的键值对分别赋值给节点的 key 和 value 属性。具体来说,通过参数_key 来初始化节点的 key 属性,通过参数_value 来初始化节点的 value 属性。
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
定义了两个私有双向链表节点,分别是 head 和 tail 。
创建一个公共的 LRUCache 类,在其构造函数中接收一个整数类型的容量参数。这个构造函数用于初始化 LRUCache 对象,以便后续使用。具体来说,它会根据传入的容量参数来设置 LRUCache 的内部状态和数据结构,以实现最近最少使用缓存的功能。
this.size = 0;
this 的容量等于容量。
// 使用伪头部和伪尾部节点
创建了一个新的 DLinkedNode 并将其赋值给 head 。
tail 等于一个新的双链表节点。
head.next = tail;
tail.prev = head;
}
public int get(int key) {
通过 key 从 cache 中获取到一个 DLinkedNode 节点,这个节点被赋值给了 node 。
if (node == null) {
return -1;
}
如果 key 存在,首先利用哈希表进行定位,接着将其移到头部。
moveToHead(node);
return node.value;
}
公共的 void 方法 put,它接收一个整数类型的 key 和一个整数类型的 value。这个方法用于将给定的键值对存储到某个数据结构中。在这个方法内部,会根据具体的实现逻辑来进行相应的操作,以确保键值对能够被正确地存储和管理。
DLinkedNode node = cache.get(key);
if (node == null) {
如果不存在 key,就创建一个新的节点。
创建一个 DLinkedNode 类型的新节点 newNode,该节点的键为 key,值为 value。
// 添加进哈希表
将 key 和 newNode 放入 cache 中。
// 添加至双向链表的头部
将新节点添加到头部。
++size;
如果大小大于容量,那么就会执行以下操作;如果大小不大于容量,就不会执行以下操作。
如果超出了容量,就把双向链表的尾部节点删除掉。
DLinkedNode 的 tail 被移除了,即 removeTail() 被执行,然后得到的结果赋给了 tail 。
// 删除哈希表中对应的项
将尾部节点的键值从缓存中去除。
--size;
}
}
else {
如果存在 key ,那么首先通过哈希表来进行定位,接着修改 value ,之后将其移到头部。
node 的值被设置为 value 。
moveToHead(node);
}
}
private void 将节点添加到头部(DLinkedNode node);
node.prev = head;
node 的下一个节点为 head 的下一个节点。
head 的下一个节点的前一个节点等于 node ;
head.next = node;
}
如果传入的节点 node 不为空,且当前链表不为空,则执行以下操作:如果要删除的节点是头节点,就将头节点指向下一个节点,并将头节点的前一个节点设置为 null;如果要删除的节点不是头节点,就将当前节点的前一个节点的后一个节点设置为当前节点的后一个节点,将当前节点的后一个节点的前一个节点设置为当前节点的前一个节点,然后将当前节点的前一个节点和后一个节点都设置为 null。
node 的前一个节点的下一个节点等于 node 的下一个节点。
node 的下一个节点的前一个节点等于 node 的前一个节点。
}
8. 将 head 设置为 node。
removeNode(node);
addToHead(node);
}
如果尾部节点存在且有多个节点,找到倒数第二个节点,将其 next 指针置为 null,然后删除尾部节点并返回删除的节点。
DLinkedNode 的 res 为 tail 的前一个节点。
removeNode(res);
return res;
}
}
</code></pre></p>
时间复杂度 O(1)
空间复杂度 O()
最后把一份全套学习资料免费分享给大家,这份资料包含视频、源码和课件,希望能帮助到那些对现状不满,想要提升自己但又没有方向的朋友。
关于技术储备
学好的话,无论是用于就业还是开展副业赚钱,都很不错。不过,要学会的话,还是需要制定一个学习规划。最后,大家分享一份完整的学习资料,给那些想要学习的小伙伴们提供一些帮助!
一、所有方向的学习路线
对所有方向的技术点进行整理,从而形成各个领域的知识点汇总。其用处在于,你能够依据上面的知识点去寻找对应的学习资源,以此确保自己学得较为全面。
二、必备开发工具
三、视频合集
观看零基础学习视频,通过看视频来学习是既快捷又有效果的。跟着视频中老师的思路,从基础开始逐步深入,是比较容易入门的。
四、实战案例
光学理论本身是没有实际用处的。我们需要学会动手去操作,跟着一起进行实践。只有这样,才能将所学的光学理论运用到实际当中。在这个过程中,我们可以找一些实战案例来进行学习。
五、练习题
检查学习结果。
六、面试资料
我们学习的目的必然是找到高薪工作。这些面试题来自阿里、腾讯、字节等一线互联网大厂,且是最新的面试资料。同时,有阿里大佬给出了权威解答。刷完这一套面试资料后,大家相信都能找到满意的工作。
这份全套学习资料是完整版的且已经打包好了。有需要的小伙伴能够戳下方链接来免费领取。 |
|