《Spring Cloud微服务架构实战》--Ribbon

 

4.1 Ribbon 介绍

4.1.1 Ribbon 简介

Ribbon是Netflix下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,它主要实现中间层应用程序的负载均衡。

Ribbon提供以下特性:

  • 负载均衡器,可支持插拔式的负载均衡规则。
  • 对多种协议提供支持,例如HTTP、TCP、UDP
  • 集成了负载均衡功能的客户端。

同为Netflix项目,Ribbon可以与Eureka整合使用,Ribbon同样被集成到Spring Cloud 中,作为spring-cloud-ne由ix项目中的子模块。

Spring Cloud将Ribbon的API进行了封装,

 

4.1.2 Ribbon 子模块

Ribbon主要有以下三大子模块。

  • 客户端
  •  ribbon-eureka:为Eureka客户端提供的负载均衡实现类。
  • 模块提供了含有负载均衡功

 

4.1.3负载均衡器组件

Ribbon的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器需要提供以下基础功能:

  • 维护服务器的IP、DNS名称等信息。
  • 根据特定的逻辑在服务器列表中循环。

为了实现负载均衡的基础功能,Ribbon的负载均衡器有以下三大子模块。

  • Rule: 一个逻辑组件,这些逻辑将会决定从服务器列表中返回哪个服务器实例。
  • Ping:该组件主要使用定时器来确保服务器网络可以连接。
  • ServerList:服务器列表,可以通过静态的配置确定负载的服务器,也可以动态指定 服务器列表。如果动态指定服务器列表,则会有后台的线程来刷新该列表。

本章关于Ribbon的知识,主要围绕负载均衡器组件进行

grcp java 实现负载均衡_客户端

 

 

 

4.2 第一个Ribbon程序

本章的4.2节和4.3节,单独使用Ribbon框架,关于整合Spring Cloud的内容,将在 4.4节讲述。本节将以一个简单的Hello World程序来展示Ribbon API的使用。

本例的程序

本书所使用的Spring Cloud, 默认集成的Ribbon版本为2.2.2, 因此本书也使用该版本的Ribbon

 

4.2.1编写服务

为了能查看负载均衡效果,先编写一个简单的REST服务,通过指定不同的端口,让服务可以启动多个实例。本例的请求服务器,仅仅是一个基于Spring Boot的Web应用,

与2.3节中的应用类似,如果读者熟悉建立过程,可跳过部分创建过程,本小节最终目的

新建名称为first-ribbon-server的Maven项目,加入以下依赖:

<dependency>
  <groupld>org.springframework.boot</groupld>
  <artifactld>spring-boot-starter-web</artifactld>
  <version>l.5.4.RELEASE</version>
</dependency>

 

建立Spring Boot启动类,如代码所示:

@SpringBootApplication
public class FirstServerApplication {
  public static void main(String[] args) {
    //读取控制台输入作为端口参数
    Scanner scan = new Scanner(System.in);
    String port = scan.nextLine();
    //设置启动的服务器端口
    new SpringApplicationBuiIder(FirstServerApplication.class).properties("server.port=" + port).run(args);
  }
}

  运行main方法,并在控制台输入端口号,即可启动Web服务器

 

接下来编写控制器:

@RestController
public class MyController {
  @RequestMapping(value = "/person/{personld}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  public Person findPerson(@PathVariable("personld") Integer personld, HttpServletRequest request) {
    Person p = new Person ();
    p.setld(personld);
    p.setName("Crazyit");
    p.setAge(30);
    p.setMessage(request.getRequestURL().toString());
    return p;
  }
  @RequestMapping(value = ”/”, method = RequestMethod.GET) 
  public String hello() {
    return "hello”;
  }
}

的服务后, 会返回一个Person实例的JSON字符串,为了看到请求的URL,为Person的message属性设置了请求的URL

 

4.2.2 编写请求客户端

新建名称为first-ribbon-client的Maven项目,加入以下依赖:

<dependency>
  <groupld>com.netflix.ribbon</groupld>
  <artifactld>ribbon</artifactld>
  <version>2.2.2</version>
</dependency>
<dependency>
  <groupld>com.netflix.ribbon</groupld>
  <artifactld>ribbon-httpclient</artifactld>
  <version>2.2.2</version>
</dependency>

  

接下来,使用Ribbon的客户端发送请求, 请见代码:

public class TestRestClient {
  public static void main(String[] args) throws Exception {
    //设置请求的服务器
    ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.listOfServers", "localhost:8080,localhost:8081");
    //获取REST请求客户端
    RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
    //创建请求实例
    HttpRequest request = HttpRequest.newBuilder()
                        .uri("/person/l").build();
    //发送6次请求到服务器中
    for (int i = 0; i < 6; i++) {
      HttpResponse response = client.executeWithLoadBalancer(request); 
      String result = response.getEntity(String.class);
      System.out.printIn(result);
    }
  }
}

为 localhost:8080 与 localhost:8081, 再使用 RestClient 对象,向/person/1 地址发送

启动两次服务器类FirstServerApplication,并在控制台分别输入8080和8081端口。启动服务器后,运行客户端,输出结果如下:

{ "id":1, "name":"Crazyit","age" : 30, "message":"http://localhost: 8081/person/l"}
{"id":1, "name":"Crazyit", "age":30,"message": "http://localhost:8080/person/l"}
{"id.":1, "name":"Crazyit","age":30,"message": "http://localhost: 8081/person/l"}
("id":1, "name":"Crazyit","age”:30,"message":"http://localhost:8080/person/l"}
{"id":l, "name":"Crazyit","age":30,"message":"http://localhost:8081/person/l"}
("id":l, "name":"Crazyit","age":30,"message":"http://localhost:8080/person/l"}

  根据输出结果可知,RestClient轮流向8080与8081端口发送请求,可见在RestClient 中己经帮我们实现了负载均衡的功能

4.2.3 Ribbon 的配置

在编写客户端时,使用了 ConfigurationManager来设置配置项,除了在代码中指定配置项外,还可以将配置放到.properties 文件中。

ConfigurationManager 的

<client><nameSpace>.<property>=<value>

时可 传入客户端的名称,即可返回对应的“请求客户端”实例。

<nameSpace>为该配置的命名空间,默认为ribbon,

<property>为属性名,<value>为属性值。

  如果想对全部客户端生效, 可以将客户端名称去掉,直接以<namespace>.<property>的格式进行配置。以下的配置为客

my-client.ribbon.listOfServers=localhost:8080,localhost:8081

的配置文件(即application.yml)中使用。

4.3 Ribbon的负载均衡机制

Ribbon提供了几个负载均衡的组件,其目的就是让请求转给合适的服务器处理。因此, 如何选择合适的服务器便成为负载均衡机制的核心。本节将围绕Ribbon负载均衡器的组件, 向大家展示Ribbon负载均衡的实现机制。

4.3.1负载均衡器

Ribbon的负载均衡器接口定义了服务器的操作,主要是用于进行服务器选择。

在前面 的例子中,客户端使用了 RestClient类,在发送请求时,会使用负载均衡器(ILoadBalancer) 接口,根据特定的逻辑来选择服务器。

服务器列表可使用listOfServers进行配置,也可以

public class ChoseServerLest{
    //创建负载均衡器
    ILoadBalancer lb = new BaseLoadBalancer();
    //添加服务器
    List<Server> servers = new ArrayList<Server>();
    servers.add(new Server("localhost", 8080));
    servers.add(new Server("localhost", 8081));
    lb.addServers(servers);
    //进行6次服务器选择
    for(int i = 0; i < 6; i++) {
        Server s = lb.chooseServer(null);
        System.out.printIn(s); 
    } 
}

代码中使用了 BaseLoadBalancer这个负载均衡器,将两个服务器对象加入负载均衡器

localhost:8081
localhost:8080
localhost:8081
localhost:8080
localhost:8081
localhost:8080

节的例 子选择服务器的逻辑是一致的,在默认情况下,会使用RoundRobinRule的规则逻辑。

 

4.3.2自定义负载规则

根据前一小节的介绍可知,选择哪个服务器进行请求处理,由ILoadBalancer接口的

而在BaseLoadBalancer类中,则使用IRule接口的choose方法来 决定选择哪一个服务器对象。

如果想自定义负载均衡规定,可以编写一个IRule接口的实 现类。

如下代码清单实现了自己的负载规定:

public class MyRule implements IRule {
  ILoadBalancer lb;
  public MyRule() {
  }
  public MyRule(ILoadBalancer lb) {
    this.lb = lb;
  }
  public Server choose(Object key) {
    //获取全部的服务器
    List<Server> servers = lb.getAHServers ();
    //只返回第一个Server对象 
    return servers.get(0);
  }
  public void setLoadBalancer(ILoadBalancer lb) {
    this.lb = lb;
  }
  public ILoadBalancer getLoadBalancer () {
    return this.lb;
  }
}

  

在自定义规则类中,实现的choose方法调用了 ILoadBalancer的getAHServers方法, 返回全部服务器,为了简单起见,本例只返回第一个服务器。

为了能在负载均衡器中使用

public class TestMyRule{
  //创建负载均衡器
  BaseLoadBalancer lb = new BaseLoadBalancer();
  //设置自定义的负载规则
  lb.setRule(new MyRule(lb));
  //添加服务器
  List<Servet> servers = new ArrayList<Server>();
  servers.add(new Server("localhost", 8080));
  servers.add(new Server("localhost", 8081));
  lb.addServers(servers);
  //进行6次服务器选择
  for(int i = 0; i < 6; i++) {
    Server s = lb.chooseServer(null);
    System.out.printIn(s);
  }

}

  运行上面代码清单可以看到,请求6次所得到的服务器均为localhost:8080。

以上是直 接使用编码方式来设置负载规则,可以使用配置的方式来完成这些工作。修改Ribbon的配

public class TestMyRuleConfig{

  //设置请求的服务器
  ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.listOfServers","localhost:8080,localhost:8081");
  //配置规则处理类
  ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
  //获取rest请求客户端
  RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
  //创建请求实例
  HttpRequest request = HttpRequest.newBuilder().uri("/person/1").build();
  //发送6次请求到服务器中
  for (int i = 0; i < 6; i++) {
    HttpResponse response = client.executeWithLoadBalancer(request);
    String result = response.getEntity(String.class); System.out.printIn(result);
  }

}

  

节中介绍的客户端基本一致,只是加入了

这个配置项同样可以在配置文件中使用,包括Spring Cloud的配置文件(application.yml等)。

可以看到输出了 6 次 {"id":l,"name":"Crazyit","age":30,"message":"http://localhost:8080/person/l"},

根据结果可知,我们的自定义规则生效了,请求只让8080端口处理。

在实际环境中,如果要实现自定义的负载规则,可能还需要结合各种因素,例如考虑:

实现中可能还涉及使用计算器、数据库等技术,具体情形会更为复杂,本例的负载规则较为简单,目的是让读者了解负载均衡的原理。

 

4.3.3 Ribbon自带的负载规则

Ribbon提供了若干个内置的负载规则,使用者完全可以直接使用,主要有以下内置的负载规则。

> RoundRobinRule:系统默认的规则,通过简单地轮询服务列表来选择服务器,其他

> AvailabilityFilteringRule:该规则会忽略以下服务器。

  • 无法连接的服务器:在默认情况下,如果3次连接失败,该服务器将会被置为“短路“的状态,该状态将持续30秒;如果再次连接失败,“短路”状态的持续时间

    可以通过修改niws.loadbalancer.<clientName>.connection- FailureCountThreshold属性,来配置连接失败的次数。

  • 并发数过高的服务器:如果连接到该服务器的并发数过高,也会被这个规则 忽略,可以通过修改<clientName>.ribbon.ActiveConnectionsLimit 属性来设定

> WeightedResponseTimeRule:为每个服务器赋予一个权重值,服务器的响应时间越 长,该权重值就越少,这个规则会随机选择服务器,权重值有可能会决定服务器的

> ZoneAvoidanceRule:该规则以区域、可用服务器为基础进行服务器选择。使用Zone 对服务器进行分类,可以理解为机架或者机房。

> BestAvailableRule:忽略“短路”的服务器,并选择并发数较低的服务器。

> RandomRule:顾名思义,随机选择可用的服务器。

> RetryRule:含有重试的选择逻辑,如果使用RoundRobinRule选择的服务器无法连接,那么将会重新选择服务器。

以上提供的负载规则基本可以满足大部分的需求,如果有更为复杂的要求,建议实现

 

4.3.4 Ping 机制

在负载均衡器中,提供了 Ping机制,每隔一段时间,会去Ping服务器,判断服务器

该工作由IPing接口的实现类负责,如果单独使用Ribbon,在默认情况下,不

如下代码清单使用了另外一个IPing实现类

public class TestPingUrl{

  //创建负载均衡器
  BaseLoadBalancer lb = new BaseLoadBalancer();
  //添加服务器
  List<Server> servers = new ArrayList<Server>();
  // 8080端口连接正常
  servers.add(new Server("localhost", 8080));
  // 一个不存在的端口
  servers.add(new Server("localhost", 8888));
  lb.addServers(servers);
  //设置IPing实现类
  lb.setPing(new PingUrl());
  //设置Ping时间间隔为2秒
  lb.setPinglnterval(2);
  Thread.sleep(6000);
  for (Server s : lb.getAHServers()) {
    System.out.printin(s.getHostPort() + "状态:"+s.isAlive());
  }
}

秒就向 两个服务器发起请求,PingUrl实际使用的是HttpCliento

在以上例子中,实际上会请求 http://localhost:8080 与 http://localhost:8888 这两个地址厂在运行前先以 8080 端口启动 4.2 节中介绍的服务器,

最终效果为8080的服务器状态正常,而8888的服务器则无法连接,

localhost: 8080 状态:true
localhost: 8888 状态:false

  

除了在代码中配置使用IPing类外,还可以在配置中设置IPing实现类,请见代码:

public class IestPingUrlConfig{
  //设置请求的服务器
  ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.1istOfServers",
                                                   "localhost:8080,localhost:8 8 8 8");
  //配置Ping处理类
  ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerPingClassName",
                                                                         PingUrl.class.getName());
  //配置Ping时间间隔
  ConfigurationManager.getConfiglnstance().setProperty("my-client.ribbon.NFLoadBalancerPinglnterval",
                                        2);
  //获取rest请求客户端
  RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
  Thread.sleep (6000);
  //获取全部服务器
  List<Server> servers = client.getLoadBalancer().getAHServers();
  System.out.printIn(servers.size());
  //输出状态
  for(Server s : servers) (
    System.out.printin(s.getHostPort() + ”状态:" + s.isAlive());
  }
}

  

注意代码中的以下两个配置。

> my-client.ribbon.NFLoadBalancerPingClassName:配置 IPing 的实现类。
> my-client.ribbon.NFLoadBalancerPinglnterval:配置 Ping 操作的时间间隔。

  以上两个配置同样可以使用在配置文件中。

4.3.5 自定义

通过前面章节的案例可知,实现自定义Ping较为简单,先实现IPing接口,然后再通过配置来设定具体的Ping实现类,下面代码清单为自定义的Ping类

public class MyPing implements IPing {
  public boolean isAlive(Server server) {
    System.out.println("这是自定义Ping实现类:" + server.getHostPort());
    return true;
  }
}

  

要使用自定义的

4.3.6其他配置

本节主要介绍了

除了这两部分外,还可以使用以下配置来改变负载均衡器的其他行为。

  • NFLoadBalancerClassName:指定负载均衡器的实现类,可利用该配置实现自己的
  • NIWSServerListClassName:服务器列表处理类,用来维护服务器列表,Ribbon已
  • NIWSServerListFilterClassName:用于处理服务器列表拦截。

4.4 在 Spring Cloud 中使用

Spring Cloud集成了 Ribbon,结合Eureka,可实现客户端的负载均衡。

前面章节中所使 用的RestTemplate (被@LoadBalanced修饰)、还有后面章节中将介绍的Feign,都己经拥有负载均衡功能。

本节将以RestTemplate为基础,讲述及测试Eureka中的Ribbon配置

4.4.1 准备工作

为了本节的测试做准备,按顺序进行以下工作:

>新建Eureka服务器端项目,命名为cloud-server,端口为8761,代码目录为 codes\04\4.4\cloud-server

>新建Eureka服务提供者项目,命名为cloud-provider,代码目录为codes\04\4.4\cloud- provider,该项目主要进行以下工作。

  • 在控制器里面发布一个REST服务,地址为/person/{personld},请求后返回 Person 实例,其中 Person 的 message 为 HTTP 请求的
  • 服务提供者需要启动两次,因此在控制台中需要输入启动端口

>新建Eureka服务调用者项目,命名为cloud.invoker,对外端口为9000,代码目录

以上项目准备完成并启动后,结构如图

grcp java 实现负载均衡_客户端_02

 

 

 

4.4.2 使用代码配置Ribbon

在4.3节中讲述了负载规则以及Ping机制,在Spring Cloud中,可将自定义的负载规 则以及Ping类放到服务调用者中查看效果。新建自定义的IRule与IPing,两个实现类请见如下代码清单:

public class MyRule implements IRule {
  private ILoadBalancer lb;
  public Server choose(Object key) {
    List<Server> servers = lb.getAHServers ();
    System.out.printin ("这是自定义服务器规则类,输出服务器信息:"); 
    for(Server s : servers) {
      System.out.printin(n	” + s.getHostPort());
    }
    return servers.get(0);
  }
  //省略setter和getter方法
}

public class MyPing implements IPing {
  public boolean isAlive(Server server) {
    System.out.printin("自定义Ping类,服务器信息:"+ server.getHostPort()); 
    return true;
  }
}

  

根据两个自定义的IRule和IPing类可知,实际上跟4.3节中介绍的自定义实现类似, 服务器选择规则只返回集合中的第一个实例,IPing实现仅仅是控制输入服务器信息。

接下来,新建配置类、返回规则与Ping的Bean,请见如下代码清单:

public class MyConfig {
  @Bean
  public IRule getRule() (
    return new MyRule();
  }
  @Bean
  public IPing getPing() (
    return new MyPing();
  }
}


@RibbonClient(name="cloud-provider", configuration=MyConfig.class)
public class CloudProviderConfig {
}

  

在代码清单4-12中,CloudProviderConfig配置类使用了 @RibbonClient注解,配置了 RibbonClient的名称为cloud-provider,对应的配置类为MyConfig ,也就是名称为

在服务调用者的控制器中,加入对外服务,服务中调用RestTemplate,如代码清单:

@RestController
@Configuration
public class InvokerController {
  @LoadBalanced
  @Bean
  public RestTemplate getRestTemplate() {
    return new RestTemplate();
  }
  @RequestMapping(value = "/router", method = RequestMethod.GET, 
                        produces = MediaType.APPLICAT10N_JSON_VALUE)
  public String router() {
    RestTemplate restTpl = getRestTemplate();
    //根据名称调用服务
    String json = restTpl.getForObject("http://cloud-provider/person/1", String.class); 
    return json;
  }
}

章类似,在此不再赘述。

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

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

 

4.4.3使用配置文件设置Ribbon

时,这些

cloud-provider:
  ribbon:
    NFLoadBalancerRuleClassName: org.crazyit.cloud.MyRule 
    NFLoadBalancerPingClassName: org.crazyit,cloud.MyPing 
    listOfServers: http://localhost:8080/,http://localhost:8081/

类以及服务器列表,以同样的方

4.4.2节使用了代码的方式来设置Ribbon,而4.4.3节则使用配置文件的方式,两种方式的效果一样,但比较起来,明显是配置文件的方式更加简便。

注: 在本案例的cloud-invoker模块中,默认使用了代码的方式来配置Ribbon, 配置文件中的配置已被注释。

 

4.4.4 Spring 使用 Ribbon 的

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

下面代码清单在服务器调用 者的控制器中使用了 LoadBalancerClient

public class lnvokerController{
  @Autowired
  private LoadBalancerClient loadBalancer;
  @RequestMapping(value = "/uselb", method = RequestMethod.GET,
                    produces = MediaType.APPLICATI0N_JSON_VALUE)
  public Serviceinstance uselb() {
    //査找服务器实例
    ServiceInstance si = loadBalancer.choose("cloud-provider");
    return si;
  }
}

如代码清单

public class lnvokerController{
  @Autowired
  private SpringClientFactory factory;
  @RequestMapping(value = "/defaultValue", method = RequestMethod.GET,
                    produces = MediaType.APPLICATI0N_JSON_VALUE)
  public String defaultvalue() (
    System.out.printin ("====输出默认配置:");
    //获取默认的配置
    ZoneAwareLoadBalancer alb = (ZoneAwareLoadBalancer) factory.getLoadBalancer("default");
    System, out .printIn (*'	IClientConf ig:"+ factory.getLoadBalancer("default") .getClass ().getName());
    System.out.printIn(" IRule: "+alb.getRule() .getClass().getName()); 
    System.out.printin ("IPing: "+alb.getPing().getClass().getName()); 
    System.out.printin ("ServerList:"+alb.getServerListlmpl().getClass().getName());
    System, out .printin ("ServerListFilter:"+alb.getFilter().getClass().getName());
    System, out .printIn ("ILoadBalancer: "+alb.getClass().getName());
    System.out .printIn ("Pinginterval: "+alb.getPinglnterval());
    System, out .printin ("====输出 cloud-provider 配置:");
    // 获取 cloud-provider 的配置
    ZoneAwareLoadBalancer alb2 = (ZoneAwareLoadBalancer) factory.getLoadBalancer("cloud-provider");
    System.out.printIn ("IClientConfig:"+ factory.getLoadBalancer("cloud-provider").getClass().getName()); 
    System.out.printIn ("IRule:" + alb2.getRule().getClass().getName()); 
    System.out.printIn ("IPing:" + alb2.getPing().getClass().getName());
    System.out.printIn ("ServerList:"+ alb2.getServerListlmpl().getClass().getName());
    System.out.printIn("ServerListFilter :"+ alb2.getFilter().getClass().getName()); 
    System.out.printIn ("ILoadBalancer: " + alb2.getClass().getName()); 
    System.out.printIn ("Pinginterval: "+ alb2.getPinglnterval());
    return "";
  }
}

代码中使用了 SpringClientFactory,通过该实例可获取各个默认的实现类以及配置,分别输出了默认配置以及cloud-provider配置。

运行上面的代码清单,在浏览器中访问地址 http://localhost:8080/defaultValue,可看到控制台中的输出如下:

  

====输出默认配置:
IClientConfig:  com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: com.netflix.loadbalancer.ZoneAvoidanceRule
IPing: com.netflix.niws.loadbalancer.NIWSDiscoveryPing
ServerList:  org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingserverList 
ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter 
ILoadBalancer:    com.netflix.loadbalancer.ZoneAwareLoadBalancer 
Pinginterval: 30

====输出 cloud-provider 配置:
IClientConfig: com.netflix.loadbalancer.ZoneAwareLoadBalancer
IRule: org.crazyit.cloud.MyRule
IPing: org.crazyit.cloud.MyPing
ServerList:  org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList
ServerListFilter:  org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter
ILoadBalancer:  com.netflix.loadbalancer.ZoneAwareLoadBalancer Pinginterval: 30

定义的实现类。

一般情况下,Spring已经帮我们封装好了 Ribbon,我们只需直接调用RestTemplate等

在接下来的章节中,我们将讲述RestTemplate进行负载均衡的原理。

 

4.5 RestTemplate 负载均衡

4.5.1 @LoadBalanced 注解概述

项目中的—个REST客户端,它遵循REST的设计原则,

RestTemplate本身不具有负载均衡的功能,该类也与Spring Cloud没有关系,但为何加入@LoadBalanced注解后,一个RestTemplate实例就具有负载均衡的功能了呢?实际上这要得益于RestTemplate的拦截器功能。

在 Spring Cloud 中,使用@LoadBalanced修饰的RestTemplate, 在 Spring 容器启动时, 会为这些被修饰过的RestTemplate添加拦截器,拦截器中使用了 LoadBalancerClient来处理请求,

LoadBalancerClient本来就是Spring封装的负载均衡客户端,通过这样间接处理,使得RestTemplate拥有了负载均衡的功能。

本节将模仿拦截器机制,带领大家实现一个简单的RestTemplate,以便让大家更了解 @LoadBalanced 以及 RestTemplate 的原理。

本节的案例只依赖了 spring-boot-starter-web 模块:

<dependency>
  <groupld>org.springframework.boot</groupld>
  <artifactld>spring-boot-starter-web</artifactld>
  <version>l.5.4.RELEASE</version>
</dependency>

  

4.5.2编写自定义注解以及拦截器

先模仿@LoadBalanced注解,编写一个自定义注解,请见代码清单:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) 
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MyLoadBalanced {
}

限定注解,接下来编写自定义的拦截

public class Mylnterceptor implements ClientHttpRequestInterceptor {
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws lOException {
    System.out.printin("这是自定义拦截器实现");
    System.out.printin("原来的URI:" + request.getURI());
    //换成新的请求对象(更换URI)
    MyHttpRequest newRequest = new MyHttpRequest(request);
    System.out.printin("拦截后新的URL:"+ request.getURI());
    return execution.execute(newRequest, body);
  }
}

  

在自定义拦截器Mylnterceptor中,实现了 intercept方法,该方法会将原来的HttpRequest 对象转换为我们自定义的MyHttpRequest,

MyHttpRequest是一个自定义的请求类,实现请见代码清单:

public class MyHttpRequest implements HttpRequest {
  private HttpRequest sourceRequest;
  public MyHttpRequest(HttpRequest sourceRequest) {
    this.sourceRequest = sourceRequest;
  }
  public HttpHeaders getHeaders() {
    return sourceRequest.getHeaders ();
  }
  public HttpMethod getMethod() {
    return sourceRequest.getMethod();
  }
  /**
   * 将URI转换
   */
  public URI getURI() {
    try{
      String oldUri = sourceRequest.getURI().toString();
      URI newUri = new URI(nhttp://localhost:8080/hello"); 
      return newUri;
    } catch (Exception e) {
      e.printStackTrace();
    } 
    return sourceRequest.getURI();
  }
}

  

在MyHttpRequest类中,会将原来请求的URI进行改写,只要使用了这个对象,所有 的请求都会被转发到

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

接下来使用自定义注解以及拦截器。

4.5.3使用自定义拦截器以及注解

编写一个Spring的配置类,在初始化的Bean中为容器中的RestTemplate实例设置自 定义拦截器,本例的Spring自动配置类请见代码清单:

@Configuration
public class MyAutoConfiguration {
  @Autowired(required=false)
  @MyLoadBalanced
  private List<RestTemplate> myTemplates = Collections.emptyList();

  @Bean
  public SmartInitializingsingleton myLoadBalancedRestTemplatelnitializer() {
    System.out.printin("这个Bean将在容器初始化时创建");
    return new SmartlnitializingSingleton() {
        public void afterSingletonsInstantiated() {
          for(RestTemplate tpl : myTemplates) (
            //创建一个自定义的拦截器实例
            My Interceptor mi = new Mylnterceptor ();
            //获取RestTemplate原来的拦截器
            List list = new ArrayList(tpl.getlnterceptors());
            //添加到拦截器集合
            list.add(mi);
            //将新的拦截器集合设置到RestTemplate实例
            tpl.setinterceptors(list);
          }
        )};
  }
}

 

在配置类中定义了 RestTemplate实例的集合,并且使用了@MyLoadBalanced以及 @Autowired注解进行修饰,@MyLoadBalanced中含有@Qualifier注解。

简单来说,就是在 Spring容器中使用了@MyLoadBalanced修饰的RestTemplate实例,该实例将会被加入配置类的RestTemplate集合中。

在容器初始化时,Spring 会调用 myLoadBalancedRestTemplatelnitializer 方法来创建 Bean, 该Bean在初始化完成后,会遍历RestTemplate集合并为它们设置“自定义拦截器”,

请见 代码清单4-19中的粗体代码。

下面在控制器中使用@MyLoadBalanced来修饰调用者的

 

4.5.4在控制器中使用RestTemplate

控制器代码请见代码清单

@RestController
@Configuration
public class InvokerController {
  @Bean
  @MyLoadBalanced
  public RestTemplate getMyRestTemplate() {
    return new RestTemplate();
  }

  /**
   * 浏览器访问的请求
   */
  @RequestMapping(value = "/router", method = RequestMethod.GET,
        produces = MediaType.APPLICATION_JSON_VALUE)
  public String router() {
    RestTemplate restTpl = getMyRestTemplate();
    //根据名称来调用服务,这个URI会被拦截器所置换
    String json = restTpl.getForObject("http://my-server/hello", String.class); 
    return json;
  }
  /**
   * 最终的请求都会转到这个服务
   */
  @RequestMapping(value = "/hello", method = RequestMethod.GET) 
  @ResponseBody 
  public String hello () { 
    return "Hello World";
  }
}

  

注意控制器的hello方法,前面实现的拦截器会将全部请求都转到这个服务中。控制器

熟悉前面使用RestTemplate的读 者可发现,我们实现的注解与Spring提供的@LoadBalanced注解使用方法一致。

在控制器

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

在访问该地址时,控制台输出如下:

=============这是自定义拦截器实现
原来的 URI:http://my-server/hello 
拦截后新的URI: http://localhost: 8080/hello

  

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

我们在实际环境中,并不需要实现自定义

本节的目的是展示RestTemplate的原理。

4.6本章小结

讲解了 Spring Cloud中的负载均衡组件Ribbon,对于Ribbon如何进行负载均 衡进行了详细讲解。读者学习完本章后,可以了解Ribbon是如何进行负载均衡的,

如果有

  作为Spring Cloud中的重要组件,Spring Cloud对Ribbon进行了封装,在使用Spring 提供的API时,我们甚至感觉不到Ribbon的存在。

本章的4.4节,重点讲述了在Spring Cloud 中如何配置和使用Ribbon;

节中,我们都使用了 RestTemplate发送请求,在4.5节,我们讲解了

读者学习完4.5节,可以更加清楚RestTemplate的工作机制,以