1 面试官介绍
OLAP引擎,离线引擎,大数据分析中间件
2 自我介绍
缺点:
(1)面试官让重点介绍自己最在行的项目,我真的在自我介绍上扯了一些别的东西…
(2)在面试的时候因为想看简历屏幕在不断的切屏,被面试官提醒说系统会有记录…太频繁了会影响面评
(3)介绍了raft kv和rpc项目,但是做了扩展,但是面试官是让说一下整体的架构的,没有让进行细节处理。
3 raft kv项目
3.1 简述raft的原理
Raft是一种为了管理复制日志而设计的分布式一致性算法,它的目标是提供一个更容易理解和实现的替代方案,用于替换难以理解和正确实现的Paxos算法。Raft算法将复杂的分布式一致性问题分解为几个关键元素,并提供了一种解决这些元素的方法。
gpt4的版本:
Raft的关键元素包括领导人选举、日志复制、安全性和成员变更。以下是这些元素的简述:
-
领导人选举:Raft算法中的所有节点都是平等的,并可以担任领导者(也称为主节点)或者跟随者(也称为从节点)。当一个领导者节点宕机或与集群失去联系时,会触发领导者选举。所有的节点都会定期发送心跳消息,如果在一段时间内(称为选且超时期)没有接收到领导者的心跳消息,跟随者节点就会开始一轮新的选举。节点会投票选举新的领导者,获得半数以上投票的节点成为新的领导者。
-
日志复制:日志是分布式系统中重要的一部分,所有的变更都会记录在日志中。在Raft算法中,领导者节点负责管理复制日志。当领导者接收到客户端的请求后,会将请求作为新的日志条目添加到它的日志中,并向其他的跟随者节点复制该日志条目。当领导者收到多数节点的确认后,该日志条目就被认为已经提交,领导者会执行该日志条目,然后将结果返回给客户端。
-
安全性:Raft算法确保了系统在发生故障或网络分区的情况下仍然能够保持一致性。Raft通过在领导者选举和日志复制过程中实施一些限制来确保系统的安全性,例如,只有拥有最新日志的节点才能被选举为领导者,领导者在复制日志时必须包括其上一个日志条目的索引和任期号等。
-
持久化存储:在Raft协议中,所有节点都必须持久化存储一些关键的信息,例如当前的任期号(用于领导人选举)、已投票的候选人(用于避免在一个任期中多次投票)、日志条目(用于记录所有的操作和状态变更)等。持久化存储的目的是在发生节点故障和重新启动时,节点可以从持久化存储中恢复数据,继续参与集群的运作。
-
快照机制:快照是Raft协议用于处理日志可能无限增长的问题的一个机制。当日志的大小增长到一定程度时,可以通过创建一个快照(即对当前系统状态的完整拷贝),并删除早于这个快照的所有日志条目来减少日志的大小。当节点故障并需要恢复时,可以直接使用快照恢复最近一次持久化的数据,能极大的缩短故障恢复时间。
Raft算法的设计目标是易于理解和实现,同时也能满足分布式系统的需求。
3.1.1 结合gpt4,我的简化版本,有助于快速让面试官知道项目的架构:
1 项目架构:有一个配置中心,实时生成配置,每一个新生成的配置文件都包含了哈希槽到某一个复制组的映射,然后还有若干个复制组,每一个复制组会定期从配置中心拉取配置文件到本地,然后开始将已经不属于自己管理的分片迁移到目标分片,当一个复制组迁移完所有分片并且接收到了所有分片后就会转换配置文件。
2 介绍raft的几个章节:这个项目基于raft共识算法做,能保证读写的强一致性。raft从leader选举、日志复制、持久化上、快照机制上能够保证这些,其中最重要的是leader选举、日志复制和持久化。
3 稍微详细介绍一下raft的各个部分:
3.1 raft的leader选举上,通过设置差异化的选举超时时间,大概率能在前几轮就选举出一个leader,新leader当选后会立即向从节点发送心跳,各个节点收到会改变自己为follower状态并且重置超时选举定时器;
3.2 选举成功后,就会进行日志复制操作,每次来一个新的put,append操作后都会将其转化为一条日志,并且将其传送给其他的节点,确保写的一致性;
3.3 同时,针对每一次的选举任期号、投票号以及日志变更操作,都会进行一次持久化,这样节点宕机重启后仍然能够根据日志进行恢复数据的操作,根据任期号和投票号可以继续加入集群中成为新的follower节点,避免出现两个leader
3.4 针对快照机制,各个节点每当数据达到一定长度的阈值后,就会对数据库中的kvMap进行一次持久化操作,存储完成后会截断一部分日志,故障恢复过程中加载快照能够极大的提高效率。
3.2 只要保证日志在多数节点就能保证一致性了嘛?
是的,只要保证日志在多数节点上一致,就可以保证系统的一致性。这是因为在Raft协议中,日志的复制是由领导者节点负责的,领导者节点接收到客户端的请求后,会先将请求记录到自己的日志中,然后复制到其他节点。当领导者收到大多数节点的确认后,这个操作就会被认为已经提交,并被应用到系统状态中。
这种机制确保了所有的节点都看到了相同的操作序列,达到了一致性。即使在领导者节点宕机的情况下,只要有一个新的领导者被选举出来,并且这个新领导者的日志包含了所有已经提交的日志条目,那么系统的一致性就可以得到保证。
但是需要注意的是,只有当日志条目在多数节点上一致,并且这些日志条目已经被提交时,才能保证一致性。如果一个日志条目只是被领导者节点记录,但还没有被复制到大多数节点,或者还没有被提交,那么这个日志条目的状态就是不确定的,不能保证一致性。
3.3 put,append请求发出到得到响应之前的大概流程
可能面试官只想听到简单版本的答案,比如是在没有分开写kv的情况下,客户端和一个raft集群的交互流程:
-
客户端每次发送put,append请求都会自增seqId,这个id用于raft集群去重,保证操作的幂等性的。客户端会轮询raft的集群节点,当轮询到一个非leader节点时,会收到一个错误,然后再轮询下一个节点,直到这个节点是leader节点,轮询到这正确的leader后,leader会成功收取到这个节点,然后它不会立即执行这个操作,而是先把这个操作放到日志中作为一个日志项,并且leader的日志复制线程会在未来的某一个时间点将这个日志复制到其他的从节点,当大多数从节点接受了并且正确响应后leader节点才会执行这个命令,执行成功后再返回一个响应给客户端。
-
客户端会通过哈希算法计算出put的key值存储于哪一个复制组,然后会从这个目标复制组中顺序选取一个raft的节点发送put请求,当被轮询的节点发现这个key不归自己管时会返回ErrWrongGroup,如果客户端接收到ErrWrongGroup状态码,它会觉得自己的配置有问题,于是去配置中心拉取最新的配置返回给客户端,客户端发送序列号不变的put请求到新的复制组,新目标复制组的节点可能会返回一些错误响应,比如可能会发现这个put的key正在迁移过程中时会返回ShardNotArrived状态码,此时客户端会等待一段时间重试直到分片到达,当发现自己不是leader时会响应一个ErrWrongLeader状态码给客户端,客户端会轮询该复制组的下一个节点直到找到正确的leader。客户端向正确的leader发送出请求后,leader会先把这个操作放到日志中作为一个日志项,并且leader的日志复制线程会在未来的某一个时间点将这个日志复制到其他的从节点,当大多数从节点接受了并且正确响应后leader节点才会执行这个命令,执行成功后再返回一个响应给客户端。(这也是3.5的答案)
3.4 这里的日志复制是指什么,WL嘛还是?(不知道wl是什么,但是我没有问为什么,而是说起了自己的东西)
"复制"在这里不是指磁盘写操作(Write I/O,WIO或者WL),而是指日志复制过程。这是Raft协议的核心,通过这种方式,Raft协议确保了分布式系统中的一致性。
3.5 raft leader什么时候该返回响应给客户端呢?(我只回答了正确收到命令的响应时间点,但是没谈到发生错误时的响应状态码)
答:3.3就是答案
3.6 如果我要查找的时候,必须走leader节点嘛(讲解了)
对,我提到了查的时候怎么四种保证一致性读的方案,性能主键提高:一致性读,
可以查看我的这篇博客的第39个问题
3.7 用读日志去处理读是个什么原理,难道我瞬时高并发10w个读请求,还要写10w个读日志吗?(面试官当时觉得这个点很差劲,最好应该直接写性能最高的那种)
答:是这样的,这是最笨的一种方法。
3.8 不是说最笨,我是指为什么要这样做?(其实没回答清楚,其实都是3.6回答冗余留下的坑)
答:因为机器可能发生故障,或者可能发生网络分区,在主分区中,新的leader会产生,但是它还不知道自己的日志提交索引,这个时候需要发送一个no-op entry去确认自己的commitIndex
正确回答:因为机器可能发生故障,或者可能发生网络分区,在主分区中,新的leader会产生,但是它还不知道自己的日志提交索引,如果leader直接读的话,可能会产生不一致,所以我采取的是也将读请求进行日志化操作,这样就一定能保证读请求在之前的写请求后面完成。
3.9 其实我多副本复制日志后再告诉用户你ok了,那我读取的时候至少多数实例能正常工作,知道哪个是最新的,那我是不是没必要这个操作?
答:对的
3.10 我目前的理解是,目前还没见到一个系统是会把这个读请求给存下来的(雷:这里的回答感觉给面试官一种你写需求会应付不关注质量)
答:只是为了解决读一致性,我采取了最简单的这种方案
3.11 对于readIndex机制,我需要每次读请求都要发送一次心跳给所有从节点,然后还需要等待过半数的响应吗?
答:第一次获取到了no-op entry的响应后,还可以使用lease read给它一个续期,在这个期间内,我得到的commitIndex都认为是有效的,一般这里的lease read时间和选举超时时间相等,就不需要每次都发送心跳,而是直接返回给客户端
4 rpc框架
本部分的所有总结都在深度思考rpc框架面经 一文中的第1,第5以及第6部分
4.1 出于什么目的?
答:了解分布式框架
4.2 简单介绍一下架构
项目架构:大概分为四个模块,服务提供者、消费者、rpc框架以及对消费者暴露出的集成接口模块
系统架构:
Apache Dubbo是一个高性能、轻量级的开源Java RPC框架。Dubbo主要提供了三个关键功能,包括接口级别的远程方法调用,容错与负载均衡,以及自动服务注册与发现。
Dubbo架构的主要流程如下:
-
启动服务提供者:服务提供者启动后,向注册中心注册其提供的服务。
-
启动服务消费者:服务消费者启动后,向注册中心查询其需要的服务信息,并建立与服务提供者的连接。
-
服务调用:服务消费者通过建立的连接调用服务提供者的服务。如果调用失败,Dubbo支持多种容错机制,例如failover(失败后重试其他服务器)。
-
加载平衡:在有多个服务提供者的情况下,Dubbo支持多种负载均衡策略,例如随机(Random)、轮询(RoundRobin)、最少活跃数(LeastActive)等。
-
服务监控:Dubbo可以记录服务的调用次数和调用时间等信息,以供开发者分析。
Dubbo的这些特性使其成为一个可靠、高性能的RPC框架,广泛用于服务间的远程方法调用。
4.3 rpc的调用过程(也是调用关系)详细讲讲
4.4 讲讲调用原理,比如服务怎么发现,怎么调用,提供者怎么响应?(我很容易跑到旁路的细枝末节,没有讲解到主干,面试官比较关注所有操作都成功的情况下编码解码的过程)
一个RPC(远程过程调用)框架的核心目的是允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的程序或服务,就像调用本地的方法或函数一样。以下是一个典型的RPC框架如何处理请求的概述:
-
服务定义和接口:
- 通常,开发者首先定义服务和它们的接口。这些接口定义了可以远程调用的方法。
- 使用IDL(接口定义语言)描述服务,生成客户端和服务器的存根。
-
服务注册:
- 服务提供者启动后,将自己的地址和提供的服务信息注册到注册中心。
-
服务发现:
- 服务消费者(或客户端)启动时,从注册中心订阅它想要调用的服务。
- 注册中心返回服务提供者的地址给消费者。为了提高效率,这些地址通常会在消费者本地进行缓存。
-
远程调用:
- 当客户端要调用某个远程方法时,它会通过本地代理或存根发起这个调用。
- 存根负责将方法调用转换为网络请求。这通常涉及序列化方法名称、参数等,然后发送到网络。
- 负载均衡策略(如轮询、随机或最少活跃调用)可以在这一步中应用,以选择最合适的服务提供者实例。
-
请求处理:
- 服务提供者接收到网络请求后,反序列化请求数据以获得原始的方法名称和参数。
- 服务提供者然后在本地执行这个方法,并获得结果。
-
响应返回:
- 服务提供者将执行结果序列化后,通过网络返回给服务消费者。
- 服务消费者的存根反序列化返回的数据,将其转换回原始的方法调用结果,并返回给调用方。
-
容错处理:
- 如果在RPC过程中发生错误(如网络中断、服务提供者崩溃等),RPC框架可能提供容错机制。
- 容错策略可能包括重新路由、重试、返回默认结果等。
-
监控和日志:
- 为了跟踪和监控远程调用的性能和可靠性,RPC框架通常提供日志记录和监控功能。
这个过程描述的是一个典型的同步RPC调用。但现代RPC框架也支持异步调用,其中客户端可以在不等待响应的情况下继续其它操作。
4.4.1 假设所有操作都成功,其调用链是什么样子的?怎么路由(这里的路由是指本地负载均衡的方法,但是我答成了去api网关进行路由,然后再经过网关返回的这么一个过程)
服务路由:
在有多个服务提供者提供相同服务的情况下,客户端需要决定调用哪一个服务提供者。这时,路由策略(如轮询、随机、最少活跃调用等)会被应用。负载均衡策略会根据本地缓存的服务提供者地址列表选择一个。
4.4.2 涉及到的HTTP协议以及自定义的MRF协议
1 在HTTP协议中,可以通过以下方式来区分请求报文和响应报文:
. 起始行的格式:
-
请求报文的起始行是一个请求行,它的格式为:
<HTTP方法> <请求URI> <HTTP版本>
。例如:
GET /index.html HTTP/1.1 -
响应报文的起始行是一个状态行,它的格式为:
<HTTP版本> <状态码> <状态描述>
。例如:
HTTP/1.1 200 OK
综上,最直接和可靠的方式是查看起始行。请求行和状态行的格式是唯一的,它们可以明确地告诉你报文是请求还是响应。
2 选择HTTP协议作为netty的上层应用协议
1 版本1:选择了HTTP协议作为我们的应用层协议,对于发送请求,有请求行、请求头、请求行以及请求体,其中请求头包含了一个content-type字段,这个字段包含了具体的序列化协议,比如常用的web服务器序列化方式application/json,还有一个content-length字段,为了区分包类型,即让服务器知道一个调用请求还是调用响应,我们可以利用http的请求行和状态行做区分。此外利用http请求的空行和content-length字段可以防止粘包和拆包问题。
3 自定义MRF协议(能跟面试官吹牛逼)
这几个字段的作用就如图所示,跟HTTP协议一对比,发现有很多相似之处。
4.6 比如我现在想要调用服务端的methodA方法,服务端怎么知道客户端想要调用的就是这个方法呢?
客户端会定一个类,叫RpcRequest,这个类定义了需要调用远程方法的所有属性,包括接口名、方法名、方法所需要的参数及其类型,同时还包含了一个请求号(这里的请求号可以不说,因为是旁路,会影响面试官的听到主要答案),然后通过客户端本地存根(也就是代理),会给我们自动创建相应的RpcRequest对象并且进行属性填充,这里的属性是根据我们调用的具体方法、消费者提供的参数进行填充的,
填充完成后,代理类还会帮我们根据选择的序列化器将实际的请求体进行序列化操作,序列化后的rpcRequest数据会放到请求体中,然后再通过网络发给服务提供者。
生产端拿到请求后会先进行反序列化请求体,拿到实际的RpcRequest的数据后会根据接口名、接口方法以及参数列表会进行反射得到一个可执行的方法,然后进行条用就行了。
4.7 编解码协议是什么(序列化和反序列化),想问你什么协议去序列化的
我用的google的protocol buffer
4.8 假设现在服务的provider想要下线了,如何做到让服务调用端感知?(本来应该是很简单的一个问题,我答非所问了)
在服务调用端设置一个超时时间,如果在相应的时间内没有回应,则认为这个服务实例不可用,然后轮询下一个实例。
当服务提供者(Service Provider)想要下线时,为了保证服务的可用性和避免服务调用失败,通常采取以下策略让服务消费者(Service Consumer)或调用端感知:
当服务提供者(Service Provider)想要下线时,为了保证服务的可用性和避免服务调用失败,通常采取以下策略让服务消费者(Service Consumer)或调用端感知:
-
使用服务注册与发现机制(涉及到注册中心、消费者和提供者三方的交互机制):
- 在许多RPC和微服务框架中,都采用了服务注册与发现的机制,如Zookeeper、Consul、Eureka等。
- 当服务提供者打算下线时,首先从服务注册中心撤销或下线其服务。
- 服务消费者定期从服务注册中心拉取服务列表,这样当提供者下线后,消费者会感知到这一变化,并不再向该提供者发送请求。
-
优雅关闭(服务提供者):
- 服务提供者在停机前,首先停止接收新的请求,但继续处理已经接收的请求。
- 一旦所有已接受的请求都处理完毕,再完全关闭服务。这确保了没有请求在中途被突然中断。
-
使用负载均衡器或API网关(利用第三方网关阻断流量):
- 如果你使用了负载均衡器或API网关来路由请求,当服务提供者想要下线时,首先在负载均衡器或API网关中移除该提供者。
- 通过这种方式,即使服务提供者还在运行,消费者的请求也不会被路由到该提供者。
-
健康检查(注册中心的机制):
- 服务提供者通常会提供健康检查的接口,用于报告其健康状态(consul)。
- 当服务提供者计划下线时,可以修改健康检查的响应,表示其不再健康或即将下线。
- 服务消费者或负载均衡器定期检查服务的健康状态,感知到该变化后,会停止向提供者发送请求。
-
通知和告警(预计的维护或下线):
- 当服务提供者计划下线时,可以手动通知所有已知的服务消费者。
- 这通常适用于预计的维护或下线,确保消费者有足够的时间来做出响应或备份方案。
-
使用熔断器和重试机制(熔断重试,会造成多余的请求):
- 熔断器可以检测到连续的请求失败,并自动切断到提供者的请求,避免向已下线的服务发送请求。
- 重试机制会在请求失败时尝试其他可用的服务提供者。
结合上述策略,可以确保当服务提供者下线时,服务消费者能够及时感知并作出相应的处理,避免服务中断或大量的请求失败。
4.9 假设现在服务端处理一个请求需要一秒钟,服务端本身还有很多请求没有处理完毕,但是在某一秒钟的时候我把服务端的机器给关了,那用户就报错了嘛,这块怎么处理让它不报错,因为报错就会对业务产生消极影响。
如果客户端(用户的设备或应用)在等待服务端响应时突然关机或断开连接,确保用户不看到错误是一项挑战。以下是一些建议的处理方法:
-
服务端冗余与故障转移:
- 在服务端部署多个实例,当一个实例发生故障时,流量可以自动切换到另一个健康的实例。
- 使用负载均衡器来分发请求,同时检查各个服务实例的健康状态。
-
客户端策略:
- 延迟显示错误:客户端可以增加一个较长的超时时间,在此时间内,如果没有收到服务端的响应,可以认为服务端出现了问题。但在此时间内,用户界面可以显示一个“处理中”或“稍等”的提示。
- 重试机制:客户端在收到错误响应或超时后,可以尝试再次发送请求,但要注意不要无限次地重试,以避免资源耗尽或不必要的网络流量。
- 本地缓存和备份:对于某些可预测的请求,客户端可以缓存之前的结果,并在服务端无响应时返回这些缓存的数据(这是我的答案,但是面试官似乎不是很认可,它提到了put,累加之类的操作就不能缓存)。
-
使用离线策略:
- 对于某些应用,可以考虑使用离线策略,即在无法立即处理的情况下,将请求保存到本地,等到网络稳定或服务端可用时再处理。
-
友好的用户界面:
- 即使在错误发生时,也应该展示友好的提示,例如:“网络不稳定,请稍后重试”或“我们正在处理您的请求,请稍候”等,避免直接展示技术性错误信息。
-
后台处理与通知:
- 对于不需要立即响应的请求,可以让客户端提交请求后立即返回,而实际的处理则在服务端后台进行。完成后,通过通知或其他方式告诉客户端。
-
数据一致性:
- 保证服务端的数据处理逻辑具有幂等性,这样即使客户端发送了多次请求,也不会影响数据的一致性。
-
使用消息队列或事件驱动模型:
- 客户端发送的请求先存入消息队列,由服务端异步处理。客户端可以轮询或等待通知来获取结果。
在设计系统时,应该从用户体验的角度出发,尽量减少错误的展示,但同时也要确保系统的稳定性和数据的准确性。
4.10 重试是一种机制,但是会导致响应变长呢?,了解注册和反注册嘛?服务端下线后会反注册,向注册中心注销对应的服务,然后摘流就好了,不让客户端的请求进来就好了
答:对,确实
4.11 本地注册中心是个什么操作?
答:相当于本地缓存,因为远程注册中心可能会宕机,本地注册中心还能正常工作一段时间,毕竟微服务实例变化的不是那么频繁,这段时间足够远程注册中心灾备恢复了
5 场景设计题目-线程池
详细的面经和设计可以参考:深度思考线程池面经
5.1 实现一个线程池,效果是这样的:当流量大的时候,线程数要多一点,当流量小的时候,缩的小一些,如果说线程数过多,线程池又不要炸掉,然后客户端可以阻塞,并且要完全站在用户的角度去考虑(没有答好)
我的回答(面试官做出了肯定):可以加一个定时探测机制,探测流量的大小,如果流量大就将允许的最大线程数增加,如果流量小,将最大线程数设置的和核心线程数一样大,cpu核心的两倍就行,但是这样回答其实太简单了
详细的回答:
主要是动态改变maxPoolSize的粒度上的区别,其他的几乎没变:
1 首先需要一个有界队列,同时设置一个corePoolSize和一个maxPoolSize参数,分别表示核心线程数以及支持的最大线程数,corePoolSize设置为OS支持的核心线程数+1,根据内存允许的情况设置maxPoolSize参数;另外会为每一个非核心线程设置一个超时时间,当执行完任务后的一段时间没有被分配新任务就自我注销;
2 主线程一般负责将任务分发给线程池中的线程或者添加任务到阻塞队列,同时我还给主线程一个任务那就是每隔一段时间监测当前的流量情况,如果当前的maxPoolSize设置的比较大,但是发现流量在明显降低,则粗粒度的减少maxPoolSize的值;如果发现当前的流量比较小,但是流量越来越大,则会一次性粗粒度的加大maxPoolSize,直到maxPoolSize达到一个系统内存的临界值,这里的检测机制是不断判断当前的等待队列是否已满,并且总线程数是否快要到达maxPoolSize,如果都是则扩容;如果等待队列都没满甚至为空,则可以慢慢减少maxPoolSize。
3 当maxPoolSize加大到系统的临界值并且阻塞队列已经满时,不会执行拒绝策略,主线程不再分配任务给本地的线程池,而是先放入到消息队列中,等待本地的队列中的任务执行了大部分时才将其从消息队列中预取一部分到本地队列。
6 算法题
6.1 " word1 word2 word3 “变成"word3 word2 word1”
思路:先使用trim()函数去除掉两端的空串,然后使用split函数将他们切割成数组,在数组中使用首尾双指针,同时向中间靠拢,同时分别交换它们,直到相遇
6.2 你这种的话,如果这个字符串有一个g,会同时占用2个g的内存,数组一个g,输入字符串一个g; 另一个问题是对于1g大的字符串进行split的时候,能不能run也是一个问题,就算run了,是不是多了一次处理时间为n的复杂度
改进:可以使用stingBuilder,从字符串尾部开始遍历,每次碰到一个单词就将其顺序加入到stingBuilder中,这样的话就不会立即占用两个g的内存了,而且只需要处理一次
7 反问
7.1 数据引擎主要是用java写的嘛,写什么呢
mq,flink都有
7.2 流量级别呢?
亿级的tps,一秒一个亿