客户端负载均衡Ribbon
在整个微服务架构中,为了保证各个服务的高可用,一定少不了负载均衡。目前主流的负载均衡实现就两种:一种是服务端实现负载均衡,有硬件的(比如F5),也有软件的(比如Nginx)。另一种就是客户端根据自己的请求情况实现负载均衡。常见的,一个服务A,部署到3台机器,A1,A2和A3,配合Nginx形成一个简单的集群,当客户端请求过来之后,随机访问A1、A2、A3,这就是服务端负载均衡;同样的,将A服务部署到3台机器,A1、A2、A3,当客户端自己记录自己的请求情况,依次访问A1、A2、A3或者基数秒访问基数服务,偶数秒访问偶数服务,这就是客户端负载均衡。
Ribbon是Netflix开源的一款用于客户端负载均衡的工具,也是Sprong Cloud Netflix中的一员。它主要的实现是,首先通过Eureka获取相关服务的信息,然后通过一定的负载均衡算法去访问不同的服务。如下图:
关于Ribbon的依赖
在目前的Spring Cloud版本中,已经默认将Ribbon加入,所以也不用单独引用Ribbon依赖,如果下面的示例无法进行,则在相关项目中加入Ribbon依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
脱离Eureka单独使用Ribbon
注:下述3个项目,我这里分别以cloud-server0,cloud-server1,cloud-server2命名。
1、创建2个简单的项目(cloud-server0和cloud-server1项目,这里表示同一个项目部署了两套),并且都将2个项目命名为同一个服务名,并提供2个简单的接口,如下:
spring.application.name=cloud-balanced
第一个服务(cloud-server0项目中编写)
@Controller
public class Server0Controller {
// 当前接口使用的服务端口是:9000
@GetMapping("/ribbon/api")
@ResponseBody
public String methodB() {
return "我是服务0:9000";
}
}
第二个服务(cloud-server1项目中编写)
@Controller
public class Server1Controller {
// 当前接口使用的服务端口是:9001
@GetMapping("/ribbon/api")
@ResponseBody
public String methodC() {
return "我是服务1:9001";
}
}
2、创建第3个项目(cloud-server2项目,这里需要创建为Spring Cloud项目),编写负载均衡实现
public class RibbonTest {
public static void main(String[] args) {
// 创建服务列表
Server server1 = new Server("localhost", 9000);
Server server2 = new Server("localhost", 9001);
List<Server> serverList = new ArrayList<>();
serverList.add(server1);
serverList.add(server2);
// 创建负载实例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
// 调用10次观察效果
for (int i = 0; i < 10; i++) {
ServerOperation<String> serverOperation = new ServerOperation<String>() {
@Override
public Observable<String> call(Server server) {
String requestUrl = "http://" + server.getHost() + ":" + server.getPort() + "/ribbon/api";
System.out.println("请求地址: " + requestUrl);
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.connect();
InputStream is = conn.getInputStream();
byte [] bytes = new byte[1024];
int length = is.read(bytes);
return Observable.just(new String(bytes, 0, length));
} catch (Exception e) {
return Observable.error(e);
}
}
};
String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build().submit(serverOperation)
.toBlocking().first();
System.out.println("服务端响应结果: " + result);
}
}
}
3、执行后,可以看到是以轮询的方式对2个服务进行访问,结果如下:
请求地址: http://localhost:9001/ribbon/api
服务端响应结果: 我是服务1:9001
请求地址: http://localhost:9000/ribbon/api
服务端响应结果: 我是服务0:9000
请求地址: http://localhost:9001/ribbon/api
服务端响应结果: 我是服务1:9001
请求地址: http://localhost:9000/ribbon/api
服务端响应结果: 我是服务0:9000
请求地址: http://localhost:9001/ribbon/api
服务端响应结果: 我是服务1:9001
请求地址: http://localhost:9000/ribbon/api
服务端响应结果: 我是服务0:9000
请求地址: http://localhost:9001/ribbon/api
服务端响应结果: 我是服务1:9001
请求地址: http://localhost:9000/ribbon/api
服务端响应结果: 我是服务0:9000
请求地址: http://localhost:9001/ribbon/api
服务端响应结果: 我是服务1:9001
请求地址: http://localhost:9000/ribbon/api
服务端响应结果: 我是服务0:9000
在Eureka中使用Ribbon
注:下述eureka项目,我这里以cloud-eureka命名。
1、将上述3个项目都改造为Spring Cloud项目,并创建一个Eureka项目,然后将上述3个服务都注册到Eureka中去
注意:需要将前2个项目的服务名定义为同一个(cloud-server0和cloud-server1项目),在配置文件加入以下配置:
spring.application.name=cloud-balanced
2、在上述第3个项目中(在cloud-server2项目中编写),配置RestTemplate,如下:
@Configuration
public class RestTemplateConfig {
// @LoadBalanced注解会将当前restTemplate bean作为LoadBanlanceClient接口的实现类装配为Spring容器中
// 注意:使用了@LoadBalanced之后,服务之间的相互调用将不再支持ip+port的方式,只能以服务名称的方式相互访问
@LoadBalanced
@Bean
public RestTemplate initRestTemplate() {
// 这里只是简单配置RestTemplate,工作中请勿这么操作!!!
return new RestTemplate();
}
}
3、编写服务消费(在cloud-server2项目中编写)
@Controller
public class Server2Controller {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/server2/api0")
@ResponseBody
public String methodA() {
// ip+端口直接替换成服务提供者的名称进行服务消费
String result = restTemplate.getForObject("http://cloud-balanced/ribbon/api",
String.class);
System.out.println("请求结果: " + result);
return result;
}
}
4、启动上述4个项目之后,多次调用服务消费者接口(cloud-server2中的接口),可以看到,同样以轮询的方式进行服务消费,结果如下:
请求结果: 我是服务1:9001
请求结果: 我是服务2:9002
请求结果: 我是服务1:9001
请求结果: 我是服务2:9002
......
Ribbon常用配置
饥饿加载
所谓饥饿加载,字面意思就是和懒加载相对立。懒加载一般是需要用到的时候才去加载,饥饿加载则是,提前加载需要用到的东西。
在使用Ribbon对服务进行调用的时候,对服务的加载时间加上网络传输时间,很容易造成http调用超时,则可以开启饥饿加载来应对。
# 开启Ribbon饥饿加载
ribbon.eager-load.enabled=true
# 指定要饥饿加载的服务名,也就是需要调用的服务(比如上面的cloud-balanced),多个服务用英文逗号隔开
ribbon.eager-load.clients=cloud-balanced
控制台有如下日志打印,说明已经为目标集群服务进行了饥饿加载,如下:
禁用eureka
脱离eureka使用Ribbon(应该很少有这么干的吧),上面提供了编程式的访问服务,如果还想配合RestTemplate快速调用其他服务,就需要手动指定需要调用的服务地址,如下:
# 禁用eureka
ribbon.eureka.enabled=false
# 禁用 Eureka 后手动配置服务地址(多个服务配置多条).格式:服务名.ribbon.listOfServers,配置项的值为该服务的所有访问地址,用英文逗号隔开
cloud-balanced.ribbon.listOfServers=localhost:9000,localhost:9001
配置负载均衡策略
Ribbon默认的负载均衡策略是轮询(它们全部都实现Irule接口),当前使用版本,还提供以下策略:
1、BestAvailableRule 会优先跳过因多次访问故障而处于"熔断"状态的服务,然后选择一个并发最小的server
2、AvailabilityFilteringRule 会优先跳过因多次访问故障而处于"熔断"状态的server,以及跳过并发数超过阈值的server,然后对剩余server进行轮询访问
3、ZoneAvoidanceRule 基于区域和可用性筛选服务器的规则
4、RandomRule 在目标集群中随机选择一个Server
5、RetryRule 对于选定负载均衡策略(默认以轮询进行),再加上重试机制。简单来说就是,可以配合其他任何负载均衡策略,在指定时间内如果目标server没有响应,会进行重复访问。
6、RoundRobinRule (默认规则)最著名和最基本的负载均衡策略,即轮询规则。
7、WeightedResponseTimeRule 根据响应时间分配一个权重(Weight),响应时间越长,权重越小,被选中的可能性越低。如果统计值不足够计算,以轮询进行。(在低版本中是:ResponseTimeWeightedRule,已废弃)
负载均衡策略配置示例1(配置文件方式),如下:
# 选择随机策略作为cloud-balanced集群的负载策略
cloud-balanced.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
负载均衡策略配置示例1(代码配置方式,推荐以这种方式),如下:
// 一个集群创建一个空的配置类,指定@RibbonClient或@RibbonClients注解
// name:集群服务名 configuration:负载策略
// @RibbonClient用于指定一个服务集群,@RibbonClients用于指定多个服务集群,根据情况选择其中一个
@RibbonClient(name = "cloud-balanced", configuration = RandomRule.class)
@RibbonClients(value = {@RibbonClient(name = "cloud-balanced", configuration = RandomRule.class)})
public class RibbonLoadbalancerConfig {
}
重试策略配合其他策略(这里以随机策略为例),配置完成之后再选择上述任意一种方式进行配置,示例:
@Configuration
public class RibbonConfig {
// 重新装配RetryRule
@Bean
public RetryRule initRetryRule() {
// 同时还提供两个有参的构造方法,可以点进源码查看
RetryRule retryRule = new RetryRule();
// 设置负载策略为随机
retryRule.setRule(new RandomRule());
// 设置最大重试超时时间,单位毫秒,默认为500毫秒
retryRule.setMaxRetryMillis(1000);
return retryRule;
}
}
负载均衡策略配置示例1(自定义负载策略,实现IRule接口,然后再选择上述任意一种方式进行配置),如下:
// 这里不用再对MyRule进行装配为Spring Bean,因为Ribbon会为实现了IRule接口,且被指定为负载策略的类装配
public class MyRule implements IRule {
private ILoadBalancer iLoadBalancer;
// 定义负载均衡策略
@Override
public Server choose(Object key) {
// 获取目标集群中所有server
List<Server> servers = iLoadBalancer.getAllServers();
// 永远返回第一个server,即永远访问第一个server
// 具体可以去实现其他更复杂逻辑
return servers.get(0);
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.iLoadBalancer = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return iLoadBalancer;
}
}
其它说明
像一些http访问超时之类的配置,因为Ribbon是配合RestTemplate使用,所以其他的一些配置应该作用于RestTemplate。又或者想使用其他重试框架,比如Spring Retry,Spring Retry是基于AOP实现的,则需要在代码中单独进行编写。一般来讲,默认使用Ribbon的重试机制即可,如果面临某些接口,需要一些特定的或者不同的重试策略的时候,就可以再考虑加入Spring Retry。