Ribbon解决了什么问题

当集群中有了注册中心之后,服务消费者可以拿到所有服务的列表,此时就需要有一个组件能够将请求负载均衡到各个实例上去。ribbon就是这样一个客户端侧的负载均衡组件,同出Netflix,与其他组件整合后非常实用。

Ribbon提供了多种不同的负载均衡算法实现,非常实用。

快速体验Ribbon(一)

此次我们直接基于原生ribbon体验下他的API,这是由于Spring Cloud Netflix对Ribbon的封装比较彻底,几乎没有使用成本,这也让我们很难了解到Ribbon的架构设计理念、核心概念、API等。

1、开发一个服务提供者

依赖项

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@RequestMapping("/greeting")
public class GreetingController {

@Autowired
private Environment env;

@GetMapping("/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
log.info("port:{}, name:{}", env.getProperty("local.server.port"), name);
return "hello, " + name;
}

}

启动类

1
2
3
4
5
6
@SpringBootApplication
public class GreetingServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GreetingServiceApplication.class, args);
}
}

分别使用如下两个端口启动工程

application.yml文件

1
2
3
4
5
6
#server:
# port: 8080


server:
port: 8088

2、开发一个服务调用者

依赖

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
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>2.2.5</version>
</dependency>
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
<version>0.7.7</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.6</version>
</dependency>
1
2
3
4
5
6
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<ribbon.version>2.3.0</ribbon.version>
<archaius-core.version>0.7.6</archaius-core.version>
</properties>

服务消费者

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
import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;

public class GreetingClient {

public static void main(String[] args) throws Exception {
// 基于ribbon的api来开发一个可以负载均衡调用一个服务的代码

// 首先使用代码的方式对ribbon进行一下配置,配置一下ribbon要调用的那个服务的server list
ConfigurationManager.getConfigInstance().setProperty(
"greeting-service.ribbon.listOfServers", "localhost:8080,localhost:8088");

// 获取指定服务的RestClient,用于请求某个服务的client
RestClient restClient = (RestClient) ClientFactory.getNamedClient("greeting-service");

// 你要请求哪个接口,构造一个对应的HttpRequest
HttpRequest request = HttpRequest.newBuilder()
.uri("/greeting/sayHello/leo")
.build();

// 模拟请求一个接口10次
for(int i = 0; i < 10; i++) {
HttpResponse response = restClient.executeWithLoadBalancer(request);
String result = response.getEntity(String.class);
System.out.println(result);
}
}
}

3、开始实验

运行GreetingClient,发现10次调用被均匀的分给了两个服务实例,实现了负载均衡。

Ribbon服务消费方

Ribbon服务提供方1

Ribbon服务提供方2

快速体验Ribbon(二)

1、体验底层的负载均衡器API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GreetingClient2 {
public static void main(String[] args) {
ILoadBalancer loadBalancer = new BaseLoadBalancer();

ArrayList<Server> servers = new ArrayList<>();
servers.add(new Server("localhost", 8080));
servers.add(new Server("localhost", 8088));
loadBalancer.addServers(servers);

// 默认使用round robin轮询策略
for (int i = 0; i < 10; i++) {
Server server = loadBalancer.chooseServer(null);
System.out.println("chosen server:" + server);
}
}
}

Ribbon底层的负载均衡器API

可以看到,底层默认的负载均衡算法就是轮询算法。RestClient底层也是依赖负载均衡器工作的

2、自定义负载均衡规则

负载均衡器是基于一个IRule接口指定的负载均衡规则,来从服务器列表里获取每次要请求的服务器的,所以可以自定义负载均衡规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 自定义负载均衡策略,这里就返回第一个服务作为示例了
*/
static class MyRule implements IRule {

ILoadBalancer loadBalancer;

@Override
public Server choose(Object key) {
List<Server> servers = loadBalancer.getAllServers();
return servers.get(0);
}

@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.loadBalancer = lb;
}

@Override
public ILoadBalancer getLoadBalancer() {
return this.loadBalancer;
}
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
BaseLoadBalancer loadBalancer = new BaseLoadBalancer();
MyRule myRule = new MyRule();
myRule.setLoadBalancer(loadBalancer);
loadBalancer.setRule(myRule);

List<Server> servers = new ArrayList<>();
servers.add(new Server("localhost", 8080));
servers.add(new Server("localhost", 8088));
loadBalancer.addServers(servers);

// 默认使用round robin轮询策略
for (int i = 0; i < 10; i++) {
Server server = loadBalancer.chooseServer(null);
System.out.println("chosen server:" + server);
}
}

一般情况下,Ribbon内置的负载均衡规则就足够使用了,这里仅仅是为了体验一下底层API的设计。我们通过这个实验了解了ILoadBalancer和IRule是什么,两者有什么关系,以及特定场景下可以通过实现IRule接口实现自己的负载均衡规则(算法)。

3、Ribbon内置负载均衡规则

Ribbon内置的负载均衡规则

  • RoundRobinRule:默认的负载均衡规则。直接从一堆server list中,不断轮询选择出一个server,每个server收到的请求是均匀的。

  • AvailabilityFilteringRule: 这个rule会检查服务器的可用性
    如果连续3次连接失败,就会等待30秒后再次访问;
    如果不断失败,那么等待时间会不断变长。
    如果某个服务器的并发请求太高了,那么会绕过去,不再访问。

  • WeightedResponseTimeRule: 附带权重的,每个服务器可以指定一个权重,权重越高优先访问,如果魔鬼服务器响应时间比较长,那么权重就会降低,减少访问

  • ZoneAvoidanceRule:根据区域和服务器来进行负载均衡,根据机房划分

  • BestAvailableRule:忽略那些连接失败的服务器,然后尽量找并发比较低的服务器来请求

  • RandomRule:随机选择一个服务器

  • RetryRule:可以重试,先通过round robin找到的服务器,如果请求失败,就会接着会重新找一个服务器

可以看到,ribbon主打的就是负载均衡、网络通信,别的一些东西都是次要的。本系列文章会分析一些负载均衡算法的实现,以及看ribbon+eureka+spring cloud是如何整合使用的,会从源码角度分析下ribbon里的一些配置参数

快速体验Ribbon(三)

1、体验Ribbon中健康检查组件IPing

负载均衡器ILoadBalancer里,有IRule负责负载均衡的规则,选择一个服务器;还有一个IPing组件负责定时ping每个服务器,判断其是否存活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GreetingClient4 {

public static void main(String[] args) throws InterruptedException {
BaseLoadBalancer loadBalancer = new BaseLoadBalancer();

List<Server> servers = new ArrayList<>();
servers.add(new Server("localhost", 8080));
servers.add(new Server("localhost", 8088));
loadBalancer.addServers(servers);

// 每隔1秒ping一次上面那2个server
loadBalancer.setPing(new PingUrl(false, "/greeting/ping"));
loadBalancer.setPingInterval(1);

Thread.sleep(5000);

for (int i = 0; i < 10; i++) {
Server server = loadBalancer.chooseServer(null);
System.out.println(server);
}

}
}

通过给负载均衡器LoadBalancer设置Ping组件,比如要ping的url,间隔时间等,就可以使用ping功能了。为了实验效果,笔者在原来的greeting服务里新加了一个ping接口,这个接口是个GET接口,没有逻辑和返回值。在启动实验时,可以等1~2秒,停掉其中一个greeting服务实例,可以观察到有趣的现象

实验Ribbon的ping组件

笔者停掉了8088端口的实例,可以看到此时选取的服务器已经都是8080了,这在生产环境是比较实用的。

在生产环境,如果我们想要自定义更为强大的健康检查接口,可以通过实现IPing这个接口,覆盖里面的isAlive方法来实现。

快速体验Ribbon(四)

1、改造eureka入门里的实验代码

在eureka入门里,我们其实已经体验过了,这里为了更明显,对之前的代码稍加改造即可。

服务A-服务提供者

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class ServiceAController {

@Resource
private Environment environment;

@GetMapping("/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
Integer port = environment.getProperty("local.server.port", Integer.class);
return "hello," + name + ", came from " + port;
}
}

服务B-服务消费者

使用@LoadBalanced注解启用Ribbon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@EnableEurekaClient
@SpringBootApplication
public class ServiceBApplication {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
SpringApplication.run(ServiceBApplication.class, args);
log.info("ServiceB 启动成功");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@RestController
@Configuration
public class ServiceBController {

@Resource
private RestTemplate restTemplate;

@GetMapping("/greeting/{name}")
public String greeting(@PathVariable("name") String name) {
String response = restTemplate.getForObject("http://serviceA/sayHello/" + name, String.class);
log.info(response);
return response;
}
}

2、开始实验

  • 分别用8081、8082两个端口启动服务A
  • 用9000端口启动服务B
  • 启动eureka集群
  • 打开浏览器,访问http://localhost:9000/greeting/sam多次刷新

SpringCloudRibbon实验

与我们快速体验Ribbon的效果一样,请求被均匀的打到各个实例上去了。

此处不知你是否有疑问,为什么只是简单的加了一个注解,就可以用restTemplate访问不同的实例了呢?

不难猜测,肯定是从eureka那里获取到了服务注册表,然后ribbon拿着这些服务注册表,用自己的IRule组件选出一个server,然后就发送过去请求就可以了。

其实,ribbon的底层原理说白了,就是这些,我们此次源码分析的目的也是围绕这些点剖析一下就可以了,难度比eureka低不少。

总结

我们通过几个实验,了解到了ribbon的几个核心组件,ILoadBalancer负载均衡器,IRule负载均衡规则、IPing定时检测服务存活,以及回味了基于Spring Cloud Netflix Ribbon的简洁集成方式。下一步,跟随笔者进入Ribbon的源码世界吧!

Comments