《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项目,加入以下依赖:

1

2

3

4

5

<dependency>

  <groupld>org.springframework.boot</groupld>

  <artifactld>spring-boot-starter-web</artifactld>

  <version>l.5.4.RELEASE</version>

</dependency>

 

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

1

2

3

4

5

6

7

8

9

10

@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服务器

 

接下来编写控制器:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@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) <br>  public String hello() {

    return "hello”;

  }

}

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

4.2.2 编写请求客户端

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

1

2

3

4

5

6

7

8

9

10

<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的客户端发送请求, 请见代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

<br>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); <br>      String result = response.getEntity(String.class);

      System.out.printIn(result);

    }

  }

}

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

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

1

2

3

4

5

6

"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文件的位置,配置格式如下:

1

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

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

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

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

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

1

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进行配置,也可以 使用动态更新机制。如下代码使用负载均衡器来选择服务器。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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这个负载均衡器,将两个服务器对象加入负载均衡器 中,再调用6次chooseServer方法,可以看到输出如下:

1

2

3

4

5

6

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接口的实 现类。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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对象 <br>    return servers.get(0);

  }

  public void setLoadBalancer(ILoadBalancer lb) {

    this.lb = lb;

  }

  public ILoadBalancer getLoadBalancer () {<br>    return this.lb;

  }

}

  

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

为了能在负载均衡器中使用 自定义的规则,需要修改选择服务器的代码,请见代码清单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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的配 置,让请求的客户端使用我们定义的负载规则,请见代码清单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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.<clientName>.connection- FailureCountThreshold属性,来配置连接失败的次数。

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

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

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

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

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

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

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

4.3.4 Ping 机制

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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,设置了每隔2秒就向 两个服务器发起请求,PingUrl实际使用的是HttpCliento

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

最终效果为8080的服务器状态正常,而8888的服务器则无法连接, 运行代码清单,可以看到输出如下:

1

2

localhost: 8080 状态:true

localhost: 8888 状态:false

  

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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());

  }

}

  

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

1

2

> my-client.ribbon.NFLoadBalancerPingClassName:配置 IPing 的实现类。

> my-client.ribbon.NFLoadBalancerPinglnterval:配置 Ping 操作的时间间隔。

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

4.3.5 自定义 Ping

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

1

2

3

4

5

6

public class MyPing implements IPing {

  public boolean isAlive(Server server) {

    System.out.println("这是自定义Ping实现类:" + server.getHostPort());

    return true;

  }

}

  

要使用自定义的 Ping 类,通过修改<client>.<nameSpace>.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,两个实现类请见如下代码清单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class MyRule implements IRule {

  private ILoadBalancer lb;

  public Server choose(Object key) {

    List<Server> servers = lb.getAHServers ();

    System.out.printin ("这是自定义服务器规则类,输出服务器信息:"); <br>    for(Server s : servers) {

      System.out.printin(n  ” + s.getHostPort());

    }

    return servers.get(0);

  }

  //省略setter和getter方法

}<br>

public class MyPing implements IPing {

  public boolean isAlive(Server server) {

    System.out.printin("自定义Ping类,服务器信息:"+ server.getHostPort()); <br>    return true;

  }

}

  

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

public class MyConfig {

  @Bean

  public IRule getRule() (

    return new MyRule();

  }

  @Bean

  public IPing getPing() (

    return new MyPing();

  }

}<br><br>

@RibbonClient(name="cloud-provider", configuration=MyConfig.class)

public class CloudProviderConfig {

}

  

在代码清单4-12中,CloudProviderConfig配置类使用了 @RibbonClient注解,配置了 RibbonClient的名称为cloud-provider,对应的配置类为MyConfig ,也就是名称为 cloud-provider的客户端将使用MyRule与MyPing两个类。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@RestController

@Configuration

public class InvokerController {

  @LoadBalanced

  @Bean

  public RestTemplate getRestTemplate() {

    return new RestTemplate();

  }

  @RequestMapping(value = "/router", method = RequestMethod.GET, <br>                        produces = MediaType.APPLICAT10N_JSON_VALUE)

  public String router() {

    RestTemplate restTpl = getRestTemplate();

    //根据名称调用服务

    String json = restTpl.getForObject("http://cloud-provider/person/1", String.class); <br>    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中,以下的配置同样生效:

1

2

3

cloud-provider:

  ribbon:

    NFLoadBalancerRuleClassName: org.crazyit.cloud.MyRule <br>    NFLoadBalancerPingClassName: org.crazyit,cloud.MyPing <br>    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

1

2

3

4

5

6

7

8

9

10

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");<br>    return si;

  }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

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());<br>    return "";

  }

}

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

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

  

1

2

3

4

5

6

7

8

9

10

11

12

====输出默认配置:

IClientConfig:  com.netflix.loadbalancer.ZoneAwareLoadBalancer

IRule: com.netflix.loadbalancer.ZoneAvoidanceRule

IPing: com.netflix.niws.loadbalancer.NIWSDiscoveryPing

ServerList:  org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingserverList <br>ServerListFilter: org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter <br>ILoadBalancer:    com.netflix.loadbalancer.ZoneAwareLoadBalancer <br>Pinginterval: 30<br>

====输出 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 模块:

1

2

3

4

5

<dependency>

  <groupld>org.springframework.boot</groupld>

  <artifactld>spring-boot-starter-web</artifactld>

  <version>l.5.4.RELEASE</version>

</dependency>

  

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

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

1

2

3

4

5

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) 

@Retention(RetentionPolicy.RUNTIME)

@Qualifier

public @interface MyLoadBalanced {

}

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

1

2

3

4

5

6

7

8

9

10

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是一个自定义的请求类,实现请见代码清单:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public class MyHttpRequest implements HttpRequest {

  private HttpRequest sourceRequest;

  public MyHttpRequest(HttpRequest sourceRequest) {

    this.sourceRequest = sourceRequest;

  }

  public HttpHeaders getHeaders() {

    return sourceRequest.getHeaders ();<br>  }

  public HttpMethod getMethod() {

    return sourceRequest.getMethod();

  }

  /**

   * 将URI转换

   */

  public URI getURI() {

    try{

      String oldUri = sourceRequest.getURI().toString();

      URI newUri = new URI(nhttp://localhost:8080/hello"); <br>      return newUri;

    } catch (Exception e) {

      e.printStackTrace();

    } <br>    return sourceRequest.getURI();

  }

}

  

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

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@Configuration

public class MyAutoConfiguration {

  @Autowired(required=false)

  @MyLoadBalanced

  private List<RestTemplate> myTemplates = Collections.emptyList();<br>

  @Bean

  public SmartInitializingsingleton myLoadBalancedRestTemplatelnitializer() {<br>    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

控制器代码请见代码清单

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@RestController

@Configuration

public class InvokerController {

  @Bean

  @MyLoadBalanced

  public RestTemplate getMyRestTemplate() {<br>    return new RestTemplate();

  }<br>

  /**

   * 浏览器访问的请求

   */

  @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); <br>    return json;

  }

  /**

   * 最终的请求都会转到这个服务

   */

  @RequestMapping(value = "/hello", method = RequestMethod.GET) <br>  @ResponseBody <br>  public String hello () { <br>    return "Hello World";

  }

}

  

注意控制器的hello方法,前面实现的拦截器会将全部请求都转到这个服务中。控制器 的RestTemplate使用了@MyLoadBalanced注解进行修饰。

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

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

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

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

1

2

=============这是自定义拦截器实现

原来的 URI:http://my-server/hello <br>拦截后新的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对其进行的拦截处理。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

time Friend

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值