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


博主整理的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服务器实例、两个服务提供者实例、一个服务调用者实例,然后服务调用者请求服务。
在这里插入图片描述

  • 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、项目完整结构图

在这里插入图片描述

2、源码下载

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


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

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

别忘了点赞关注收藏~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值