前语
我们好,我是「周三不Coding」。
许多公司秋招提前批现已启动啦,相信小伙伴们一定在张狂地备战秋招。
或许许多同学都现已做过 RPC 项目啦,可是担心到底能不能将其写到简历上,担心 RPC 是烂大街的项目。
其实,我在面试之前,也犹豫过。可是面试的成果却出人意料,除了面试腾讯时没有问过我 RPC 相关的问题,其它厂几乎上来第一个问题便是:说一说你的 RPC 项目,惊人的一致。这就导致每次面试开端,我都会滔滔不绝的说 10 分钟左右关于 RPC 的那套东西。
所以,我想送给我们一句话:
在面试中,「RPC」并没有想象中的 “不堪”,关键在于熟练把握其关键,做到应对自若,便是加分点。
接下来,咱们一起结合实际的面试题,看看怎样做到 「应对自若」!
RPC 模仿面试
此时省掉一番毛遂自荐和问寒问暖,面试官开端夺命十八连问~
面试官:小T同学,你先说说你的 RPC 项目是怎样完成的?
小T:(居然一上来就问这么硬的,还好我早有预备)
小T:我先给您说说我这个 RPC 项目的中心原理和组件吧,请看这张图:
RPC 全称为 Remote Procedure Call,意思为长途调用,而且这个过程就像调用本地办法一样简略!咱们不需求关注底层的网络传输细节,只需求依照调用本地办法的流程去调用长途办法即可。
通常,一个 RPC 结构有这么几个中心组件:
- Server
- Client
- Server Stub
- Client Stub
Server 和 Client 比较简略,便是惯例含义的服务端和客户端,而在 RPC 中,又引入了一个新的概念 「Stub」。
它起到的效果其实便是署理,处理一些琐碎的作业:
- 关于 Client Stub,它首要是将客户端的恳求参数、恳求服务地址、恳求服务称号做一个封装,并发送给 Server Stub
- 关于 Server Stub,它首要用于接纳 Client Stub 发送的数据并解析,去调用 Server 端的本地办法
以上便是 RPC 的中心啦 ……
(面试官突然打断)
面试官:了解,但这些内容属于比较基础的 RPC,实际运用场景中的 RPC 远不止这么简略,你的 RPC 结构还有其它规划吗?
小T:(我正预备说呢!)
是这样的,接下来我给您看看我这个 RPC 项目的层次结构吧!(好戏还在后面)
首先,我讲一讲署理层。它其实对应到我之前说到的:依照调用本地办法的流程去调用长途办法。这一功用便是经过署理层来加以完成。经过运用署理形式,咱们能够屏蔽长途办法的调用细节,如:网络衔接建立、序列化、发送恳求数据、获取回来成果、解析成果等一系列操作。
关于调用者、结构运用者来说,他们只需求直接调用长途办法即可,杂乱的逻辑都封装在 RPC 结构中处理。
我顺便说一下,除了屏蔽调用细节,署理层的其他长处吧:
- 署理层能够扩展方针目标的功用
- 署理层能够与客户端进行解耦,提高体系的可扩展性
(又被面试官打断)
面试官:那你的署理层是怎样转发恳求的呢?在微服务分布式场景下,是有许多服务的,一个服务也或许对应多个实例,你是怎样处理的?
小T:(这不便是注册中心需求处理的作业吗?简略!)
这正是我预备说的「注册中心层」。假如只运用署理层的话,是很难处理您所说的这种状况的,因为咱们需求考虑怎样记载很多的服务的地址信息,并在某个服务上下线时,告诉其他服务。若这个时分单纯运用署理层去办理这些琐碎的作业,就会形成代码杂乱度、耦合度上升,不易于扩展与维护。
因而,我在 RPC 结构中笼统出了「注册中心层」,专门用于处理服务注册、服务信息查找、服务上下线告诉。
更具体地来说,负责以下三类事项:
- 服务发现:客户端需求订阅注册中心。在需求长途调用时,从注册中心中获取信息,然后进行办法调用
- 服务注册:服务供给者将地址、接口、分组等信息存放在注册中心模块,当服务上线、下线均会告诉注册中心
- 服务办理:供给服务的上下线办理、服务装备办理、服务健康检查等功用,以保证服务的可靠性和安稳性
就像我图中所画的一般:
面试官:那你的注册中心具体是怎样完成的?你是手写了一个注册中心组件吗?
小T:(糟了,他不会认为我是手写的吧!我得迂回一下!)
呃,并没有,因为正如之前所说,这儿触及到了数据存储、事情监听机制、心跳机制等多个杂乱的作业,而市面上恰好有满意这些特性的开源组件,考虑到整体项目的进度以及手写的杂乱程度,最终我仍是挑选了开源的处理方案。
面试官:那你是怎样做挑选的?换句话说,你之前有仔细了解过这些开源组件吗?
小T:(就知道会问这个,好在我早有预备~)
在谈怎样挑选注册中心之前,请让我先简略介绍一下 CAP 理论哈~因为之后我会依据 CAP 理论挑选注册中心!
CAP 理论是分布式体系中的重中之重!
敲黑板!注册中心要点来喽!
CAP 是 Consistency(一致性) 、Availability(可用性) 、Partition Tolerance(分区容错性) 这三个单词首字母组合。
一致性(Consistency) : 一切节点拜访同一份最新的数据副本
可用性(Availability) : 非故障的节点在合理的时刻内回来合理的呼应(不是错误或许超时的呼应)。
分区容错性(Partition Tolerance) : 分布式体系呈现网络分区的时分,依然能够对外供给服务。
CAP 并不是简略的 3 选 2,因为分区容错性是有必要完成的。以分区容错性作为前提,在一致性与可用性中做挑选。
接下来,我说一下我是怎样在 Zookeeper、Nacos、Eureka、Consul 中做技能选型的!
-
Zookeeper
-
Zookeeper 经过 znode 节点来存储数据。因而咱们能够利用这一特性进行服务注册,节点用于存储服务 IP、端口、协议等信息。
- 例如:服务供给者上线时,Zookeeper 创建该节点 – /provider/{serviceName}:{ip}:{port}
-
Zookeeper 供给 Watcher 机制,能够监听相应的节点途径。因而咱们能够利用这一机制监听对应的途径,一旦途径上的数据发生了改变,咱们便向其他订阅该服务的服务发送数据变更音讯。收到音讯的服务便去更新本地缓存列表。
-
Zookeeper 供给心跳检测功用,守时向各个服务供给者发送心跳恳求,保证各个服务存活。假如服务一向未呼应,则阐明服务挂了,将该节点删去。
-
Zookeeper 遵从一致性准则,即 「CP」
- 关于注册中心而言,最重要的是可用性,咱们需求随时能够获取到服务供给者的信息,即便它或许是几分钟曾经的旧信息。
- 可是 Zookeeper 因为其中心算法是 ZAB,首要适用于分布式协调体系(分布式装备、集群办理等场景)。当 master 节点故障后,剩余节点会重新进行 leader 推举,导致在推举期间整个 Zookeeper 集群不可用。
-
-
Nacos
-
服务供给者启动时,会向 Nacos Server 注册当时服务信息,并建立心跳机制,检测服务状况。
-
服务顾客启动时,从 Nacos Server 中读取订阅服务的实例列表,缓存到本地。并敞开守时使命,每隔 10s 轮询一次服务列表并更新。
-
Nacos Server 选用 Map 保存实例信息。当装备耐久化后,该信息会被保存到数据库中。
-
关于服务健康检查,Nacos 供给了 agent 上报与服务端主动监测两种形式
-
Nacos 支撑 CP 和 AP 架构,依据 ephemeral 装备决议
- ephemeral = true,则为 AP
- ephemeral = false,则为 CP
-
-
Eureka
-
服务供给者启动时,会到 Eureka Server 去注册服务
-
服务顾客会从 Eureka Server 中守时以全量或增量的办法获取服务供给者信息,并缓存到本地
-
各个服务会每隔 30s 向 Eureka Server 发送一次心跳恳求,确认当时服务正常运行。若 90s 内 Eureka Server 未收到心跳恳求,则将对应服务节点除掉。
-
Eureka 遵从可用性准则,即「AP」。
- Eureka 为「去中心化结构」,没有 master / slave 节点之分。只要还有一个 Eureka 节点存活,就依然能够保证服务可用。可是或许会呈现数据不一致的状况,即查到的信息不是最新的。
- Eureka 节点收到恳求后,会在集群节点间进行仿制操作,仿制到其他节点中。
-
-
Consul
- 服务供给者启动时,会向 Consul Server 发送一个 Post 恳求,注册当时服务信息
- 服务顾客建议长途调用时,会向 Consul Server 发送一个 Get 恳求,获取对应服务的全部节点信息
- Consul Server 每隔 10s 会向服务供给者发送健康检查恳求,保证服务存活,并更新服务节点列表信息。
- Consul 遵从一致性准则,即「CP」
这 4 种开源组件均满意注册中心需求。在这种场景下,技能选型便是一个 Trade-off 了,咱们需求挑选一个最适合的组件!
- 关于 Consul,它底层言语是 Go,更支撑容器化场景,而当时 RPC 结构选用的是 Java 言语,所以就先淘汰啦~
- 关于 Eureka,它很适合作为注册中心,可是其维护更新频率很低,目前国内运用的人很少,所以在这儿就先不运用啦~
- 关于 Nacos,它是目前国内十分干流的一种注册中心,而且由 Alibaba 开源。
- 最终,我仍是挑选了 Zookeeper,尽管 Zookeeper 寻求一致性导致其不太适合于注册中心场景,可是国内 Dubbo 结构选用了 Zookeeper 作为注册中心,能从 Dubbo 结构中参考到许多优异的完成技巧。而且,咱们能够经过操作 Zookeeper 节点,从愈加底层的视点感触怎样完成注册服务。
(啊~总算说完了,好累)
面试官:嗯,说的很不错,看来在技能选型上做了许多功课!你最终选用了 Zookeeper,那你还知道 Zookeeper 的其它运用场景吗?
小T:(居然问这么细)除了注册中心,Zookeeper 还能够完成分布式锁、分布式 ID、装备中心等功用。
关于分布式锁:
-
Zookeeper 有一种节点为临时节点,它能够保证服务宕机后节点自动被删去,不需求额定考虑增加节点过期时刻来处理死锁问题。
-
Zookeeper 能够经过运用次序节点,满意公正锁特性。
-
Zookeper 节点加锁时,经过监听前驱节点状况,判别是否获取到锁。
- 假如监听到它的前驱节点被删去时,则相当于获取到锁;否则堵塞。
关于分布式 ID:
- Zookeeper 能够经过其次序节点,完成分布式 ID,保证分布式环境下 ID 不重复。
关于装备中心:
- 经过 znode 节点完成装备存储
- 经过 Watcher 监听节点信息是否发生改变,若发生改变,则告诉客户端更新装备信息。
面试官:把握得能够呀~那你接着说吧。
小T:之前咱们有说到,一个服务或许对应着多个实例节点,从注册中心中获取到的或许不止有一个服务地址,或许是一个地址信息 List。这时分咱们就需求凭借「路由层」,协助咱们从多个实例节点中选取一个,这便是「负载均衡」。在我的 RPC 结构中,我供给了如下 5 种负载均衡战略:
- 随机选取战略
- 轮询战略
- 加权轮询战略
- 最少活泼衔接战略
- 一致性 Hash 战略
-
关于「随机选取」战略,望文生义,即从多个节点中随机选取一个节点进行拜访。这种办法最大的长处便是简略,可是当恳求数量较少时,随机性或许不强,或许会呈现单实例节点负载过大的状况。当恳求数量很大时,每个实例节点承受的恳求数量会接近于均衡,效果较好。
-
关于「轮询」战略,即轮转调度。假设当时服务有 3 个实例节点,第一次恳求发送给 A 节点,第2次恳求发送给 B 节点,第三次恳求发送给 C 节点,那么第四次恳求就会再次发送给 A 节点,完成均衡恳求的效果。
-
关于「加权轮询」战略,是为了处理「轮询」战略所面对的问题。试想一种场景,在当时服务的集群中,有的实例节点装备较高,内存大且多核处理器,那么它就能够承载更多恳求。有的实例装备低,那么它的承载能力就会弱一些。这时分「轮询」战略不足以满意这一运用场景。
因而,咱们需求考虑为每个实例节点设置权重,使权严重装备高的节点处理更多的恳求,这便是「加权轮询」战略。
-
关于「最少活泼衔接」战略,是为了处理以上战略所面对的一起问题。咱们再试想一种场景,某些恳求的处理时刻更长,比方拉取用户粉丝列表,关于头部博主来说,其粉丝数多,拉取时刻长,而关于普通用户来说,其粉丝数少,很快就能够拉取结束。这就导致拉取一个大 V 粉丝列表的时刻远善于拉取 100 个普通用户粉丝列表的时刻。
这时分假如仍是依照「轮询」战略,会导致 A 节点即便收到的恳求比 B 节点少,但却超越所能承受的最大负载。
而「最少活泼衔接」战略的意思是选取当时活泼恳求最少的服务节点。
因而,在这种场景下,更适合运用「最少活泼衔接」战略,会得到更合理的负载均衡效果。
-
关于「一致性 Hash」战略,是经过恳求中带着的参数来定位对应的实例节点。
比方,恳求参数中带着了用户 ID。用户 ID 为 1 ~ 10 的恳求永远对应到 A 节点,用户 ID 为 11 ~ 20 的恳求永远对应到 B 节点,顺次类推…
这便是路由层的中心效果 —— 「负载均衡」啦~
面试官:我方才听你说到一致性 Hash 战略,但好像没有说到 Hash,你能在具体说说吗?
小T:(居然听的这么细!)好嘞,或许是我漏啦~咱们能够从一致性 Hash 环的原理讲起!
咱们能够看到如图「圆环」中存在有 3 个节点,分别为 NodeA / Node B / Node C,4 个恳求,分别为 Req 1 / Req 2 / Req 3 / Req 4。
为什么叫这个「圆环」为「一致性 Hash 环」呢?这是因为咱们要对每一个节点依据 Hash 算法计算得到一个 Hash 值,并将其映射到圆环中的某一个方位。关于恳求也是如此,依据参数来计算具体的 Hash 值,也映射到圆环中对应的方位。
接下来的作业就很简略啦,每一个恳求沿着当时圆环 顺时针 寻找,找到的第一个节点便是对应的处理恳求节点。
如图对应关系为,Node A 处理 Req 3 和 Req 4,Node B 处理 Req 1,Node C 处理 Req 2。
可是一致性 Hash 环容易形成一个问题,看下面这个图就一望而知啦!
当服务节点过少时,节点 Hash 值映射到圆环的方位或许集合于某一处,容易因为节点分布不均匀而形成恳求均衡问题,即「数据歪斜」。
在图中,Node A 承当了大部分恳求,Node C 只承当了一个恳求,Node B 一个都没有。
为此,咱们需求引入「虚拟节点」处理这一问题。
图中黄色节点对应的便是「虚拟节点」,起到均衡恳求、避免数据歪斜的效果。
面试官:了解的很到位!你接着说路由层吧~
小T:(感觉稳了!)路由层除了完成中心的「负载均衡」功用之外,还承当了分配流量的效果。在 RPC 结构中,咱们能够将流量标签、实例标签、路由规矩等信息存储在恳求中,这样一来,咱们就能够随意操控流量,将恳求分配到不同的流量环境。
基于此,咱们能够完成「泳道测验」,即关于出产环境恳求,打上对应的 prod 标签,关于测验环境恳求,打上对应的 test 标签。这样就能够让大部分恳求转发到出产环境服务,而新版本测验恳求转发到测验环境服务。
面试官:时刻不多啦,你接着说下一层吧~
关于路由层的知识点,面试一般只会考到「负载均衡」算法。所认为了考虑到大部分读者快速预备面试的需求,关于路由层的高档路由部分,如:条件路由、泳道测验、灰度测验等具体内容,我会放到我的微服务专栏进行具体的讲解~
小T:好嘞!咱们接着说序列化层!因为 RPC 调用的底层是网络恳求,当咱们的恳求带着参数时,恳求发送方需求将参数进行「序列化」,从而完成在网络中传输。而恳求接纳方也需求进行「反序列化」操作,获取到可了解的参数。
- 序列化:将数据结构或目标转化为二进制字节省
- 反序列化:将在序列化过程中生成的二进制字节省通化为数据结构或目标
为了整合多种序列化结构,我笼统出了序列化层,运用工厂形式,界说了笼统工厂接口,其中有两个办法:serialize 和 deserialize
并界说相应的序列化完成类,包含如下序列化结构:
-
JDK序列化
- 经过ObjectOutputStream的writeObject和readObject办法完成,可经过重写指定其他序列化办法
- JDK默许序列化办法功能差,且只适用于Java
-
Protocol Buffer
- 支撑跨言语、跨渠道,可扩展性强
- 需求运用IDL来界说Schema描述文件,界说完描述文件后,能够直接运用protoc来直接生成序列化与反序列化代码
- 功能低于Kyro,可是高于大部分序列化协议,序列化后的size也较小
-
Kyro
- 首要适用于Java,不支撑字段扩展
- 运用简练,直接运用Input、Output目标
- 高功能,序列化与反序列化时刻开支都很低,序列化后的size也很小
-
Hessian
- 支撑跨言语、跨渠道,可扩展性强
- 易用:只需求完成Serializable接口即可
- 序列化时刻与巨细都比较小
面试官:能够的~那在数据传输过程中,或许会呈现粘包和半包问题,你是怎样处理的?
小T:(好问题,正好我温习了!)我在做之前,是有了解调研过业界的处理方案,有以下几种:
-
固定长度传输:固定好每次数据包的长度,比方规则每次传输长度为 32 个字节。当接纳方接满 32 个字节时,代表承受到了完好的信息。
- 该办法灵活性太低
-
特别字符分割:即每次接纳方读取数据时,读到事先约定好的特别字符时,代表承受到了完好的信息。
- 假如音讯内容中刚好有这一特别字符,需求提前做转义,仍是比较费事。
-
自界说音讯结构
最终,考虑到灵活性,决议自界说音讯结构,因而我笼统出了「协议层」,专门用于界说音讯收发格式。
自界说协议结构体如下:
-
MagicNumber 魔数:用于做安全检测,能够快速确认当时恳求是否合法。恳求发送方和接纳方能够提前约定好魔数。
-
ContentLength 恳求长度:协议长度。
-
Content 中心传输数据:封装恳求的接纳方称号、恳求的办法、恳求参数等内容
- 这儿的 Content 为二进制字节省,对应的便是「序列化层」序列化后得到的二进制字节省。
面试官:似乎还有「链路层」和「容错层」,你再展开说说吧~
小T:好的。我先说说为什么需求「链路层」,其首要用于处理以下两个问题:
- 对 RPC 恳求做鉴权:恳求在到达恳求接纳方之前,先校验其是否有恳求凭证(Token)
- 记载恳求过程中的调用日志信息
「链路层」的中心规划思想是职责链规划形式:
- 职责链形式能够使咱们恣意增加恳求的「前处理」和「后处理」目标,并调整处理次序,提高了维护性和可拓展性,能够依据需求新增处理类,满意开闭准则。
- 职责链简化了目标之间的衔接。每个目标只需坚持一个指向其后继者的引用,不需坚持其他一切处理者的引用,这避免了运用很多的 if 或许 ifelse 语句。
- 职责分管,职责别离。每个类只需求处理自己该处理的作业,不应处理的传递给下一个目标完成,清晰各类的职责规模,契合类的单一职责准则。
关于「容错层」,我首要是为了完成服务安稳性管理,保证服务的高可用性。首要经过以下几种手法完成容错层:
-
超时重试机制
- 恳求一般能够分为「幂等」与「非幂等」恳求,幂等性指的是屡次恳求某一个资源,最终的成果相同,对体系发生的影响相同。
- 在恳求重试时,咱们需求额定考虑「非幂等恳求」重试所带来的风险(比方转账、下单等触及资金事务场景),当恳求超时时,很难判别数据包是否现已到达服务接纳方。因而,在恳求参数中增加 retry 参数,重试次数由用户自行决议。当 retry = 0 时,则代表恳求失败不进行重试。
-
服务限流
- 在高并发场景下,经过限制瞬发 QPS 最大值,从而防止体系被流量击退,最大极限保证服务高可用。
-
服务熔断
- 在调用过程中,或许呈现 Bug、故障等问题。为了防止因为此类问题导致故障在调用链路中扩散,引起「链路雪崩」,咱们挑选触发「服务熔断」,直接抛出反常,抛弃持续调用下流服务,最大程度维护其他服务。
面试官:不错,说得很好。可是你的 RPC 结构最终有做压力测验吗?一切结构最终都得运用呀,一定要能投入到出产环境运用。
小T:(这也要问吗?还好我做了这一步!)您说的没错,我也有考虑到这一点。在写完代码后,我做了一次压力测验:
我经过设置连续恳求次数为 100 / 1000 / 10000,对结构进行压力测验,发现随着恳求次数梯度上升,整体接口的呼应速度和成果并没有发生改变,阐明结构安稳。
面试官:好的。最终我再问一个问题,你觉得你的 RPC 结构还有什么规划亮眼的地方?
小T:(啊,总算快要问完了)我在结构中屡次运用到了「异步规划」,对各个操作进行解耦。
-
关于服务端:
当恳求抵达服务器时,将其直接丢入事务堵塞行列中,然后开辟一个新的线程,从堵塞行列中循环获取Handler恳求使命。
将获取到的使命目标交付于事务线程池进行消费处理。
-
关于客户端:
署理层在发送完恳求之后,不需求同步堵塞等待呼应成果。成果的回来为异步。
而且用户能够经过装备文件的办法,自行挑选异步或同步。
面试官:咦,你说到了你有用线程池技能,那么你是怎样挑选线程数的呀?
小T:(什么?不是最终一个问题吗?怎样还有?)
通用的挑选办法是依据线程池处理使命的类型进行挑选:
-
假如是CPU密集型使命,如:加密、解密、压缩、计算,应该依据当时服务器CPU中心数进行挑选,最好是CPU中心数的1~2倍
-
假如是IO密集型使命,如:数据库、网络传输、文件读写,应该尽或许提高线程数
-
公式为:线程数 = CPU 中心数 *(1+均匀等待时刻/均匀作业时刻)
- 均匀等待时刻越长,阐明是IO密集型,需求增大线程数
- 均匀作业时刻越长,阐明是CPU密集型,需求减少线程数
关于我这个结构来说,大部分都是 IO 密集型使命,因而我调大了线程数。
面试官:好的,能够看出 小T同学 关于 RPC 规划把握得的确不错。行吧,回去等告诉吧~
小T:(????????????)
总结
总算写完了,洋洋洒洒将近 1w 字。
总结一下全文:咱们从面试的视点出发,将 RPC 结构拆分了多个层次,逐层剖析 RPC 结构的具体完成原理,根本涵盖了 RPC 结构常考的知识点!
假如我们觉得部分知识点不够细致,我会在后续的文章中持续弥补。
我们感兴趣的话请关注我的微服务专栏,我会在这儿对路由层、服务管理等内容进行具体的弥补。
个人认为应付面试应该满足运用啦。
此外,我还想再提一下文章最初的那句话:在面试中,「RPC」并没有想象中的 “不堪”,关键在于熟练把握其关键,做到应对自若,便是加分点。
假如能很好的把握 RPC 完成细节,在面试中绝对是大大大大大的加分点!假如你很会 RPC,放心大胆地写到简历上!
这便是本期的全部内容啦,假如我们觉得本篇文章对你有协助,费事帮忙点个赞、保藏一下呀~那么今天就到这儿啦,下期再会!