《Spring Cloud微服务架构实战》--负载均衡 --Ribbon


《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进行了封装, 使用者可以使用封装后的API来实现负载均衡,也可以直接使用Ribbon的原生APL

4.1.2 Ribbon 子模块

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

  •  ribbon-core:该模块为Ribbon项目的核心,主要包括负载均衡器接口定义、客户端 接口定义、内置的负载均衡实现等API。
  •  ribbon-eureka:为Eureka客户端提供的负载均衡实现类。
  •  ribbon-httpclient:对Apache的HttpClient进行封装,该模块提供了含有负载均衡功 能的REST客户端。

4.1.3负载均衡器组件

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

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

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

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

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

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节中的应用类似,如果读者熟悉建立过程,可跳过部分创建过程,本小节最终目的 是发布两个REST服务。

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


  org.springframework.boot
  spring-boot-starter-web
  l.5.4.RELEASE

建立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”;   } }

  在控制器中,发布了两个REST服务。其中,调用地址为/person/personld的服务后, 会返回一个Person实例的JSON字符串,为了看到请求的URL,为Person的message属性设置了请求的URL

4.2.2 编写请求客户端

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


  com.netflix.ribbon
  ribbon
  2.2.2


  com.netflix.ribbon
  ribbon-httpclient
  2.2.2

接下来,使用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);     }   } }

  在代码清单中,使用ConfigurationManager类来配置请求的服务器列表, localhost:8080 localhost:8081, 再使用 RestClient 对象,向/person/1 地址发送 6 次请求。

启动两次服务器类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 loadPropertiesFromResources 方法可以指定properties文件的位置,配置格式如下:

.=

  其中为客户的名称,声明该配置属于哪一个客户端,在使用ClientFactory时可 传入客户端的名称,即可返回对应的“请求客户端”实例。

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

为属性名,为属性值。

  如果想对全部客户端生效, 可以将客户端名称去掉,直接以.的格式进行配置。以下的配置为客 户端指定了服务器列表:

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

  Ribbon的配置同样可以在Spring Cloud的配置文件(即application.yml)中使用。

4.3 Ribbon的负载均衡机制

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

4.3.1负载均衡器

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

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

服务器列表可使用listOfServers进行配置,也可以 使用动态更新机制。如下代码使用负载均衡器来选择服务器。

public class ChoseServerLest{
    //创建负载均衡器
    ILoadBalancer lb = new BaseLoadBalancer();
    //添加服务器
    List servers = new ArrayList();
    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这个负载均衡器,将两个服务器对象加入负载均衡器 中,再调用6次chooseServer方法,可以看到输出如下:

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

 根据结果可知,最终选择的服务器与4.2节中介绍的一致,可以判定本例与4.2节的例 子选择服务器的逻辑是一致的,在默认情况下,会使用RoundRobinRule的规则逻辑。

4.3.2自定义负载规则

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

而在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 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 servers = new ArrayList();
  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);
  }

}

  在请求客户端时,与4.2节中介绍的客户端基本一致,只是加入了 my-client.ribbon.NFLoad- BalancerRuleClassName属性,设置了自定义规则处理类为MyRule,

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

  启动4.2节中介绍的服务器端两次,分别设置8080与8081端口,再运行代码清单4-7, 可以看到输出了 6 次 {"id":l,"name":"Crazyit","age":30,"message":"http://localhost:8080/person/l"},

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

在实际环境中,如果要实现自定义的负载规则,可能还需要结合各种因素,例如考虑: 具体业务的发生时间、服务器性能等。

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

 

4.3.3 Ribbon自带的负载规则

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

> RoundRobinRule:系统默认的规则,通过简单地轮询服务列表来选择服务器,其他 规则在很多情况下仍然使用RoundRobinRule

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

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

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

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

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

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

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

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

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

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

 

4.3.4 Ping 机制

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

该工作由IPing接口的实现类负责,如果单独使用Ribbon,在默认情况下,不 会激活Ping机制,默认的实现类为DummyPing。

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

public class TestPingUrl{

  //创建负载均衡器
  BaseLoadBalancer lb = new BaseLoadBalancer();
  //添加服务器
  List servers = new ArrayList();
  // 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,设置了每隔2秒就向 个服务器发起请求,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 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

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

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

使用自定义的 Ping 类,通过修改..NFLoadBalancerPingClassName 配置即可,在此不再赘述

4.3.6其他配置

本节主要介绍了 Ribbon负载均衡器的负载规则以及Ping,这两部分可以通过配置来实现逻辑的改变。

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

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

4.4 Spring Cloud 中使用 Ribbon

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 请求的 URL。
  • 服务提供者需要启动两次,因此在控制台中需要输入启动端口

>新建Eureka服务调用者项目,命名为cloud.invoker,对外端口为9000,代码目录 为codes\04\4.4\cloud-invoker;本例的负载均衡配置主要针对服务调用者。

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

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 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 ,也就是名称为 cloud-provider的客户端将使用MyRule与MyPing两个类。

在服务调用者的控制器中,加入对外服务,服务中调用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加入了@LoadBalanced修饰,与第3章类似,在此不再赘述。

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

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

 

4.4.3使用配置文件设置Ribbon

  在前面使用Ribbon时,可以通过配置来定义各个属性。在使用Spring Cloud时,这些 属性同样可以配置到application.yml中,以下的配置同样生效:

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

  为cloud-provider这个客户端配置了规则处理类、Ping类以及服务器列表,以同样的方 式运行本小节的例子,可看到同样的效果,在此不再赘述。

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

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

4.4.4 Spring 使用 Ribbon API

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;   } }

  除了使用Spring封装的负载客户端外,还可以直接使用Ribbon的API,如代码清单 4-15所示,直接获取Spring Cloud默认环境中各个Ribbon的实现类。

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

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

一般情况下,Spring已经帮我们封装好了 Ribbon,我们只需直接调用RestTemplate API来访问服务即可。

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

4.5 RestTemplate 负载均衡

4.5.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-starter-web 块:


  org.springframework.boot
  spring-boot-starter-web
  l.5.4.RELEASE

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

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

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

  注意,MyLoadBalanced注解中使用了@Qualifier限定注解,接下来编写自定义的拦截 器,请见代码清单:

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进行改写,只要使用了这个对象,所有 的请求都会被转发到 http://localhost:8080/hello 这个地址。

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

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

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

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

@Configuration
public class MyAutoConfiguration {
  @Autowired(required=false)
  @MyLoadBalanced
  private List 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来修饰调用者的 RestTemplate

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使用了@MyLoadBalanced注解进行修饰。

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

在控制器 的router方法中,使用这个被拦截过的RestTemplate发送请求。

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

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

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

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

我们在实际环境中,并不需要实现自定义 注解以及拦截器,用Spring提供的现成API即可,

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

4.6本章小结

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

如果有 需要,还可以实现自己的负载均衡规则,以满足实际环境中多变的需求。

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

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

  在很多章节中,我们都使用了 RestTemplate发送请求,在4.5节,我们讲解了 RestTemplate 如何拥有负载均衡功能。

读者学习完4.5节,可以更加清楚RestTemplate的工作机制,以 及Spring Cloud对其进行的拦截处理。