-
- 3.2.1 程序启动可能带来的问题
-
3.2.2 启动预热
-
3.2.3 延迟暴露
-
3.3 优雅关闭
-
- 3.3.1 硬关闭的问题
-
3.3.2 关闭流程
-
3.3.3 优雅关闭
-
3.4 熔断限流
-
- 3.4.1 服务端自我保护
-
3.4.2 调用端的自我保护
===========================================================================
RPC 本质上是一个远程调用,那肯定就需要通过网络来传输数据,所以采用TCP协议和HTTP协议,这两个模块共同构成了传输层。
请求是调用了远程方法,方法出入参数都是对象数据,我们需要提前把对象转成可传输的二进制,即序列化过程。我们还需要在二进制数据里适当位置增加分隔符号来分隔出不同的请求,所以我们可以把这两个处理过程放在架构中的同一个模块,统称为协议模块。
除此之外,还要在协议模块中加入压缩功能,因为在实际的网络传输过程中,请求数据包在数据链路层可能会因为太大而被拆分成多个数据包进行传输,为了减少被拆分的次数,从而导致整个传输过程时间太长的问题。
RPC架构的目的是让开发人员像调用本地方法一样来调用,所以需要我们在 RPC 里面把这些细节对研发人员进行屏蔽,让他们感觉不到本地调用和远程调用的区别,整体对应上面图里的入口层。
我们还要让架构支持集群功能,在 RPC 里面我们需要在 RPC 里面维护好接口跟服务提供者地址的关系,这样调用方在发起请求的时候才能快速地找到对应的接收地址,即服务发现。
此外 TCP 是有状态协议,所以我们的 RPC框架里面要有连接管理器去维护 TCP 连接的状态。
有了集群之后,提供方需要管理好这些服务了, RPC 就需要内置一些服务治理的功能,比如服务提供方权重的设置、调用授权等一些治理手段。
为了使RPC架构更灵活,便于以后功能扩展,我们需要考虑插件化架构,我们可以将每个功能点抽象成一个接口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离,并提供接口的默认实现。
这样一来,我们的设计实现了开闭原则,用户可以非常方便地通过插件扩展实现自己的功能,而且不需要修改核心功能的本身;其次就是保持了核心包的精简,依赖外部包少,这样可以有效减少开发人员引入 RPC 导致的包版本冲突问题。
=====================================================================
为了高可用,服务提供方都是以集群的方式对外提供服务,集群里面的这些 IP 随时可能变化,我们也需要用一本通讯录及时获取到对应的服务节点。
对于服务调用方和服务提供方来说,其契约就是接口,相当于“通信录”中的姓名,服务 IP 集合作为通讯录中的地址,从而可以通过接口获取服务 IP 的集合来完成服务的发现。
-
服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
-
服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。
2.1.1 基于DNS的服务发现
如果基于 DNS 来做服务发现,所有的服务提供者都配置在了同一个域名下,调用方的确可以通过 DNS 拿到随机的一个服务提供者的 IP并与之建立长连接,但由于以下两个原因,导致其并不适用于RPC架构:
-
如果某个 IP 端口下线了,服务调用者不能及时剔除掉服务节点;
-
如果对某个服务进行节点的扩容,新上线的服务节点无法及时接收到流量。
因为为了提升性能和减少 DNS 服务的压力,DNS采取了多级缓存机制,一般配置的缓存时间较长,特别是 JVM 的默认缓存是永久有效的,所以服务调用者不能及时感知到服务节点的变化。
2.1.2 基于VIP的服务发现
我们还可以在上面的加一个负载均衡设备,通过 DNS 拿到负载均衡的 IP。这样服务调用的时候,服务调用方就可以直接跟 VIP建立连接,然后由 VIP 机器完成 TCP 转发:
但也由于以下四点导致其并不适用于RPC框架:
-
搭建负载均衡设备需求额外成本;
-
请求流量都经过负载均衡设备,多一次网络传输会额外性能;
-
负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和延迟;
-
服务治理需要更灵活的负载均衡策略,目前的负载均衡设备的算法无法满足。
2.1.3 基于 ZooKeeper 的服务发现
搭建一个 ZooKeeper 集群作为注册中心,服务注册的时候只需要服务节点向 ZooKeeper 写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能
-
服务端管理平台先在 ZooKeeper 中创建一个服务根路径,可以根据接口名命名(例如:/service/com.demo.xxService),在这个路径下再创建服务提供方目录与服务调用方目录(例如:provider、consumer),分别存储服务端和调用方的信息。
-
当服务端发起注册时,会在服务端目录中创建一个临时节点,节点中存储该服务端的注册信息。
-
当调用端发起订阅时,在调用端目录中创建一个临时节点,节点中存储调用端信息,同时调用端 watch 该服务的服务端目录(/service/com.demo.xxService/provider)中所有的服务节点数据。
-
当服务端目录下有节点数据发生变更时,ZooKeeper 就会通知给发起订阅的调用端。
2.1.4 基于消息总线的最终一致性的注册中心
ZooKeeper 的一个特点就是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同步更新,这也导致了 ZooKeeper 集群性能上的下降。
而在服务节点刚上线时,调用端是可以容忍在一段时间之后才发现这个新上线的节点的,所以我们可以牺牲掉强制一致性(CP),而选择最终一致性 (AP)来换取整个注册中心集群的性能和稳定性。
所以可以考虑采用消息总线机制。注册数据全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服务下发,从而达到注册中心间数据最终一致性。
当有服务上线,就会生成一个消息,推送给消息总线,每个消息都有整体递增的版本。
消息总线会主动推送消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到的消息在消息回放模块里面回放,只接受大于本地版本号的消息,实现最终一致性。
采用推拉模式,消费者可以及时地拿到服务实例增量变化情况,并和内存中的缓存数据进行合并。
如果目标节点已经下线或停止服务,可以在目标节点里面进行校验,如果指定接口服务不存在或正在下线,则会拒绝该请求并安全重试到其它节点。
2.2.1 健康检测的逻辑
业内常用的检测方法就是用心跳机制,调用端每隔一段时间就询问一下服务端的健康状态,一般会有三种情况:
-
健康状态:建立连接成功,并且心跳探活也一直成功;
-
亚健康状态:建立连接成功,但是心跳请求连续失败;
-
死亡状态:建立连接失败。
上面的三种状态会随着心跳的结果来动态变化
-
初始化时,如果建立连接成功,那就是健康状态,否则就是死亡状态;
-
如果健康状态的节点连续出现几次不能响应心跳请求的情况,那就会被标记为亚健康状态;
-
亚健康状态时,如果连续几次都能正常响应心跳请求,那就可以转回健康状态;
-
死亡的节点能够重连成功,那它就可以重新被标记为健康状态。
调用端每次发请求的时候,就可以优先从健康列表里面选择一个节点,如果健康列表为空,为了提高可用性,也可以尝试从亚健康列表里面选择一个。
2.2.2 可用率
-
调用方每个接口的调用频次不一样,有的接口可能 1 秒内调用上百次,有的接口可能半个小时才会调用一次,所以我们不能把简单的把总失败的次数当作判断条件。
-
服务的接口响应时间也是不一样的,有的接口可能 1ms,有的接口可能是 10s,所以我们也不能把 TPS 至来当作判断条件。
于是我们可以采用可用率这个参数来做为健康监测的指标,计算方式是某一个时间窗口内接口调用成功次数的百分比。当可用率低于某个比例就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了不同低频的调用接口,也兼顾了接口响应时间不同的问题。
2.2.3 分布式部署
因为检测程序所在的机器和目标机器之间的网络可能还会出现故障,就会产生误判。
解决方法就是把检测程序部署在多个机器里面,分布在不同的机器上。因为网络同时故障的概率非常低,所以只要任意一个检测程序实例访问目标机器正常,就可以说明该目标机器正常。
2.3.1 路由策略的意义
在真实环境中的服务端是以一个集群的方式提供服务,每次上线应用的时候都不止一台服务器会运行实例,那上线就涉及到变更,只要变更就可能导致原本正常运行的程序出现异常,为了减少这种风险,我们一般会选择灰度发布我们的应用实例。
但线上一旦出现问题,影响范围还是挺大的。因为对于服务提供方来说,服务会同时提供给很多调用方来调用,一旦刚上线的实例有问题了,那将会导致所有的调用方业务都会受损。
但路由策略就可以减少上线变更导致的风险。
2.3.2 路由策略的实现
当我们选择要灰度验证功能的时候,注册中心只会把刚上线的服务 IP 地址推送到选择指定的调用方,而其他调用方是不能通过服务发现拿到这个 IP 地址的。
在 RPC 发起真实请求的时候,有一个步骤就是从服务提供方节点集合里面选择一个合适的节点(负载均衡),可以在负载均衡前加上筛选逻辑把符合要求的节点筛选出来。
例如:我们要求新上线的节点只允许某个 IP 可以调用,那我们的注册中心会把这条规则下发到服务调用方。在调用方收到规则后,在选择具体要发请求的节点前,会先通过筛选规则过滤节点集合,最后会过滤出新上线的节点。
上面例子里面的路由策略是我们常见的 IP 路由策略,用于限制可以调用服务提供方的 IP。
2.3.3 参数路由
有些场景下,我们可能还需要更细粒度的路由方式。
例如:在升级改造应用的时候,为了保证调用方能平滑地切调用我们的新应用逻辑,在升级过程中我们常用的方式是让新老应用并行运行一段时间,然后通过切流量百分比的方式,慢慢增大新应用承接的流量,直到新应用承担了 100% 且运行一段时间后才能去下线老应用。
为了保证整个流程的完整性,我们必须保证某个主题对象的所有请求都使用同一种应用来承接。假设我们改造的是商品应用,主题对象是商品 ID
,在切流量的过程中,我们必须保证某个商品的所有操作都是用新应用(或者老应用)来完成所有请求的响应。
我们可以给所有的服务提供方节点都打上标签,用来区分新老应用节点。在服务调用方发生请求的时候,可以很容易地拿到商品 ID
,我们可以根据注册中心下发的规则来判断当前请求是用新应用还是旧应用。
相比 IP 路由,参数路由支持的灰度粒度更小,他为服务提供方应用提供了另外一个服务治理的手段。
2.4.1 RPC中的负载均衡
RPC 实现的负载均衡所采用的策略与传统的 Web 服务实现负载均衡有所不同。
RPC 的服务调用者会与“注册中心”下发的所有服务节点建立长连接,在每次发起 RPC 调用时,服务调用者都会通过配置的负载均衡插件,自主选择一个服务节点,发起 RPC 调用请求。
由于负载均衡机制完全是由 RPC 框架自身实现的,所以它不再需要依赖任何负载均衡设备,自然也不会发生负载均衡设备的单点问题
2.4.2 自适应负载均衡实现
只要调用端知道每个服务节点处理请求的能力,再根据此来判断要打给它多少流量就可以了。
调用端如何判定一个服务节点的处理能力?
可以采用一种打分的策略,服务调用者收集与之建立长连接的每个服务节点的指标数据,如服务节点的负载指标、CPU 核数、内存大小、请求处理的耗时指标、服务节点的状态指标。通过这些指标,计算出一个分数,比如总分 10 分,如果 CPU 负载达到 70%,就减它 3 分。
如何根据这些指标来打分?
可以为每个指标都设置一个指标权重占比,然后再根据这些指标数据,计算分数。
如何根据分数去控制给每个服务节点发送多少流量?
可以配合随机权重的负载均衡策略去控制,通过最终的指标分数修改服务节点最终的权重。例如给一个服务节点综合打分是 8 分,满分 10 分,服务节点的权重是 100,那么计算后最终权重就是 80(100*80%)。
关键步骤:
-
添加服务指标收集器插件,默认有运行时状态指标收集器、请求耗时指标收集器。
-
运行时状态指标收集器收集服务节点 CPU 核数、CPU 负载以及内存等指标,在调用端与服务端的心跳数据中获取。
-
请求耗时指标收集器收集请求耗时数据,如平均耗时、TP99、TP999 等。
-
可以配置开启哪些指标收集器,并设置这些参考指标的指标权重,再根据指标数据和指标权重来综合打分。
-
通过服务节点的综合打分与节点的权重,最终计算出节点的最终权重,之后调用端会根据策略来选择服务节点。
==========================================================================
3.1.1 RPC 框架的重试机制
调用端在发起 RPC调用时,会经过负载均衡选择一个节点,发送请求。当消息发送失败或收到异常消息时,就可以捕获异常,根据异常触发重试,重新通过负载均衡选择一个节点发送请求消息,并且记录请求的重试次数,当重试次数达到阈值时,就返回给调用端动态代理一个失败异常。
我们要在触发重试之前对捕获的异常进行判定,只有符合重试条件的异常才能触发重试,比如网络超时异常、网络连接异常等等。
注意:我们要确保被调用的服务的业务逻辑是幂等的,这样我们才能考虑根据事件情况开启RPC 框架的异常重试功能。
3.2.1 程序启动可能带来的问题
运行了一段时间后的应用,执行速度会比刚启动的应用更快。
因为在 Java 程序运行过程中,JVM 虚拟机会把高频的代码编译成机器码,被加载过的类也会被缓存到 JVM 缓存中,再次使用的时候不会触发临时加载,从而提升执行速度。
但是这些临时数据在应用重启后就消失了。如果让刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,造成损害。
3.2.2 启动预热
在微服务架构里面,上线肯定是频繁发生的,可以通过某些方法,让应用一开始只承接少许流量,这样低功率运行一段时间后,再逐渐提升至最佳状态。
RPC中实现思路:
要控制调用方发送到服务提供方的流量,可以让负载均衡在选择连接的时候,区分一下是否是刚启动不久的应用,对于刚启动的应用,我们可以让它被选择到的概率特别低,但这个概率会随着时间的推移慢慢变大,从而实现一个动态增加流量的过程。
具体实现:
首先对于调用方来说,我们要知道服务提供方启动的时间,这个问题有两种总解决方案:
-
服务提供方在启动的时候,把自己启动的时间告诉注册中心;
-
注册中心收到的服务提供方的请求注册时间。
我们需要把这个时间作用在负载均衡上。基于权重的负载均衡,但是这个权重是由服务提供方设置的,属于一个固定状态。现在我们要让这个权重变成动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值,整个过程如下图所示:
这样一来,当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
3.2.3 延迟暴露
启动预热更多是从调用方的角度出发,去解决服务提供方应用冷启动的问题,让调用方的请求量通过一个时间窗口过渡,慢慢达到一个正常水平,从而实现平滑上线。服务端的相关方案就是延迟暴露。
以 Spring 应用启动为例,在加载的过程中,Spring 容器会顺序加载 Spring Bean,如果某个 Bean 是RPC 服务的话,我们不光要把它注册到 SpringBeanFactory 里面去,还要把这个 Bean对应的接口注册到注册中心。注册中心在收到新上线的服务提供方地址的时候,会把这个地址推送到调用方应用内存中;当调用方收到这个服务提供方地址的时候,就会去建立连接发请求。
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。**
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-7QRCnsKn-1715571176510)]
[外链图片转存中…(img-d7thAmQq-1715571176511)]
[外链图片转存中…(img-SgLelUBV-1715571176511)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!