字节跳动大数据开发面试题-附答案
此面试题来自牛客网友分享的字节跳动应届一面,面试时长一小时。
网友情况:985 本硕。
参考答案由公众号【五分钟学大数据】提供。如有错误,欢迎指正!
以下为面试过程中提问,岗位为大数据开发:
-
自我介绍+项目介绍
-
为什么用 kafka、sparkstreaming、hbase?有什么替代方案吗?
-
聊聊你觉得大数据的整个体系?
-
你看过 hdfs 源码?nn 的高可用说一下
-
zookeeper 简单介绍一下,为什么要用 zk?zk 的架构?zab?
-
hbase 的架构,读写缓存?
-
blockcache 的底层实现?你提到了 LRU 那除了 LRU 还可以有什么方案?
-
聊聊 sparkstreaming 和 flink?flink 流批一体解释一下?
-
spark 的几种 shuffle 说下?为什么要丢弃 hashshuffle?
-
java gc 可达性分析+垃圾回收器+垃圾回收算法+为什么分代垃圾回收+调优
-
数据库引擎,innodb 索引实现+聚集和非聚集区别+为什么用 b+树不用 hash
-
聊聊 tcp 和 udp 的区别
-
http 知道吗?说一下
-
http 版本之间的比较
-
让你设计一个 hash 表,怎么设计?
-
时间不多了,手撸一个二分查找
答案解析
本文首发公众号【五分钟学大数据】,可以搜索关注下,超多大数据精品文章
1. 自我介绍+项目介绍
自我介绍可以参考美团面试的这篇文章:美团优选大数据开发岗面试题
2. 为什么用 kafka、sparkstreaming、hbase?有什么替代方案吗?
根据简历中写的项目,谈谈为什么用这几个框架,是公司大数据平台历史选择还是更适合公司业务。
然后在说下每个框架的优点:
Kafka:
- 高吞吐量、低延迟:kafka 每秒可以处理几十万条消息,它的延迟最低只有几毫秒;
- 可扩展性:kafka 集群支持热扩展;
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;
- 容错性:允许集群中节点故障(若副本数量为 n,则允许 n-1 个节点故障);
- 高并发:支持数千个客户端同时读写。
Kafka 应用场景:
- 日志收集:一个公司可以用 Kafka 可以收集各种服务的 log,通过 kafka 以统一接口服务的方式开放给各种 consumer;
- 消息系统:解耦生产者和消费者、缓存消息等;
- 用户活动跟踪:kafka 经常被用来记录 web 用户或者 app 用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到 kafka 的 topic 中,然后消费者通过订阅这些 topic 来做实时的监控分析,亦可保存到数据库;
- 运营指标:kafka 也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;
- 流式处理:比如 spark streaming 和 flink。
Spark Streaming 优点:
-
spark streaming 会被转化为 spark 作业执行,由于 spark 作业依赖 DAGScheduler 和 RDD,所以是粗粒度方式而不是细粒度方式,可以快速处理小批量数据,获得准实时的特性;
-
以 spark 作业提交和执行,很方便的实现容错机制;
-
DStreaming 是在 RDD 上的抽象,更容易与 RDD 进行交互操作。需要将流式数据与批数据结合分析的情况下,非常方便。
因为我们的业务对实时性要求不是特别高,所以使用 spark streaming 是非常合适的。
HBase 优点:
-
HDFS 有高容错,高扩展的特点,而 Hbase 基于 HDFS 实现数据的存储,因此 Hbase 拥有与生俱来的超强的扩展性和吞吐量。
-
HBase 采用的是 Key/Value 的存储方式,这意味着,即便面临海量数据的增长,也几乎不会导致查询性能下降。
-
HBase 是一个列式数据库,相对于于传统的行式数据库而言。当你的单张表字段很多的时候,可以将相同的列(以 regin 为单位)存在到不同的服务实例上,分散负载压力。
有什么替代方案,就可以聊聊和这几个功能类似的框架,它们的优缺点等,比如 Apache kafka 对应的 Apache Pulsar;Spark Streaming 对应的 Flink;HBase 对应的列式数据库可以举几个例子,如 Cassandra、MongoDB 等。
3. 聊聊你觉得大数据的整个体系?
这个是一个开放性题,把你知道的大数据框架都可以说下,下面是我做的一个 Apache 大数据框架的集合图,当然也没有包含全部,只是比较常见的几个:
说的时候尽量按照它们的功能划分及时间先后顺序讲解。
4. 你看过 hdfs 源码?nn 的高可用说一下
一个 NameNode 有单点故障的问题,那就配置双 NameNode,配置有两个关键点,一是必须要保证这两个 NN 的元数据信息必须要同步的,二是一个 NN 挂掉之后另一个要立马补上。
- 元数据信息同步在 HA 方案中采用的是“共享存储”。每次写文件时,需要将日志同步写入共享存储,这个步骤成功才能认定写文件成功。然后备份节点定期从共享存储同步日志,以便进行主备切换。
- 监控 NN 状态采用 zookeeper,两个 NN 节点的状态存放在 ZK 中,另外两个 NN 节点分别有一个进程监控程序,实施读取 ZK 中有 NN 的状态,来判断当前的 NN 是不是已经 down 机。如果 standby 的 NN 节点的 ZKFC 发现主节点已经挂掉,那么就会强制给原本的 active NN 节点发送强制关闭请求,之后将备用的 NN 设置为 active。
如果面试官再问 HA 中的 共享存储 是怎么实现的知道吗?
可以进行解释下:NameNode 共享存储方案有很多,比如 Linux HA, VMware FT, QJM 等,目前社区已经把由 Clouderea 公司实现的基于 QJM(Quorum Journal Manager)的方案合并到 HDFS 的 trunk 之中并且作为默认的共享存储实现
基于 QJM 的共享存储系统主要用于保存 EditLog,并不保存 FSImage 文件。FSImage 文件还是在 NameNode 的本地磁盘上。QJM 共享存储的基本思想来自于 Paxos 算法,采用多个称为 JournalNode 的节点组成的 JournalNode 集群来存储 EditLog。每个 JournalNode 保存同样的 EditLog 副本。每次 NameNode 写 EditLog 的时候,除了向本地磁盘写入 EditLog 之外,也会并行地向 JournalNode 集群之中的每一个 JournalNode 发送写请求,只要大多数 (majority) 的 JournalNode 节点返回成功就认为向 JournalNode 集群写入 EditLog 成功。如果有 2N+1 台 JournalNode,那么根据大多数的原则,最多可以容忍有 N 台 JournalNode 节点挂掉
注:Hadoop3.x 允许用户运行多个备用 NameNode。例如,通过配置三个 NameNode 和五个 JournalNode,群集能够容忍两个节点而不是一个节点的故障。
HDFS 的其他内容可看之前写的这篇文章:HDFS 分布式文件系统详解
5. zookeeper 简单介绍一下,为什么要用 zk?zk 的架构?zab?
zk 介绍及功能:
Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。
ZooKeeper 本质上是一个分布式的小文件存储系统。提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。从而用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理(solr 的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。
zk 架构:
zk 架构图:
Leader:
Zookeeper 集群工作的核心;
事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;
集群内部各个服务器的调度者。
对于 create, setData, delete 等有写操作的请求,则需要统一转发给 leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。
Follower:
处理客户端非事务(读操作) 请求,
转发事务请求给 Leader;
参与集群 Leader 选举投票 2n-1 台可以做集群投票。
此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色。
Observer:
观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader 服务器进行处理。
不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务 处理能力的前提下提升集群的非事务处理能力。
简答:说白了就是增加并发的读请求
ZAB 协议全称:Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。
ZAB 协议是专门为 zookeeper 实现分布式协调功能而设计。zookeeper 主要是根据 ZAB 协议是实现分布式系统数据一致性。
zookeeper 根据 ZAB 协议建立了主备模型完成 zookeeper 集群中数据的同步。这里所说的主备系统架构模型是指,在 zookeeper 集群中,只有一台 leader 负责处理外部客户端的事物请求(或写操作),然后 leader 服务器将客户端的写操作数据同步到所有的 follower 节点中。
6. HBase 的架构,读写缓存?
HBase 的架构可以看这篇文章,非常详细:HBase 底层原理详解
下面说下HBase 的读写缓存:
HBase 的 RegionServer 的缓存主要分为两个部分,分别是MemStore和BlockCache,其中 MemStore 主要用于写缓存,而 BlockCache 用于读缓存。
HBase 执行写操作首先会将数据写入 MemStore,并顺序写入 HLog,等满足一定条件后统一将 MemStore 中数据刷新到磁盘,这种设计可以极大地提升 HBase 的写性能。
不仅如此,MemStore 对于读性能也至关重要,假如没有 MemStore,读取刚写入的数据就需要从文件中通过 IO 查找,这种代价显然是昂贵的!
BlockCache 称为读缓存,HBase 会将一次文件查找的 Block 块缓存到 Cache 中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的 IO 操作。
7. BlockCache 的底层实现?你提到了 LRU 那除了 LRU 还可以有什么方案?
我们知道缓存有三种不同的更新策略,分别是FIFO(先入先出)、LRU(最近最少使用)和 LFU(最近最不常使用)。
HBase 的 block 默认使用的是 LRU 策略:LRUBlockCache。此外还有 BucketCache、SlabCache(此缓存在 0.98 版本已经不被建议使用)
LRUBlockCache 实现机制:
LRUBlockCache 是 HBase 目前默认的 BlockCache 机制,实现机制比较简单。它使用一个 ConcurrentHashMap 管理 BlockKey 到 Block 的映射关系,缓存 Block 只需要将 BlockKey 和对应的 Block 放入该 HashMap 中,查询缓存就根据 BlockKey 从 HashMap 中获取即可。
同时该方案采用严格的 LRU 淘汰算法,当 Block Cache 总量达到一定阈值之后就会启动淘汰机制,最近最少使用的 Block 会被置换出来。在具体的实现细节方面,需要关注几点:
- 缓存分层策略
HBase 在 LRU 缓存基础上,采用了缓存分层设计,将整个 BlockCache 分为三个部分:single-access、mutil-access 和 inMemory。
需要特别注意的是,HBase 系统元数据存放在 InMemory 区,因此设置数据属性 InMemory = true 需要非常谨慎,确保此列族数据量很小且访问频繁,否则有可能会将 hbase.meta 元数据挤出内存,严重影响所有业务性能。
- LRU 淘汰算法实现
系统在每次 cache block 时将 BlockKey 和 Block 放入 HashMap 后都会检查 BlockCache 总量是否达到阈值,如果达到阈值,就会唤醒淘汰线程对 Map 中的 Block 进行淘汰。
系统设置三个 MinMaxPriorityQueue 队列,分别对应上述三个分层,每个队列中的元素按照最近最少被使用排列,系统会优先 poll 出最近最少使用的元素,将其对应的内存释放。可见,三个分层中的 Block 会分别执行 LRU 淘汰算法进行淘汰。
8. 聊聊 sparkstreaming 和 flink?flink 流批一体解释一下?
Flink 是标准的实时处理引擎,基于事件驱动。而 Spark Streaming 是微批( Micro-Batch )的模型。
下面就分几个方面介绍两个框架的主要区别:
-
架构模型:
- Spark Streaming 在运行时的主要角色包括:Master、Worker、Driver、Executor;
- Flink 在运行时主要包:Jobmanager、Taskmanager 和 Slot。
-
任务调度:
- Spark Streaming 连续不断的生成微小的数据批次,构建有向无环图 DAG, Spark Streaming 会依次创 DStreamGraph、JobGenerator、JobScheduler;
- Flink 根据用户提交的代码生成 StreamGraph,经过优化生成 JobGraph,然后提交给 JobManager 进行处理, JobManager 会根据 JobGraph 生成 ExecutionGraph,ExecutionGraph 是 Flink 调度最核心的数据结构,JobManager 根据 ExecutionGraph 对 Job 进行调度。
-
时间机制:
- Spark Streaming 支持的时间机制有限,只支持处理时间。
- Flink 支持了流处理程序在时间上的三个定义:处理时间、事件时间、注入时间。同时也支持 watermark 机 制来处理滞后数据。
-
容错机制:
- 对于 Spark Streaming 任务,我们可以设置 checkpoint,然后假如发生故障并重启,我们可以从上次 checkpoint 之处恢复,但是这个行为只能使得数据不丢失,可能 会重复处理,不能做到恰好一次处理语义。
- Flink 则使用两阶段提交协议来解决这个问题。
Flink 的两阶段提交协议具体可以看这篇文章:八张图搞懂 Flink 端到端精准一次处理语义 Exactly-once
9. spark 的几种 shuffle 说下?为什么要丢弃 hashshuffle?
前段时间刚写的,可以看下:Spark 的两种核心 Shuffle 详解
10. java gc 可达性分析+垃圾回收器+垃圾回收算法+为什么分代垃圾回收+调优
JVM 相关的面试题可看这篇文章,文中第四、五题和本问题相关:精选大数据面试真题 JVM 专项
11. 数据库引擎,innodb 索引实现+聚集和非聚集区别+为什么用 b+树不用 hash
innodb 索引实现:
innoDB使用的是聚集索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。若对Name列进行条件搜索,则需要两个步骤:第一步在辅助索引B+树中检索Name,到达其叶子节点获取对应的主键。
第二步使用主键在主索引B+树中再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。
聚集索引和非聚集索引的区别:
-
聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个。
-
聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。
-
聚集索引:物理存储按照索引排序;聚集索引是一种索引组织形式,索引的键值逻辑顺序决定了表数据行的物理存储顺序。
-
非聚集索引:物理存储不按照索引排序;非聚集索引则就是普通索引了,仅仅只是对数据列创建相应的索引,不影响整个表的物理存储顺序。
-
索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
数据库索引使用B+树原因:
InnoDB采用B+树结构,是因为B+树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数,降低IO、提升性能等。
数据库索引不适合用hash的原因:
-
区间值难找。因为单个值计算会很快,而找区间值,比如 100 < id < 200 就悲催了,需要遍历全部hash节点。
-
排序难。通过hash算法,也就是压缩算法,可能会很大的值和很小的值落在同一个hash桶里,比如一万个数压缩成1000个数存到hash桶里,也就是会产生hash冲突。
-
不支持利用索引完成排序、以及like ‘xxx%’ 这样的部分模糊查询。
-
不支持联合索引的最左前缀匹配规则。
12. 聊聊 tcp 和 udp 的区别
简单说下:
-
TCP面向连接 (如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
-
TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
-
UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
-
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
-
TCP对系统资源要求较多;UDP对系统资源要求较少。
13. http 知道吗?说一下
超文本传输协议(缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。
HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
- 客户端连接到Web服务器:
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.baidu.com。
- 发送HTTP请求:
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
- 服务器接受请求并返回HTTP响应:
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
- 释放连接TCP连接:
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
- 客户端浏览器解析HTML内容:
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
14. http 版本之间的比较
这道题字节经常问,需要记住:
http0.9:
最初的http版本,仅支持get方法,只能传输纯文本内容,所以请求结束服务段会给客户端返回一个HTML格式的字符串,然后由浏览器自己渲染。
http0.9是典型的无状态连接(无状态是指协议对于事务处理没有记忆功能,对同一个url请求没有上下文关系,每次的请求都是独立的,服务器中没有保存客户端的状态)
http1.0:
这个版本后任何文件形式都可以被传输,本质上支持长连接,但是默认还是短连接,增加了keep-alive关键字来由短链接变成长连接。
HTTP的请求和回应格式也发生了变化,除了要传输的数据之外,每次通信都包含头信息,用来描述一些信息。
还增加了状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。
http1.1:
HTTP1.1最大的变化就是引入了长链接,也就是TCP链接默认是不关闭的可以被多个请求复用。客户端或者服务器如果长时间发现对方没有活动就会关闭链接,但是规范的做法是客户端在最后一个请求的时候要求服务器关闭链接。对于同一个域名,目前浏览器支持建立6个长链接。
节约带宽,HTTP1.1支持只发送header头信息不带任何body信息,如果服务器认为客户端有权限请求指定数据那就返回100,没有就返回401,当客户端收到100的时候可以才把要请求的信息发给服务器。并且1.1还支持了请求部分内容,如果当前客户端已经有一部分资源了,只需要向服务器请求另外的部分资源即可,这也是支持文件断点续传的基础。
1.1版本中增加了host处理,在HTTP1.0中认为每台服务器都绑定一个唯一的ip地址,因此在URL中并没有传递主机名,但是随着虚拟机技术的发展,可能在一台物理机器上存在多个虚拟主机,并且他们共享了一个ip地址,http1.1中请求消息和响应消息都支持host头域,如果不存在还会报出错误。
http2.0:
多路复用:在一个连接里面并发处理请求,不像http1.1在一个tcp连接中各个请求是串行的,花销很大。
在1.0版本后增加了header头信息,2.0版本通过算法把header进行了压缩这样数据体积就更小,在网络上传输就更快。
服务端有了推送功能,将客户端感兴趣的东西推给客户端,当客户端请求这些时,直接去缓存中取就行。
15. 让你设计一个 hash 表,怎么设计?
可以把java的hashmap的实现原理说下,因为这就是hash表的经典设计!内容较多,并且网上资料很多,可以自己搜索查看!
16. 时间不多了,手撸一个二分查找
二分查找图解:
二分查找:时间复杂度O(log2n);空间复杂度O(1)
def binarySearch(arr:Array[Int],left:Int,right:Int,findVal:Int): Int={
if(left>right){//递归退出条件,找不到,返回-1
-1
}
val midIndex = (left+right)/2
if (findVal < arr(midIndex)){//向左递归查找
binarySearch(arr,left,midIndex,findVal)
}else if(findVal > arr(midIndex)){//向右递归查找
binarySearch(arr,midIndex,right,findVal)
}else{//查找到,返回下标
midIndex
}
}
拓展需求:当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到。
/*
{1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.
//分析
1. 返回的结果是一个可变数组 ArrayBuffer
2. 在找到结果时,向左边扫描,向右边扫描 [条件]
3. 找到结果后,就加入到ArrayBuffer
*/
def binarySearch2(arr: Array[Int], l: Int, r: Int,
findVal: Int): ArrayBuffer[Int] = {
//找不到条件?
if (l > r) {
return ArrayBuffer()
}
val midIndex = (l + r) / 2
val midVal = arr(midIndex)
if (midVal > findVal) {
//向左进行递归查找
binarySearch2(arr, l, midIndex - 1, findVal)
} else if (midVal < findVal) { //向右进行递归查找
binarySearch2(arr, midIndex + 1, r, findVal)
} else {
println("midIndex=" + midIndex)
//定义一个可变数组
val resArr = ArrayBuffer[Int]()
//向左边扫描
var temp = midIndex - 1
breakable {
while (true) {
if (temp < 0 || arr(temp) != findVal) {
break()
}
if (arr(temp) == findVal) {
resArr.append(temp)
}
temp -= 1
}
}
//将中间这个索引加入
resArr.append(midIndex)
//向右边扫描
temp = midIndex + 1
breakable {
while (true) {
if (temp > arr.length - 1 || arr(temp) != findVal) {
break()
}
if (arr(temp) == findVal) {
resArr.append(temp)
}
temp += 1
}
}
return resArr
}
本文首发公众号【五分钟学大数据】,可以搜索看下,超多大数据精品文章
可关注公众号:五分钟学大数据,获取最新文章!本专栏专注于大数据领域,分享大数据技术文章,大数据面试技巧及大数据前沿技术!