Ribbon

Ribbon 是 Netflix 开源的基于 HTTP 和 TCP 的客户端负载均衡器框架,目前也已被 Spring Cloud 团队集成在 spring-cloud-netflix子项目下,主要用于客户端软负载功能,内部已实现了 随机轮训权重减压(选取压力最小的) 等常见的负载算法,同时也提供了 ILoadBalance 与 IRule 两个接口方便我们自己编写适合自己的负载算法

  • 负载均衡
  • 容错
  • 多协议(HTTP,TCP,UDP)支持异步和反应模型
  • 缓存和批处理

Ribbon软负载 (F版)-LMLPHP交互图

Try

要尝试 Spring Cloud Ribbon 首要的就是准备一个服务注册中心,还不太清楚的可以在回头看看上一章 认识Eureka ,这里就不做过多赘述了,准备 eureka-server(回顾上一章)product-serverorder-server 三个项目,后面的两个可以理解为上一章的 eureka-client

Eureka Server

详情参考上一章,或从文末的 GITHUB 链接获取对应篇幅的完整代码

Product Server

一个普通的 Eureka Client 即可,详情参考上一章,或从文末的 GITHUB 链接获取对应篇幅的完整代码

Order Server

一个普通的 Eureka Client

细心的小伙伴会发现,这和一个普通的 Eureka Client 也没啥区别啊,没任何额外依赖的,那是因为在 spring-cloud-starter-netflix-eureka-client 中已经帮我们依赖过 spring-cloud-starter-netflix-ribbon 了。假如使用 consulzookeeperetcd 等容器为服务发现为者时,就必须依赖 spring-cloud-starter-netflix-ribbon 包

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

在 src/main/resources 目录下创建一个 bootstrap.yml 的文件,写上 eureka 相关配置信息

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 7072
spring:
application:
name: order-server
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
client:
service-url:
defaultZone: http://localhost:7071/eureka/

各位小伙伴对 Spring Boot 中的 RestTemplate 应该都不陌生,它是由 Spring Boot 提供而不是 Spring Cloud ,无负载功能,为了方便开发者,Spring Cloud 团队提供了一个 @LoadBalanced 注解(默认采用轮训算法)

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
29
30
package com.battcn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate; /**
* @author Levin
*/
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication { @Configuration
class MyConfiguration {
@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
} public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}

客户端(order-server:7702)从 Eureka Server 同步了 product-server:7703 和 product-server:7704 这个时候它是如何知晓注册表中的信息呢?上一章中遗留了一个知识点就是 DiscoveryClient ,通过它就可以获得注册表中客户端的信息了,下列代码块演示了 DiscoveryClient 的简单用法,更多 API 可以自行尝试

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
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.battcn.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import java.util.List; /**
* @author Levin
* @since 2018/9/28 0028
*/
@RestController
@RequestMapping("/orders")
public class OrderController { private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient; @Autowired
public OrderController(RestTemplate restTemplate, DiscoveryClient discoveryClient) {
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
} @GetMapping
public String query() {
final List<String> services = discoveryClient.getServices();
for (String service : services) {
List<ServiceInstance> list = discoveryClient.getInstances(service);
for (ServiceInstance instance : list) {
System.out.println(instance.getUri() + "/" + service + " - " + instance.getServiceId());
}
}
return restTemplate.getForObject("http://PRODUCT-SERVER/products", String.class);
}
}

有的小伙伴对上面的内容会存在一些疑问,为什么没有写 IP:PORT 了,而是写了一串字符,它是怎么做到的?

1
2
3
4
5
6
7
8
9
10
11
12
13
用通俗的概念来说,它就是编码与解码操作,还记得 Eureka Server UI 中 Application 吗? 
编码:根据 spring.application.name 设置 serviceId
Server ID:PRODUCT-SERVER
Client A :http://localhost:7073/products
Client B :http://localhost:7074/products 解码:通过 serviceId 找到对应的客户端,
然后根据客户端配置的负载算法从对应集合中
找出符合当前算法条件的结果,最后拼接出相应的 http 地址即可 解码前:http://PRODUCT-SERVER/products
那么将 http://PRODUCT-SERVER 替换成 instance.getUri() 内容是不是就出来了
http://localhost:7073/products 和 http://localhost:7074/products

Ribbon软负载 (F版)-LMLPHP效果图

自定义 IRule

假如我们不想使用轮训了,换换口味改成随机算法,又或者想自己写一套适合自己的负载算法,可以用下面这种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.battcn.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; /**
* @author Levin
* @since 2018/9/28 0028
*/
@Configuration
public class RibbonRuleConfiguration { @Bean
public IRule ribbonRule() {
return new RandomRule();
}
} @RibbonClient(name = "ribbonRule",configuration = RibbonRuleConfiguration.class)
public class OrderController { }
05-23 11:15