欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud 服务消费者(rest+ribbon)

程序员文章站 2022-06-23 22:33:12
...

一、Ribbon

在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http restful的。Spring Cloud有两种微服务调用方式:①ribbon+restTemplate ②feign
本篇主要是将ribbon+restTemplate

ribben这个东西比较复杂,我们先简单说一下:

Spring Cloud Ribbon 是一个基于Http和TCP的客服端负载均衡工具,它是基于Netflix Ribbon实现的。它不像服务注册中心、配置中心、API网关那样独立部署,但是它几乎存在于每个微服务的基础设施中。包括前面的提供的声明式服务调用也是基于该Ribbon实现的。

RestTemplate是Spring自己提供的对象,不是新的内容。读者不知道RestTemplate可以查看相关的文档。

二、简单实例

1.新建MAVEN项目,加入依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>test</groupId>
  <artifactId>java-maven-user</artifactId>
  <version>0.0.1-SNAPSHOT</version>

   <name>service-ribbon</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
</project>
2.配置文件

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8764
spring:
  application:
    name: service-ribbon
在工程的启动类中,通过@EnableDiscoveryClient向服务中心注册;并且向程序的ioc注入一个bean: restTemplate;并通过@LoadBalanced注解表明这个restRemplate开启负载均衡的功能。
package test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {
	 public static void main(String[] args) {
	        SpringApplication.run(ServiceRibbonApplication.class, args);
	    }

	    @Bean
	    @LoadBalanced
	    RestTemplate restTemplate() {
	        return new RestTemplate();
	    }
}
3.测试类

写一个测试类HelloService,通过之前注入ioc容器的restTemplate来消费service-hi服务的“/hi”接口,在这里我们直接用的程序名替代了具体的url地址,在ribbon中它会根据服务名来选择具体的服务实例,根据服务实例在请求的时候会用具体的url替换掉服务名,代码如下:

package test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class HelloService {
	  @Autowired
	    RestTemplate restTemplate;

	    public String hiService(String name) {
	        return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
	    }
}
写一个controller,在controller中用调用HelloService 的方法,代码如下:
package test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloControler {

	    @Autowired
	    HelloService helloService;
	    @RequestMapping(value = "/hi")
	    public String hi(@RequestParam String name){
	        return helloService.hiService(name);
	    }
}
4.发送请求

http://localhost:8764/hi?name=heidou,可以看到页面:

hi heidou,i am from port:8763

hi heidou,i am from port:8762 交替出现


三、原理分析

最基本原理;

普通使用RestTemplate请求其他服务时,内部使用的就是常规的http请求实例发送请求。
为RestTemplate增加了@LoanBalanced 注解后,实际上通过LoadBalancerInterceptor对HTTP请求进行拦截,并利用负载均衡器LoadBalancerClient将以逻辑用户名为host的URI转换为具体的服务实例地址。

再用Ribbon实现负载均衡器的时候,实际上使用的是Ribbon中定义的ILoadBalancer接口的实现,默认是ZoneAwareLoadBalancer。

1.负载均衡分类:

⑴服务端负债均衡

①硬件负债均衡,F5

②软件负债均衡,Nginx

⑵客户端负载均衡

所有客户端结点都维护者自己要访问的服务端清单。

在微服务使用客户端负载均衡很简单,只需要:

①服务提供者注册到注册中心

②服务消费者直接调用@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。

2.RestTemplate

该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced开启客户端负载均衡

几种主要的请求类型:

⑴GET请求

⑵POST请求

⑶PUT请求

⑷DELETE请求

3.源码分析

Spring Cloud 服务消费者(rest+ribbon)

⑴LoadBalancerClient接口

Spring Cloud 服务消费者(rest+ribbon)

从这个接口我们看到负载均衡器所需要具备的能力:

①根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。

②使用挑选出的实例执行请求内容

③为系统构建一个合适的host:port形式的URI

⑵LoadBalancerAutoConfiguration客户端负载均衡器的自动化配置类

先贴源码:

Spring Cloud 服务消费者(rest+ribbon)

Spring Cloud 服务消费者(rest+ribbon)

根据类头的注解我们可以看出,自动化配置需要满足两个条件:

①RestTemplate类必须存在于当前工程中

②在Spring的Bean工程中必须有LoadBalancerClient的实现Bean

在这个类中,主要做了三件事:

①创建LoadBalancerInterceptor的Bean来实现对客户端发起请时进行拦截,以实现客户端的负载均衡。

②创建RestTemplateCustomizer的Bean来给RestTemplate增加LoadBalancerInterceptor拦截器

③维护一个被@LoadBalanced注解修饰的对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加

LoadBalancerInterceptor拦截器

⑶LoadBalancerInterceptor

我们来看看LoadBalancerInterceptor拦截器如何将一个普通RestTemplate变成客户端负载均衡的

Spring Cloud 服务消费者(rest+ribbon)

从代码中可知,我们注入了LoadBalancerClient的实现

当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被intercept函数拦截,可以通过getHost()拿到服务名,

然后通过execute发起实际请求

然后我们找到LoadBalancerClient的实现Ribbon-LoadBalancerClient

Spring Cloud 服务消费者(rest+ribbon)

步骤:

①可以看到第一步就是给句serviceId获得具体的服务实例

Spring Cloud 服务消费者(rest+ribbon)

这里使用了Netfilx Ribbon自身的ILoadBalancer接口中的chooseServer函数(通过某种策略,从负载均衡器中挑选出一个具体的服务实例)

Spring Cloud 服务消费者(rest+ribbon)

SpringCloud默认采用ZoneAwareLoadBalancer来实现负载均衡器

②获取到Server之后,增加ServerId等内容对它进行包装

③调用LoadBalancerRequest的apply方法,向具体服务实例发送请求。这样就实现了从host的URI请求到host:post形式的实例访问地址的转换。

我们看看apply方法传入的参数:

我们传入的是RibbonService:

Spring Cloud 服务消费者(rest+ribbon)

它除了server对象之外还存储了服务名,是否HTTPS标识以及一个MAP类型的元数据集合。

我们具体分析下apply这个函数:

Spring Cloud 服务消费者(rest+ribbon)

先看第一句:

Spring Cloud 服务消费者(rest+ribbon)

重新构建了URI

再看第二句:

Spring Cloud 服务消费者(rest+ribbon)

这里有个request.getURI()方法,会调用之前的ServiceRequestWrapper对象中重写的getURI函数。它会使用

RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。

Spring Cloud 服务消费者(rest+ribbon)

看最后一句话,这个方法从Server对象中获取HOST和PORT信息,然后和URI对象进行整合,最终形成要访问的实例地址。

4.负载均衡器

这里我们主要是分析ILoadBalancer接口的具体实现类

⑴AbstractLoadBalancer

Spring Cloud 服务消费者(rest+ribbon)

这个是ILoadBalancer的抽象实现。

①这里边有个分组的枚举:

ALL:所有服务实例

STATUS_UP:正常服务实例

STATUS_NOT_UP:挂掉的服务实例

②gerServerList():根据分组类型获取不同的服务实例列表。

③getLoadBalancerStats():定义了获取 LoadBalancerStats的方法

④LoadBalancerStats是负载均衡器存储各个实例当前的属性和统计信息。

⑵BaseLoadBalancer

是Ribbon负载均衡器的基础实现类,定义了很多关于负载均衡器的基本内容。

①定义了两个服务列表,一个是所有的,一个是正常的

Spring Cloud 服务消费者(rest+ribbon)

②BaseLoadBalancer中定义了一个IPingStrategy,用来描述服务检查策略,IPingStrategy默认实现采用了SerialPingStrategy实现,SerialPingStrategy中的pingServers方法就是遍历所有的服务实例,一个一个发送请求,查看这些服务实例是否还有效,如果网络环境不好的话,这种检查策略效率会很低,如果我们想自定义检查策略的话,可以重写SerialPingStrategy的pingServers方法。
③在BaseLoadBalancer的chooseServer方法中(负载均衡的核心方法),我们发现最终调用了IRule中的choose方法来找到一个具体的服务实例,IRule是一个接口,在BaseLoadBalancer它的默认实现是RoundRobinRule类,RoundRobinRule类中采用了最常用的线性负载均衡规则,也就是所有有效的服务端轮流调用。
④在BaseLoadBalancer的构造方法中会启动一个PingTask,这个PingTask用来检查Server是否有效,PingTask的默认执行时间间隔为10秒。
⑤markServerDown方法用来标记一个服务是否有效,标记方式为调用Server对象的setAlive方法设置isAliveFlag属性为false。
⑥getReachableServers方法用来获取所有有效的服务实例列表。
⑦getAllServers方法用来获取所有服务的实例列表。
⑧addServers方法表示向负载均衡器中添加一个新的服务实例列表。

⑶DynamicServerListLoadBalancer

它继承了BaseLoadBalancer,实现了服务清单在运行期的动态更新能力。它还具备了对服务实例清单的过滤功能。也就是说我们可以通过过滤器来选择性的获取一批实例服务清单。

①首先DynamicServerListLoadBalancer类一开始就声明了一个变量serverListImpl,serverListImpl变量的类型是一个ServerList<T extends Server>,这里的泛型得是Server的子类,ServerList是一个接口,里边定义了两个方法:一个getInitialListOfServers用来获取初始化的服务实例清单;另一个getUpdatedListOfServers用于获取更新的服务实例清单。
②ServerList接口有很多实现类,DynamicServerListLoadBalancer默认使用了DomainExtractingServerList类作为ServerList的实现,但是在DomainExtractingServerList的构造方法中又传入了DiscoveryEnabledNIWSServerList对象,查看源码发现最终两个清单的获取方式是由DiscoveryEnabledNIWSServerList类来提供的。
③DomainExtractingServerList类中的obtainServersViaDiscovery方法是用来发现服务实例并获取的,obtainServersViaDiscovery方法的主要逻辑是这样:首先依靠EurekaClient从服务注册中心获取到具体的服务实例InstanceInfo列表,然后对这个列表进行遍历,将状态为UP的实例转换成DiscoveryEnabledServer对象并放到一个集合中,最后将这个集合返回。
④DynamicServerListLoadBalancer中还定义了一个ServerListUpdater.UpdateAction类型的服务更新器,Spring Cloud提供了两种服务更新策略:一种是PollingServerListUpdater,表示定时更新;另一种是EurekaNotificationServerListUpdater表示由Eureka的事件监听来驱动服务列表的更新操作,默认的实现策略是第一种,即定时更新,定时的方式很简单,创建Runnable,调用DynamicServerListLoadBalancer中updateAction对象的doUpdate方法,Runnable延迟启动时间为1秒,重复周期为30秒。
⑤在更新服务清单的时候,调用了我们在第一点提到的getUpdatedListOfServers方法,拿到实例清单之后,又调用了一个过滤器中的方法进行过滤。过滤器的类型有好几种,默认是DefaultNIWSServerListFilter,这是一个继承自ZoneAffinityServerListFilter的过滤器,具有区域感知功能。即它会对服务提供者所处的Zone和服务消费者所处的Zone进行比较,过滤掉哪些不是同一个区域的实例。

⑷ZoneAwareLoadBalancer

ZoneAwareLoadBalancer是DynamicServerListLoadBalancer的子类,ZoneAwareLoadBalancer的出现主要是为了弥补DynamicServerListLoadBalancer的不足。由于DynamicServerListLoadBalancer中并没有重写chooseServer方法,所以DynamicServerListLoadBalancer中负责均衡的策略依然是我们在BaseLoadBalancer中分析出来的线性轮询策略,这种策略不具备区域感知功能,这样当需要跨区域调用时,可能会产生高延迟。ZoneAwareLoadBalancer重写了setServerListForZones方法,该方法在其父类中的功能主要是根据区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并存入集合中,ZoneStats是一个用来存储每个Zone的状态和统计信息。重写之后的setServerListForZones方法主要做了两件事:一件是调用getLoadBalancer方法来创建负载均衡器,同时创建服务选择策略;另一件是对Zone区域中的实例清单进行检查,如果对应的Zone下已经没有实例了,则将Zone区域的实例列表清空,防止节点选择时出现异常。

5.Spring Cloud中的负载均衡策略

IRule
这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例。

AbstractLoadBalancerRule
AbstractLoadBalancerRule是一个抽象类,里边主要定义了一个ILoadBalancer,就是我们上文所说的负载均衡器,负载均衡器的功能我们在上文已经说的很详细了,这里就不再赘述,这里定义它的目的主要是辅助负责均衡策略选取合适的服务端实例。

RandomRule
看名字就知道,这种负载均衡策略就是随机选择一个服务实例,看源码我们知道,在RandomRule的无参构造方法中初始化了一个Random对象,然后在它重写的choose方法又调用了choose(ILoadBalancer lb, Object key)这个重载的choose方法,在这个重载的choose方法中,每次利用random对象生成一个不大于服务实例总数的随机数,并将该数作为下标所以获取一个服务实例。

RoundRobinRule
RoundRobinRule这种负载均衡策略叫做线性负载均衡策略,也就是我们在上文所说的BaseLoadBalancer负载均衡器中默认采用的负载均衡策略。这个类的choose(ILoadBalancer lb, Object key)函数整体逻辑是这样的:开启一个计数器count,在while循环中遍历服务清单,获取清单之前先通过incrementAndGetModulo方法获取一个下标,这个下标是一个不断自增长的数先加1然后和服务清单总数取模之后获取到的(所以这个下标从来不会越界),拿着下标再去服务清单列表中取服务,每次循环计数器都会加1,如果连续10次都没有取到服务,则会报一个警告No available alive servers after 10 tries from load balancer: XXXX。

RetryRule
看名字就知道这种负载均衡策略带有重试功能。首先RetryRule中又定义了一个subRule,它的实现类是RoundRobinRule,然后在RetryRule的choose(ILoadBalancer lb, Object key)方法中,每次还是采用RoundRobinRule中的choose规则来选择一个服务实例,如果选到的实例正常就返回,如果选择的服务实例为null或者已经失效,则在失效时间deadline之前不断的进行重试(重试时获取服务的策略还是RoundRobinRule中定义的策略),如果超过了deadline还是没取到则会返回一个null。

WeightedResponseTimeRule
WeightedResponseTimeRule是RoundRobinRule的一个子类,在WeightedResponseTimeRule中对RoundRobinRule的功能进行了扩展,WeightedResponseTimeRule中会根据每一个实例的运行情况来给计算出该实例的一个权重,然后在挑选实例的时候则根据权重进行挑选,这样能够实现更优的实例调用。WeightedResponseTimeRule中有一个名叫DynamicServerWeightTask的定时任务,默认情况下每隔30秒会计算一次各个服务实例的权重,权重的计算规则也很简单,如果一个服务的平均响应时间越短则权重越大,那么该服务实例被选中执行任务的概率也就越大。

ClientConfigEnabledRoundRobinRule
ClientConfigEnabledRoundRobinRule选择策略的实现很简单,内部定义了RoundRobinRule,choose方法还是采用了RoundRobinRule的choose方法,所以它的选择策略和RoundRobinRule的选择策略一致,不赘述。

BestAvailableRule
BestAvailableRule继承自ClientConfigEnabledRoundRobinRule,它在ClientConfigEnabledRoundRobinRule的基础上主要增加了根据loadBalancerStats中保存的服务实例的状态信息来过滤掉失效的服务实例的功能,然后顺便找出并发请求最小的服务实例来使用。然而loadBalancerStats有可能为null,如果loadBalancerStats为null,则BestAvailableRule将采用它的父类即ClientConfigEnabledRoundRobinRule的服务选取策略(线性轮询)。

PredicateBasedRule
PredicateBasedRule是ClientConfigEnabledRoundRobinRule的一个子类,它先通过内部定义的一个过滤器过滤出一部分服务实例清单,然后再采用线性轮询的方式从过滤出来的结果中选取一个服务实例。

ZoneAvoidanceRule
ZoneAvoidanceRule是PredicateBasedRule的一个实现类,只不过这里多一个过滤条件,ZoneAvoidanceRule中的过滤条件是以ZoneAvoidancePredicate为主过滤条件和以AvailabilityPredicate为次过滤条件组成的一个叫做CompositePredicate的组合过滤条件,过滤成功之后,继续采用线性轮询的方式从过滤结果中选择一个出来。