一场比较有深度的面试

246 阅读8分钟

一、你能简单描述一下HBase吗?能画出它的架构图吗?

HBase是一个面向列的 NoSQL 分布式数据库,它利用HDFS作为底层存储系统。那么,HBase相对于传统的关系型数据库有什么不同呢?

1、HBase是schema-free的,它的列是可以动态增加的(仅仅定义列族),并且为空的列不占物理存储空间; 2、HBase是基于列存储的,每个列族都由几个文件保存,不同的列族的文件是分离的; 3、HBase自动切分数据,使得数据存储自动具有很好的横向扩展性; 4、HBase没有任何事务,提供了高并发读写操作的支持。

HBase中的Table是一个稀疏的、多维度的、排序的映射表,这张表的索引是[RowKey, ColumnFamily, ColumnQualifier, Timestamp],其中Timestamp表示版本,默认获取最新版本。HBase是通过RowKey来检索数据的,RowKey是Table设计的核心,它按照ASCII有序排序,因此应尽量避免顺序写入。RowKey设计应该注意三点:

1、唯一原则:在HBase中rowkey可以看成是表的主键,必须保证其唯一性。 2、散列原则:由于rowkey是按字典有序的,故应避免rowkey连续有序而导致在某一台RegionServer上堆积的现象。例如可以拼接随机数、将时间戳倒序等。 3、长度原则:设计时RowKey要尽量短,这样可以提高有效数据的比例,节省存储空间,也可以提高查询的性能。

下面是HBase的整体架构图:

图片.png 二、你说了解kafka,能简单描述一下Kafka吗?能画出它的架构图吗?

Kafka是一个高吞吐、易扩展的分布式发布-订阅消息系统,它能够将消息持久化到磁盘,用于批量的消费。Kafka中有以下几个概念:

1、Topic:特指Kafka处理的消息源(feeds of messages)的不同分类; 2、Partition:Topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset); 3、Broker:Kafa集群中包含一台或多台服务器,这种服务器被称为broker; 4、Producer:生产者,向Kafka的一个topic发布消息; 5、Consumers:消费者,从kafka的某个topic读取消息。

Kafka架构图如下:

图片.png 详见:Apache Kafka:下一代分布式消息系统

三、请介绍你的一个亮点项目?你在其中做了什么?碰到了什么技术难点?

【解】介绍项目《九州卡牌》手游,我在项目中主要负责客户端逻辑与战斗效果的实现,以及网络通信模块的设计与开发。 首先,对于网络通信我们选择使用TCP长连接,因为对于卡牌类手游可以容忍偶尔地延迟,并且有服务器主动给客户端推送消息的需求。 优点: 1、简单有效的长连接; 2、可靠的信息传输; 3、数据包的大小没有限制; 4、服务器可以主动向客户端推送消息(广播等)。

客户端每隔3s发送一次心跳包给服务器,通知服务器自己仍然在线,并获取服务器数据更新 —— 心跳包可以防止TCP的死连接问题,避免出现长时间不在线的死链接仍然出现在服务端的管理任务中。当客户端长时间切换到后台时,进程被挂起,连接会断开。 TCP协议本身就有keep-alive机制,为什么还要在应用层实现自己的心跳检测机制呢?

1、TCP的keep-alive机制可能在短暂的网络异常中,将一个良好的连接给断开; 2、keep-alive设计初衷是清除和回收死亡时间长的连接,不适合实时性高的场合,而且它会先要求连接一定时间内没有活动,周期长,这样其实已经断开很长一段时间,没有及时性; 3、keep-alive不能主动通知应用层; 4、另外,想要通过心跳包来获取服务器的数据更新,所以选择自己在应用层实现;

还有一个问题就是一台机器的连接数有限制,可以通过滚服或者分布式来解决。

1、滚服:指老的服务器连接数达到上限了,就开新的服务区,不同服务区的用户不能交互。 2、分布式:长连接不分服的话,可以多个cluster节点连接同样的CACHE数据源,只是跨节点进行通信比较麻烦一点(如用户A连接到节点1,用户B连接到节点2,用户A向节点1发起TCP请求处理业务需要再通知到节点2的用户B)。一般来说有2种解决方案: ①是建立场景服务器,即专门用一个socket server来保持所有玩家的连接,然后它只处理数据推送,不做业务,可以达到10-20W承载; ②是采用发布订阅方式实现节点间的实时通信。

我在Linux下写了一个Socket心跳包示例程序,见文《TCP socket心跳包示例程序》。

四、请介绍一下MapReduce的工作原理。

【解】MapReduce是一个分布式计算框架,用于大规模数据集的并行运算。简单地说,MapReduce就是”任务的分解与结果的汇总”:将一个大的数据处理任务划分成许多个子任务,并将这些子任务分配给各个节点并行处理,然后通过整合各个节点的中间结果,得到最终结果。

MapReduce是主从架构,在master上跑的是JobTracker/ResourceManager,负责资源分配与任务调度;而各个slave上跑的是TaskTracker/NodeManager,负责执行任务,并定期向master汇报最新状态与执行进度。

对于一个MR任务,它的输入、输出以及中间结果都是<key, value>键值对:

Map:<k1, v1> ——> list(<k2, v2>)
Reduce:<k2, list(v2)> ——> list(<k3, v3>)

MR程序的执行过程主要分为三步:Map阶段、Shuffle阶段、Reduce阶段,如下图:

图片.png 1、Map阶段 分片(Split):map阶段的输入通常是HDFS上文件,在运行Mapper前,FileInputFormat会将输入文件分割成多个split ——1个split至少包含1个HDFS的Block(默认为64M);然后每一个分片运行一个map进行处理。

执行(Map):对输入分片中的每个键值对调用map()函数进行运算,然后输出一个结果键值对。 Partitioner:对 map 函数的输出进行partition,即根据key或value及reduce的数量来决定当前的这对键值对最终应该交由哪个reduce处理。默认是对key哈希后再以reduce task数量取模,默认的取模方式只是为了避免数据倾斜。然后该key/value对以及partitionIdx的结果都会被写入环形缓冲区。

溢写(Spill):map输出写在内存中的环形缓冲区,默认当缓冲区满80%,启动溢写线程,将缓冲的数据写出到磁盘。 Sort:在溢写到磁盘之前,使用快排对缓冲区数据按照partitionIdx, key排序。(每个partitionIdx表示一个分区,一个分区对应一个reduce) Combiner:如果设置了Combiner,那么在Sort之后,还会对具有相同key的键值对进行合并,减少溢写到磁盘的数据量。

合并(Merge):溢写可能会生成多个文件,这时需要将多个文件合并成一个文件。合并的过程中会不断地进行 sort & combine 操作,最后合并成了一个已分区且已排序的文件。

2、Shuffle阶段:广义上Shuffle阶段横跨Map端和Reduce端,在Map端包括Spill过程,在Reduce端包括copy和merge/sort过程。通常认为Shuffle阶段就是将map的输出作为reduce的输入的过程

Copy过程:Reduce端启动一些copy线程,通过HTTP方式将map端输出文件中属于自己的部分拉取到本地。Reduce会从多个map端拉取数据,并且每个map的数据都是有序的。

Merge过程:Copy过来的数据会先放入内存缓冲区中,这里的缓冲区比较大;当缓冲区数据量达到一定阈值时,将数据溢写到磁盘(与map端类似,溢写过程会执行 sort & combine)。如果生成了多个溢写文件,它们会被merge成一个有序的最终文件。这个过程也会不停地执行 sort & combine 操作。

3、Reduce阶段:Shuffle阶段最终生成了一个有序的文件作为Reduce的输入,对于该文件中的每一个键值对调用reduce()方法,并将结果写到HDFS。

参考《了解MapReduce核心Shuffle》