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

Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(下)

程序员文章站 2022-06-13 10:45:58
...


博主整理的SpringCloud系列目录:>>戳这里<<

内容关联篇(建议先看):
Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(上)


一、Spring Cloud 中使用 Ribbon

在上篇,我们介绍了 Ribbon 组件的作用,以及 Ribbon 中的基本组件,如 ILoadBalancer、IPing、IRule、ServerList、ServerListUpdater。

本篇呢,将介绍如何在 Spring Cloud 中集成使用 Ribbon,结合 Eureka,实现客户端的负载均衡。

前面我们使用的 RestTemplate(被@LoadBalanced修饰),还有后面将介绍的 Feign,都已经拥有了负载均衡功能。这里将以 RestTemplate 为基础,介绍 Eureka 中的 Ribbon 配置。

这里再挖一个坑,后面博主还会讲讲如何在不使用 Eureka 支持的情况下,利用 Ribbon 各组件来保障集群服务的高可用性(主要是针对传统 Web 项目微服务化的支持)。

1.1、本例架构图

本例将会运行一个Eureka服务器实例、两个服务提供者实例、一个服务调用者实例,然后服务调用者请求服务。
Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(下)

  • cloud-ribbon-server

    新建Eureka服务端项目,命名为cloud-ribbon-server,端口为8761,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-server

  • cloud-ribbon-provider

    新建Eureka服务提供者项目,命名为cloud-ribbon-provider,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-provider
    该项目主要进行一下工作:

    • 在控制器中发布一个 REST 服务,地址为 /user/{userId},请求后返回 UserInfo 实例(包含服务端口信息)。
    • 服务提供者需启动两次,分别发布80818082两个端口。
  • cloud-ribbon-invoker

    新建Eureka服务调用者项目,命名为cloud-ribbon-invoker,端口为9000,Gitee上代码目录为Spring-Cloud-Study/02/ribbon02/cloud-ribbon-invoker。本例的负载均衡配置主要针对服务调用者。

由于博主前面的文章中已经讲解过如何快速搭建一个 Eureka 服务,服务提供者也只是发布一个简单的 REST 服务,这里就不再赘述了,下面主要讲讲服务调用者,Ribbon 的使用。

1.2、使用代码配置 Ribbon

上篇讲述了负载规则 IRule 以及 Ping 机制,在 Spring Cloud 中,可将自定义的负载规则以及 Ping 类放到服务调用者中查看效果。新建自定义的 IRule 和 IPing,代码如下:

package com.swotxu.ribboninvoker.ribbon;

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * @Date: 2020/9/19 19:31
 * @Author: swotXu
 */
@Slf4j
public class MyRule implements IRule {
    private ILoadBalancer lb;

    @Override
    public Server choose(Object o) {
        List<Server> servers = lb.getAllServers();
        log.info("这是自定义服务器规则类,输出服务器信息:");
        for (Server s : servers) {
            log.info("-> {}", s.getHostPort());
        }
        log.info("最后选择的服务器为:");
        return servers.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
        this.lb = iLoadBalancer;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }
}


package com.swotxu.ribboninvoker.ribbon;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;

/**
 * @Date: 2020/9/19 19:30
 * @Author: swotXu
 */
@Slf4j
public class MyPing implements IPing {
    @Override
    public boolean isAlive(Server server) {
        log.info("这是自定义Ping实现类:{}", server.getHostPort());
        return true;
    }
}

根据两个自定义 IRule 和 IPing 类可知,服务选择规则只返回集合中的第一个实例,IPing 的实现仅是输出日志信息。下面新建配置类:

package com.swotxu.ribboninvoker.config;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.swotxu.ribboninvoker.ribbon.MyPing;
import com.swotxu.ribboninvoker.ribbon.MyRule;
import org.springframework.context.annotation.Bean;

/**
 * @Date: 2020/9/19 19:35
 * @Author: swotXu
 */
public class MyConfig {

    @Bean
    public IRule getRule(){
        return new MyRule();
    }

    @Bean
    public IPing getPing(){
        return new MyPing();
    }
}


package com.swotxu.ribboninvoker.config;

import org.springframework.cloud.netflix.ribbon.RibbonClient;

/**
 * 设置 cloud-ribbon-provider 服务的 Ribbon 规则
 *
 * @Date: 2020/9/19 19:38
 * @Author: swotXu
 */
@RibbonClient(name = "cloud-ribbon-provider", configuration = MyConfig.class)
public class CloudProviderConfig {}

CloudProviderConfig配置类使用@RibbonClient注解,配置了 RibbonClient 的名称为 cloud-ribbon-provider,对应的配置类为 MyConfig,也就是名称为 cloud-ribbon-provider 的客户端将使用 MyRule 和 MyPing 两个类。

下面,我们编写一个对外的服务,服务中通过 RestTemplate 调用服务提供者。

package com.swotxu.ribboninvoker;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @Date: 2020/9/19 19:09
 * @Author: swotXu
 */
@Slf4j
@Configuration
@RestController
public class lnvokerController {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    @RequestMapping(value = "/router", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public String router(){
        RestTemplate restTemplate = getRestTemplate();
        String json = restTemplate.getForObject("http://cloud-ribbon-provider/user/1", String.class);
        log.info("result: {}", json);
        return json;
    }
}

关于 RestTemplate 的原理,将在后面讲述。进行以下操作,查看本例效果:

  • 启动一个 Eureka 服务器(cloud-ribbon-server)
  • 启动两个 Eureka 服务提供者(cloud-ribbon-provider)端口为8081和8082
  • 启动一个 Eureka 服务调用者(cloud-ribbon-invoker)
  • 打开浏览器访问http://127.0.0.1:9000/router,可以看到返回的JSON字符串,不管刷新多少次,最终都只会访问其中一个端口。

1.3、使用配置文件设置 Ribbon

前面我们使用代码配置类的方式,来设置使用 Ribbon。同样,我们可以通过配置文件的方式使用,在 application.yml 中添加以下配置:

cloud-ribbon-provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.swotxu.ribboninvoker.ribbon.MyRule
    NFLoadBalancerPingClassName: com.swotxu.ribboninvoker.ribbon.MyPing
    NFLoadBalancerPinglnterval: 2
    listOfServers: http://localhost:8081/,http://localhost:8082/

如上配置,以相同的方式运行此例子,可以看到同样的效果。

1.4、Spring 使用 Ribbon 的 API

Spring Cloud 对 Ribbon 进行封装,例如像负载客户端、负载均衡器等,我们可以直接使用 Spring 的 LoadBalancerClient 来处理请求及服务选择。如下

// 代码位置:02/ribbon02/cloud-ribbon-invoker/src/main/java/com/swotxu/ribboninvoker/lnvokerController.java

@Autowired
private LoadBalancerClient loadBalanced;

@RequestMapping(value = "/uselb", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ServiceInstance uselb(){
    // 查找服务器实例
    ServiceInstance si = loadBalanced.choose(SERVICE_NAME_PROVIDER);
    log.info("ServiceInstance: {}", si);
    return si;
}

除了使用 Spring 封装的负载客户端外,还可以直接使用 Ribbon 的 API,直接获取 Spring Cloud 默认环境中各个 Ribbon 的实现类,代码如下:

// 代码位置:02/ribbon02/cloud-ribbon-invoker/src/main/java/com/swotxu/ribboninvoker/lnvokerController.java

@Autowired
private SpringClientFactory factory;

@RequestMapping(value = "/usefactory", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public String usefactory(){
    log.info("========== 输出 default 配置 ==========");
    printlb(factory.getLoadBalancer("default"));

    log.info("========== 输出 {} 配置 ==========", SERVICE_NAME_PROVIDER);
    printlb(factory.getLoadBalancer(SERVICE_NAME_PROVIDER));

    return "OK!";
}
private void printlb(ILoadBalancer lb){
    ZoneAwareLoadBalancer zalb = (ZoneAwareLoadBalancer) lb;

    log.info("ILoadBalancer: {}", lb.getClass().getName());
    log.info("IRule: {}", zalb.getRule().getClass().getName());
    log.info("IPing: {}", zalb.getPing().getClass().getName());
    log.info("PingInterval: {}", zalb.getPingInterval());
    log.info("ClientConfig: {}", zalb.getClientConfig().getClass().getName());
    log.info("ServerListFilter: {}", zalb.getFilter().getClass().getName());
    log.info("ServerListImpl: {}", zalb.getServerListImpl().getClass().getName());
    log.info("ServerListUpdater: {}", zalb.getServerListUpdater().getClass().getName());
}

在浏览器中访问地址 http://localhost:9000/usefactory,可看到控制台输出如下:

========== 输出 default 配置 ==========
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.netflix.loadbalancer.ZoneAvoidanceRule
IPing: com.netflix.niws.loadbalancer.NIWSDiscoveryPing
PingInterval: 30
ClientConfig: com.netflix.client.config.DefaultClientConfigImpl
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ServerListImpl: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListUpdater: com.netflix.loadbalancer.PollingServerListUpdater

========== 输出 cloud-ribbon-provider 配置 ==========
ILoadBalancer: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.swotxu.ribboninvoker.ribbon.MyRule
IPing: com.swotxu.ribboninvoker.ribbon.MyPing
PingInterval: 30
ClientConfig: com.netflix.client.config.DefaultClientConfigImpl
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ServerListImpl: org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListUpdater: com.netflix.loadbalancer.PollingServerListUpdater

根据输出可知, cloud-ribbon-provider 客户端使用的负载规则类以及 Ping 类,是我们自定义的实现类。

下面,我们讲讲 RestTemplate 进行负载均衡的原理。

二、RestTemplate 负载均衡

2.1、@LoadBalanced 注解概述

RestTemplate 本是 spring-web 项目中的一个 REST 客户端,它遵 REST 的设计原则,提供简单的 API 让我们去调用 HTTP 服务。RestTemplate 本身不具有负载均衡的功能,该类也与 Spring Cloud 没有关系,但为何加入 @LoadBalanced 注解后,一个 RestTemplate 实例就具有负载均衡的功能了呢?

实际上这要得益于 RestTemplate 的拦截器功能。

在 Spring Cloud 中,使用 @LoadBalanced 修饰的 RestTemplate,在 Spring 容器启动时,会为这些被修饰过的 RestTemplate 添加拦截器 ,拦截器中使用了 LoadBalancerClient 来处理请求,LoadBalancerClient 本来就是 Spring 封装的负载均衡客户端,通过这样间接处理,使得 RestTemplate 拥有了负载均衡的功能。

下面我们将模仿拦截器机制,实现一个简单的 RestTemplate,以便让大家更了解 @LoadBalanced 以及 RestTemplate 的原理。

此案例需新建一个 Spring Boot 项目,仅依赖了spring-boot-starter-web模块.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.2、编写自定义注解及拦截器

先模仿 @LoadBalanced 注解,编写一个自定义注解,如下

package com.swotxu.rttest.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 模仿 @LoadBalanced 自定义负载均衡注解
 *
 * @Date: 2020/9/22 22:55
 * @Author: swotXu
 */
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {
}

注意,这里使用了@Qualifier限定注解。下面编写自定义拦截器

package com.swotxu.rttest.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

/**
 * 自定义拦截器,将原始 HttpRequest 替换为自定义的 MyHttpRequest
 *
 * @Date: 2020/9/22 23:00
 * @Author: swotXu
 */
@Slf4j
public class MyInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        log.info("===== 进入自定义拦截器实现 =====");
        log.info("  oldUri: {}", httpRequest.getURI());
        MyHttpRequest myHttpRequest = new MyHttpRequest(httpRequest);
        log.info("  newUri: {}", myHttpRequest.getURI());
        return clientHttpRequestExecution.execute(myHttpRequest, bytes);
    }
}

在拦截器中,将原始 HttpRequest 替换为自定义的 MyHttpRequest。MyHttpRequest 代码如下:

package com.swotxu.rttest.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;

import java.net.URI;

/**
 * HttpRequest 封装类
 *
 * @Date: 2020/9/22 23:01
 * @Author: swotXu
 */
@Slf4j
public class MyHttpRequest implements HttpRequest {

    private HttpRequest sourceRequest;

    public MyHttpRequest(HttpRequest httpRequest) {
        this.sourceRequest = httpRequest;
    }

    @Override
    public HttpMethod getMethod() {
        return sourceRequest.getMethod();
    }

    @Override
    public String getMethodValue() {
        return sourceRequest.getMethodValue();
    }

    @Override
    public HttpHeaders getHeaders() {
        return sourceRequest.getHeaders();
    }

    /**
     * 转换 URI
     * @return
     */
    @Override
    public URI getURI() {
        try {
            return new URI("http://localhost:8080/hello");
        } catch (Exception e) {
            log.warn("URI地址转换失败!", e);
        }
        return sourceRequest.getURI();
    }
}

在 MyHttpRequest 类中,会将原来请求的 URI 进行改写,只要使用了这个对象,所有的请求都会被转发到 http://localhost:8080/hello这个地址。

Spring Cloud 在对 RestTemplate 进行拦截时也做了同样的事情,只不过b并没有像我们这个固定了 URI,而是对“源请求”进行了更加灵活的处理。

下面,我们来使用自定义注解及拦截器

2.3、使用自定义注解及拦截器

编写一个 Spring 的配置类,在初始化的 Bean 中为容器中的 RestTemplate 实例设置自定义拦截器,如下:

package com.swotxu.rttest.config;

import com.swotxu.rttest.annotation.MyLoadBalanced;
import com.swotxu.rttest.interceptor.MyInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @Date: 2020/9/22 23:09
 * @Author: swotXu
 */
@Slf4j
@Configuration
public class MyAutoConfiguration {

    @Autowired(required = false)
    @MyLoadBalanced
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer(){
        log.info("===== 容器初始化注入 =====");
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate myRt : restTemplates) {
                    // 获取 RestTemplate 原来所有的拦截器
                    List<ClientHttpRequestInterceptor> list = new ArrayList(myRt.getInterceptors());
                    // 增加自定义拦截器
                    list.add(new MyInterceptor());
                    // 重新设置到 RestTemplate
                    myRt.setInterceptors(list);
                }
            }
        };
    }
}

在配置类中定义了 RestTemplate 集合,并且使用 @MyLoadBalanced 以及 @Autowired 注解修饰,@MyLoadBalanced 中含有 @Qualifier 注解。简单来说,就是在 Spring 容器中使用了 @MyLoadBalanced 修饰的 RestTemplate 实例,将会被加入配置类的 RestTemplate 集合中。

在容器初始化时,Spring 会向容器中注入 SmartInitializingSingleton 实例,该 Bean 在初始化完成后,会遍历 RestTemplate 集合并为它们设置“自定义拦截器”。

下面我们在控制器中使用@MyLoadBalanced

2.4、在控制器中使用RestTemplate

package com.swotxu.rttest.controller;

import com.swotxu.rttest.annotation.MyLoadBalanced;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @Date: 2020/9/22 23:18
 * @Author: swotXu
 */
@Slf4j
@RestController
@Configuration
public class InvokerController {

    @Bean
    @MyLoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    /**
     * 浏览器访问此请求
     *
     * @return
     */
    @RequestMapping(value = "/router", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public String router(){
        RestTemplate restTemplate = getRestTemplate();
        String json = restTemplate.getForObject("http://my-server/hello", String.class);
        log.info("result: {}", json);
        return json;
    }

    /**
     * 最终会转发到这个服务
     * @return
     */
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String hello(){
        return "Hello World";
    }
}

上述代码中,我们将/router的请求全部转到/hello服务上,我们实现的注解和 Spring 提供的 @LoadBalanced 注解使用一致。

打开浏览器,访问http://localhost:8080/router,可以看到实际上调用了 hello 的服务。

Spring Cloud 对 RestTemplate 的拦截实现更加复杂,并且在拦截器中使用 LoadBalancerClient 来实现请求的负载均衡功能。

三、项目下载

1、项目完整结构图

Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡(下)

2、源码下载

码云Gitee仓库地址:https://gitee.com/swotxu/Spring-Cloud-Study.git >>戳这里<<
项目路径:Spring-Cloud-Study/02/ribbon02


这篇,我们讲了在 Spring 中使用 Ribbon 负载均衡,同时,我们也深入介绍了 @LoadBalanced 注解如何使得 RestTemplate 具备负载均衡功能的。

下篇,我们将讲解 Spring Cloud 全家桶中的 Feign

别忘了点赞关注收藏~