转|SOFARPC路由实现剖析

Posted by lyp on Tuesday, December 11, 2018

TOC

前言

RPC 框架需要创造一种调用远程服务如同调用本地般的体验,因此在实现一个基于 RPC 框架的微服务架构的系统时,服务消费者(客户端)往往只需要知道服务端提供了哪些接口和方法,并不需要知道服务具体由哪些 IP 在提供。RPC 框架本身的服务发现和路由寻址功能解决了如何知道目标地址的问题,该过程对于 RPC 客户端调用方来说应该是完全透明的。

在这个过程中,RPC 框架需要接入注册中心来完成服务发现和路由寻址的功能。同时,在应用大规模请求时,微服务系统还需要对请求服务集群化,同时通过负载均衡来达到降低访问压力的效果。

本文我们会先介绍一下注册中心,然后介绍一下 SOFRPC 中的几种路由,最后会介绍一下负载均衡的几种比较。

注册中心支持

首先我们简要介绍一下注册中心的原理。

dsr.png | center | 747x435

服务端推送地址给注册中心,注册中心将地址进行合并后,推送给客户端。

其中,注册中心的场景依赖于各类注册中心的实现。在这里,SOFARPC 提供了注册中心的抽象类 Registry,该抽象类提供了注册中心的配置、启动、注册、反注册、订阅等方法。客户端在接入过程中,可以通过配置来激活 Zookeeper、Consul、local 等注册中心注册进启动类中,当请求到来时,可以通过注册中心进行相应的路由。

注册中心的抽象类如下:

carbon (3).png | center | 747x1142

在这个接口的基础上,目前内置实现了几种注册中心。包括即将合并的。

registry.png | center | 747x212

Local注册中心(Local)

Local 注册中心是 SOFARPC 自己实现的一个本地注册中心,该注册中心的实现主要由类 LocalRegistry提供,可以调用其 register(ProviderConfig config) 方法实现服务的注册,主要是文件的读写。

实现原理很简单,通过在本地注册文件来保存服务的发布和订阅信息。

Zookeeper注册中心(Zookeeper)

Zookeeper 接入 SOFARPC 的实现类为 ZookeeperRegistry。目前是 SOFARPC 中默认的注册中心实现。也是大多数情况下,可以方便使用的。

Zookeeper 是一个分布式协调服务,用于维护配置信息、命名、提供分布式同步功能以及提供组服务等。Zookeeper 提供了服务注册与发现的解决方案,提供了简单的 API,可以让集成者简洁调用。

当要发布一个 SOFARPC 服务时,首先需要在 zookeeper 中注册服务提供者的相关信息,包括:该服务接口隶属于哪个系统、服务的 IP 以及端口号、服务的请求 URL 和服务的权重等等。zookeeper 在这个过程中,注意负责对 SOFARPC 中的服务信息进行中心存储,同时负责把服务注册信息的更新及时通知到服务消费者。

作为服务调用者,SOFARPC 调用端在调用时,若走的路由链路中有注册中心,则会从注册中心中获取到服务注册的相关信息,然后在调用时会根据负载均衡策略来发送请求。

Consul注册中心(Consul)

Consul 注册中心与 SOFARPC 之间的对接主要依赖于 ConsulRegistry类。

该注册中心在功能表现上与 zookeeper 看起来一致。对比起 Zookeeper 来,Consul 支持多数据中心,同时支持 http 和 dns 等接口,有着多语言的能力。

其他注册中心

目前已经在开发中的有 Nacos,SOFAMesh 等。也可以根据自己的场景,进行方便的扩展。

路由设计

路由原理和设计

在阅读本部分之前,请大家注意:路由是为了选中一组地址。

SOFARPC 通过对各类注册中心的支持,实现了服务发现、路由寻址的功能。访问客户端时,请求的路由可以由以下一些实现类实现:DirectUrlRouter、RegistryRouter、CustomRouter,上述三个路由实现类分别对应了直接地址路由(不需要经过注册中心直接路由直连到某个地址)、注册中心路由、以及客户自定义路由等。路由从 AddressHolder 获取到地址,同时通过各种负载均衡算法进行负载均衡,请求到相应的系统接口。

首先我们看一下整个路由寻址过程的阶段。

image.png | left | 747x673

这 SOFARPC 中,路由可以分为地址直连路由、注册中心路由以及客户定制化路由。这以上三个路由均扩展了 Router 抽象类。服务路由的抽象类代码如下:

carbon (4).png | center | 747x573

这里的核心代码是 route 这个方法,将本次请求的信息,和服务列表进行计算。当客户端请求到达 Router 时,会根据请求的参数信息从 Router 和连接管理器中获取请求地址,通过调用 route(SofaRequest request, List<ProviderInfo> providerInfos) 方法达到路由寻址的目的。

其中,路由并不是一个非此即彼的过程,这些可选的路由是由用户和系统的配置,被构造成一个路由链来执行的。这样。就可以有一些兜底的逻辑,如指定了 IP 地址,那我们就直接路由到这个地址,如果没有,就进行注册中心的路由等等。

直连(DirectUrlRouter)

直接路由是比较简单的,因为有专门的配置,所以地址列表这些都是可以很方便地进行识别,在客户端配置时,可通过如下方式配置:

ConsumerConfig<HelloService> consumer = new ConsumerConfig<HelloService>()        
            .setInterfaceId(HelloService.class.getName())        
            .setRegistry(registryConfig)        
            .setDirectUrl("bolt://127.0.0.1:12201");

直接地址路由扩展了 Router 抽象类的实现,在重写的 route 方法中,直接获取配置好的直接路由地址。当请求到来时,直接从地址管理列表中,拿到对应的地址,就实现了直接地址路由的功能。

注册中心(RegistryRouter)

注册中心路由同样扩展了 Router 抽象方法,这个 Router是大多数情况下使用最多的路由,主要是从本应用使用的注册中心中获取对应的地址,并进行路由寻址等。后面我们会介绍目前注册中心的几个内置实现。

自定义(CustomRouter)

客户定制化路由可以配置客户自己所定制的路由实现,可以参考直接地址路由或者注册中心路由的实现,扩展 Router 类即可。

这里的使用场景

一种是对于某些用户来说,在注册中心的场景下,用户认为所有的地址并不是等价的。会对地址进行人为拆分,使用方保存了自己的的所有服务提供方地址(或者是通过某种方法查询),然后重写路由定制逻辑,通过方法级别进行地址的选择。

另一种是,用户可以通过这个接口实现一些功能,例如切流、灰度、同机房优先等等。

负载均衡实现

经过前面两个部分的介绍,我们知道,通过不同的注册中心和直连指定的方式,经过路由链的选择,我们已经拿到了一组地址,这一组地址如果选出一个地址进行本次的调用。就涉及到负载均衡的选择。下面我们会给大家介绍下每种负载均衡的比较和优势。

现在,SOFARPC 提供如下一些负载均衡器。下面是各个负载均衡器的类名称以及算法原理,以下各个类名可以直接在代码中进行搜索阅读。

注意:负载均衡的计算全部是在客户端实现的。

负载均衡器的抽象类下:

carbon (5).png | center | 747x470

权重随机(RandomLoadBalancer)

这个负载均衡算法是默认的实现,带权重的随机负载均衡算法。是目前默认开启的负载均衡算法。 在算法在进行负载均衡时,全部列表按照权重进行随机选择。

权重随机的思路很简单,每个服务地址,在发布的时候,带上了一个 weight 的标签,SOFARPC 在路由的时候,汇总所有的权重值,然后产生一个0到这个总权重值的随机数,看这个数是落在哪个范围内,就知道要选哪个服务端作为本次调用的地址,默认为100,当然,用户也可以指定或者通过某些操作 API 进行修改。在 SOFARPC,预热权重功能会在启动期间,使得某个刚刚启动的服务端地址的权重为一个较小的值。

对于所有权重一样的情况下。权重随机也就退化成了完全随机。 而对于权重有差异的情况下,就能实现,权重小的调用少,权重多的调用量大。

router.png | center | 747x217

这个直观理解就是一个线段掷骰子 的问题,相信大家配合代码一看就明白了。

配合预热权重功能,这个就将权重随机的功能真正使用了起来。另外,也可以通过配置覆盖,动态得修改某些服务提供方的权重信息。来实现地址摘除等。这个需要依赖于注册中心的实现。

顺序轮询(RoundRobinLoadBalancer)

进行依次轮询来进行负载均衡。主要用来调用量比较少的情况下。 该负载均衡算法在实现时,不关心权重,按照方法级进行轮询,相互不会影响。这个非常简单。就是一个环状,轮询完了就再次重新开始。

Canvas 2.png | center | 599x509

本地优先(LocalPreferenceLoadBalancer)

该负载均衡算法提升了本机的调用性能。在负载均衡时使用保持本机优先。这个相信大家也比较好理解。 在所有的可选地址中,找到本机发布的地址,然后进行调用。

一致性 Hash(ConsistentHashLoadBalancer)

一致性 hash 算法,保障了客户端和服务器之间比较稳定的连接关系。 该算法通过一致性 hash,保证了第一参数同样的请求能够负载均衡到同样的节点上。一致性 Hash 大家都了解比较多了,这里 SOFARPC 是通过方法入参的第一个参数来做负载均衡的 Hash 的。

hash.png | center | 747x554

权重一致性 Hash(WeightConsistentHashLoadBalancer)

带权重的一致性 hash 算法。在一致性 Hash的基础上,设置虚拟节点的时候,权重大的 ProviderInfo 会生成更多的节点。这样被选中的概率就更高。这里不再做详细说明。大家有兴趣可以从代码中进行阅读。

负载均衡比较

负载均衡算法优点缺点场景
权重随机快速方便调用量小的情况下不完全均衡大多数场景
顺序轮询调用完全均衡没有权重。TPS小的场景
本地优先本地优先有场景限制。本地有服务发布的场景。
一致性 Hash调用机器相对固定性能相对一般对调用机器需要相对固定的场景。
权重一致性 Hash调用机器相对固定,有权重性能相对一般对调用机器需要相对固定的场景。

总结

到这里,路由实现剖析基本就介绍完了。

在本文中,我们剖析了 SOFARPC 的路由实现,详细解释了 SOFARPC 在对路由的实现方式及相应的扩展方式。 除此之外,介绍了不同注册中心的接入方法,及接入的实现。 最后,本文介绍了一些负载均衡算法,并对这些算法进行了相应的对比。


文章转自剖析 | SOFARPC 框架】之SOFARPC 路由实现剖析