RPC学习记录(二)-RPC框架原理

架构设计,就是从顶层角度出发,厘清各模块组件之间数据交互的流程,让我们对系统有一个整体的宏观认识。

传输模块:封装一个单独的数据传输模块用来收发二进制数据
协议封装:序列化与反序列化过程,以及为减少所需传输的数据量和数据包拆分次数而引入的数据压缩。
集群模块: 针对同一个接口有着多个服务提供者,但这多个服务提供者对于我们的调用方来说是透明的,所以在 RPC 里面我们还需要给调用方找到所有的服务提供方,并需要在 RPC 里面维护好接口跟服务提供者地址的关系,这样调用方在发起请求的时候才能快速地找到对应的接收地址,这就是我们常说的“服务发现”,、维护TCP连接状态的连接管理器、和集群内部协同的负载均衡策略和服务治理功能。
常见的RPC架构就如下图所示(grpc为例):
在这里插入图片描述
在 Java 里面,JDK 有自带的 SPI(Service Provider Interface)服务发现机制,它可以动态地为某个接口寻找服务实现。但是它不能按需加载,ServiceLoader 加载某个接口实现类的时候,会遍历全部获取,也就是接口的实现类得全部载入并实例化一遍,会造成不必要的浪费。另外就是扩展如果依赖其它的扩展,那就做不到自动注入和装配,这就很难和其他框架集成。

RPC框架支持插件功能,能够很好的满足业务迭代带来架构扩展问题:
在这里插入图片描述

服务发现

在生产环境中服务提供方都是以集群的方式对外提供服务,集群中某些节点的IP可能随时发生变化,我们需要一个类似“DNS” 这样的服务来实时获取到对应的服务 节点,这个过程被称为服务发现。
在这里插入图片描述

  • 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
  • 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。

使用DNS做服务发现

在这里插入图片描述

虽然这个过程很像互联网中的DNS的服务,但是平时业务过程中我们并不会使用DNS来实现“服务发现”,因为DNS存在着多级缓存机制,而且一般情况下配置的缓存时间较长,所以说服务调用者无法及时感知到服务节点的变化。而且新上线的服务节点也无法及时收到流量。

使用DNS和负载均衡做服务发现

如果使用服务均衡和DNS相配合的方案的话,服务调用的时候,服务调用方就可以直接跟负载均衡的节点建立连接,有该节点来完成TCP的转发,如下图所示。
在这里插入图片描述
该方案虽然解决了DNS上遇到的一些问题,但是还有有很多不合适的地方;

  • 由于引入了负载均衡设备,增加了系统的复杂图,需要额外的成本
  • 所有的服务调用的请求流量都要经过负载均衡节点的转发,多了一次网络船速,浪费了一些额外的性能。
  • 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;
  • 我们在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。

利用zookeeper的服务发现

使用以上两种解决方案都会存在一定的弊端,但是可以用使用类似的ZOOKeeper、etcd等实现。以zookeeper为例,可以搭建一个Zookeeper集群作为注册中心,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务发现功能,但是该方案随着微服务化程度越来越高之后,ZooKeeper 集群整体压力也越来越高,尤其在集中上线的时候越发明显。当时有超大批量的服务节点在同时发起注册操作,ZooKeeper 集群的 CPU 突然飙升,导致 ZooKeeper 集群不能工作了,而且我们当时也无法立马将 ZooKeeper 集群重新启动,一直到 ZooKeeper 集群恢复后业务才能继续上线。

主要是由于ZooKeeper 的一大特点就是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。它要求保证每个节点的数据能够实时的完全一致,这也就直接导致了 ZooKeeper 集群性能上的下降,当连接到 ZooKeeper 的节点数量特别多,对 ZooKeeper 读写特别频繁,且 ZooKeeper 存储的目录达到一定数量的时候,ZooKeeper 将不再稳定,CPU 持续升高,最终宕机。而宕机之后,由于各业务的节点还在持续发送读写请求,刚一启动,ZooKeeper 就因无法承受瞬间的读写压力,马上宕机。

在日常业务中服务调用方是可以容忍在一段时间之后(比如几秒钟之后)发现这个新上线的节点的,所以我们可以牺牲掉 CP(强制一致性),而选择 AP(最终一致),来换取整个注册中心集群的性能和稳定性。所以我们就可以放弃像zookeeper这种强一致性的更新机制。

基于消息总线的最终一致性的注册中心

因为要求最终一致性,我们可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服务下发,从而达到注册中心间数据最终一致性
在这里插入图片描述

  • 当有服务上线,注册中心节点收到注册请求,服务列表数据发生变化,会生成一个消息,推送给消息总线,每个消息都有整体递增的版本。
  • 消息总线会主动推送消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到消息的在消息回放模块里面回放,只接受大于本地版本号的消息,小于本地版本号的消息直接丢弃,从而实现最终一致性。
  • 消费者订阅可以从注册中心内存拿到指定接口的全部服务实例,并缓存到消费者的内存里面。
  • 采用推拉模式,消费者可以及时地拿到服务实例增量变化情况,并和内存中的缓存数据进行合并。

节点的健康检测—容错策略

为了保证调用方的请求成功,我们就需要确保每次选择出来的 IP 对应的连接是健康的,如何达到这种目的:主要是要让调用方能够实时感知到节点的状态变化。

在业务上常用的检测方法就是用心跳机制,正常的节点状态有以下三种:

  • 健康状态:建立连接成功,并且心跳探活也一直成功;
  • 亚健康状态:建立连接成功,但是心跳请求连续失败;
  • 死亡状态:建立连接失败。
    在这里插入图片描述

在正常的业务场景中调用方跟服务节点之间网络状况瞬息万变,出现网络波动的时候会导致误判。第二,在负载高情况,服务端来不及处理心跳请求,由于心跳时间很短,会导致调用方很快触发连续心跳失败而造成断开连接。所以由于业务的复杂性所以单纯的以心跳状态并不能完全解决这个问题。

可以考虑将心跳机制和业务请求相结合。详细来说就是将一个时间窗口内接口调用成功次数的百分比(成功次数 / 总调用次数)即接口可用率,当可用率低于某个比例就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了高低频的调用接口,也兼顾了接口响应时间不同的问题。对心跳机制来说一开始初始化的时候,如果建立连接成功,那就是健康状态,否则就是死亡状态。这里没有亚健康这样的中间态。紧接着,如果健康状态的节点连续出现几次不能响应心跳请求的情况,那就会被标记为亚健康状态。将心跳机制和业务接口可用率结合的这种情况下可以在一定程度上解决现实业务中的一些分布式节点的健康检测问题。

路由策略

真实环境中我们的服务提供方是以一个集群的方式提供服务,这对于服务调用方来说,就是一个接口会有多个服务提供方同时提供服务,所以我们的 RPC 在每次发起请求的时候,都需要从多个服务提供方节点里面选择一个用于发请求的节点。但是在上线应用的时候,会涉及到节点的变更,这种情况下可能会导致原本正常运行的程序出现异常。

所以为了减少这种风险,我们一般会选择灰度发布,即先发布少量的实例观察是否出现异常,然后根据观察的情况,选择发布更多实例还是回滚已经上线的实例。
为了尽量减小上线出问题导致业务受损的范围。我们可以在上线完成后,先让一小部分调用方请求过来进行逻辑验证,待没问题后再接入其他调用方,从而实现流量隔离的效果。

如果使用注册中心来实现流量隔离的话,即注册中心只会把刚上线的服务 IP 地址推送到选择指定的调用方,而其他调用方是不能通过服务发现拿到这个 IP 地址的。虽然可以实现,但是需要对注册中心进行二次开发,而且实现过后这种复杂的计算逻辑会导致注册中压力很大。

其实可以在服务提供方的节点前面加一层“筛选逻辑”,把符合要求的节点给筛选出来,就像我们常说的负载均衡。比如说我们要求新上线的节点只允许某个 IP 可以调用,那我们的注册中心会把这条规则下发到服务调用方。在调用方收到规则后,在选择具体要发请求的节点前,会先通过筛选规则过滤节点集合,如果是我们的目标调用方的话,最后会过滤出一个我们新上线的节点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值