zookeeper 根据 ZAB 协议建立了主备模型完成 zookeeper 集群中数据的同步。这里所说的主备系统架构模型是指,在 zookeeper 集群中,只有一台 leader 负责处理外部客户端的事物请求(或写操作),然后 leader 服务器将客户端的写操作数据同步到所有的 follower 节点中。
- HBase 的架构,读写缓存?
HBase 的架构可以看这篇文章,非常详细:HBase 底层原理详解
下面说下HBase 的读写缓存:
HBase 的 RegionServer 的缓存主要分为两个部分,分别是MemStore和BlockCache,其中 MemStore 主要用于写缓存,而 BlockCache 用于读缓存。
HBase 执行写操作首先会将数据写入 MemStore,并顺序写入 HLog,等满足一定条件后统一将 MemStore 中数据刷新到磁盘,这种设计可以极大地提升 HBase 的写性能。
不仅如此,MemStore 对于读性能也至关重要,假如没有 MemStore,读取刚写入的数据就需要从文件中通过 IO 查找,这种代价显然是昂贵的!
BlockCache 称为读缓存,HBase 会将一次文件查找的 Block 块缓存到 Cache 中,以便后续同一请求或者邻近数据查找请求,可以直接从内存中获取,避免昂贵的 IO 操作。
- 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 淘汰算法进行淘汰。
- 聊聊 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
- spark 的几种 shuffle 说下?为什么要丢弃 hashshuffle?
前段时间刚写的,可以看下:Spark 的两种核心 Shuffle 详解
- java gc 可达性分析+垃圾回收器+垃圾回收算法+为什么分代垃圾回收+调优
JVM 相关的面试题可看这篇文章,文中第四、五题和本问题相关:精选大数据面试真题 JVM 专项
- 数据库引擎,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%’ 这样的部分模糊查询。
-
不支持联合索引的最左前缀匹配规则。
-
聊聊 tcp 和 udp 的区别
简单说下:
- TCP面向连接 (如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
-
UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
-
每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
-
TCP对系统资源要求较多;UDP对系统资源要求较少。
-
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的语法对其进行格式化,并在浏览器窗口中显示。
- 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进行了压缩这样数据体积就更小,在网络上传输就更快。
服务端有了推送功能,将客户端感兴趣的东西推给客户端,当客户端请求这些时,直接去缓存中取就行。
- 让你设计一个 hash 表,怎么设计?
可以把java的hashmap的实现原理说下,因为这就是hash表的经典设计!内容较多,并且网上资料很多,可以自己搜索查看!
- 时间不多了,手撸一个二分查找
二分查找图解:
二分查找:时间复杂度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)
![img](https://img-blog.csdnimg.cn/img_convert/26f9473b5431e94647a63e0aee65b016.png)
![img](https://img-blog.csdnimg.cn/img_convert/7b6930be5cc23546055e68b2ccaa47c6.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
resArr.append(midIndex)
[外链图片转存中...(img-Rm1Zl1aO-1714664771361)]
[外链图片转存中...(img-El38Sx9g-1714664771362)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**