Ribbon
是Netflix
开发的客户端负载均衡器,为Ribbon
配置服务提供者地址列表后,Ribbon
就可以基于某种负载均衡策略算法让服务消费者去请求提供者。Ribbon
默认为我们提供了很多负载均衡算法,例如轮询、随机等,也可以实现自定义负载均衡算法。
一、Ribbon
的组成
官网首页:github.com/Netflix/rib…
-
ribbon-core
:核心的通用性代码 -
ribbon-eureka
:基于eureka
封装的模块,能快速集成eureka
-
ribbon-examples
:学习示例 -
ribbon-httpclient
:基于Apache HttpClient
封装的rest
客户端,集成了负载均衡模块 -
ribbon-loadbalancer
:负载均衡算法模块 -
ribbon-transport
:基于netty
实现多协议的支持,比如http
、tcp
、udp
等
1. 负载均衡形式
负载均衡的目的就是使集群中服务器的负载保持在稳定高效的状态,从而提供整个系统的请求处理能力。通常分为客户端负载均衡
、服务端负载均衡
。
(1)客户端负载均衡
在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些服务端地址列表都是从服务注册中心拉取的
(2)服务端负载均衡
在服务端负载均衡中,在客户端和服务端中间使用代理,比如Nginx
。但客户端节点只知道服务代理地址,服务代理则知道所有服务端的地址
总结: 客户端负载均衡和服务端负载均衡最大的区别在于 服务端地址列表的存储位置,以及负载算法在哪里
二、负载均衡算法
1. RoundRobinRule
轮询策略,Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多轮询 10 轮,若最终还没有找到,则返回 null
2. RandomRule
随机策略,从所有可用的 provider 中随机选择一个
3. RetryRule
重试策略,先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
4. BestAvailableRule
最低并发策略,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略
6. AvailabilityFilteringRule
可用过滤策略,会先过滤掉多次访问故障而处于熔断状态的服务、过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表按照轮询策略进行访问
7. ZoneAvoidanceRule
区域权衡策略,复合判断 Server 所在区域的性能和 Server 的可用性,轮询选择服务器
8. WeightedResponseTimeRule
响应时间加权策略,据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用 RoundRobinRule,等统计的信息足够了会自动的切换到 WeightedResponseTimeRule 。响应时间长,权重低,被选择的概率低
三、Ribbon
负载均衡搭建
3.1 服务提供方
pom.xml
文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.10.RELEASE
com.example
user-provider
0.0.1-SNAPSHOT
8
Hoxton.SR12
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
application.properties
配置
server.port=8081
#
#
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=user-provider-8081
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务名称
spring.application.name=user-provider
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到server
eureka.instance.prefer-ip-address=true
#
# eureka 服务名,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.appname=user-provider
#
# 实例的虚拟主机名称,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.virtual-host-name=user-provider
#
# 对外开放所有监控端点
management.endpoints.web.exposure.include=*
#
# 是否将自己注册到其他Eureka Server,默认为true
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息, 需要
eureka.client.fetch-registry=true
- 代码实现
public class User implements Serializable {
private String idCard;
private String username;
public User() {
}
public User(String idCard, String username) {
this.idCard = idCard;
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
@Override
public String toString() {
return "User{" +
"idCard='" + idCard + ''' +
", username='" + username + ''' +
'}';
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceProviderApplication.class, args);
}
}
@RestController
@RequestMapping("/user")
public class UserController {
private final static Log log = LogFactory.getLog(UserController.class);
@GetMapping("/get")
public User getUser() {
User user = new User("110", "大宝");
log.info("UserProvider result : " + user);
return user;
}
}
3.2 服务消费方
pom.xml
依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.10.RELEASE
com.example
web-consumer
0.0.1-SNAPSHOT
8
Hoxton.SR12
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
com.alibaba
fastjson
1.2.83
org.springframework.boot
spring-boot-maven-plugin
application.properties
配置
server.port=7071
# 注册中心地址
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=web-consumer-7071
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务对外暴露的名称
spring.application.name=web-consumer
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到 server
eureka.instance.prefer-ip-address=true
#
# 是否将自己注册到其他Eureka Server
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息
eureka.client.fetch-registry=true
#
# 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=5
#
# 设置 ribbon 和服务注册中心Eureka一起工作,ribbon 可以从服务注册中心获取服务端的地址信息
ribbon.eureka.enabled=true
- 代码实现
public class User implements Serializable {
private String idCard;
private String username;
public User() {
}
public User(String idCard, String username) {
this.idCard = idCard;
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
}
public class ResultBody implements Serializable {
private boolean status;
// 响应码
private String code;
// 响应描述信息
private String message;
// 响应数据
private T data;
public ResultBody() {
}
private ResultBody(T data) {
this.data = data;
}
private ResultBody(String code, String msg) {
this.code = code;
this.message = msg;
}
public static ResultBody success() {
ResultBody result = new ResultBody();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
return result;
}
public static ResultBody success(T data) {
ResultBody result = new ResultBody();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
result.setData(data);
return result;
}
public static ResultBody error(String code, String message) {
ResultBody result = new ResultBody(code, message);
result.setStatus(Boolean.FALSE);
return result;
}
public boolean isStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
@Configuration
public class RestTemplateConfig {
// 开启消费者的负载均衡,默认负载均衡策略是:轮询; @LoadBalanced 注解的作用就是标识 RestTemplate 为 LoadBalancerClient 客户端
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
RestTemplate build = restTemplateBuilder.build();
return build;
}
}
@SpringBootApplication
@EnableDiscoveryClient
public class WebConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(WebConsumerApplication.class, args);
}
}
@RestController
@RequestMapping("/web")
public class UserProviderController {
private final static Logger log = LoggerFactory.getLogger(UserProviderController.class);
private final static String PROVIDER_SERVICE_APPLICATION_NAME = "user-provider";
@Resource
private EurekaClient client;
@Resource
private RestTemplate restTemplate;
@Resource
private LoadBalancerClient loadBalancerClient;
/**
* RestTemplate + LoadBalancerClient 方式实现负载均衡
*/
@GetMapping("/balancer-client")
public ResultBody getUserByLoadBalancerClient() {
ServiceInstance server = loadBalancerClient.choose(PROVIDER_SERVICE_APPLICATION_NAME);
String url = server.getUri() + "/user/get";
RestTemplate restTemplate = new RestTemplateBuilder().build();
User res = restTemplate.getForObject(url, User.class);
return ResultBody.success(res);
}
/**
* RestTemplate + @LoadBalanced 方式实现负载均衡
*/
@GetMapping("/get")
public ResultBody getUserByRestTemplate() {
// 服务提供者的应用名称,当使用 RestTemplate + @LoadBalanced 实现客户端的负载均衡时,这的请求路径并不是ip+端口号
String url = "http://" + PROVIDER_SERVICE_APPLICATION_NAME + "/user/get";
User res = restTemplate.getForObject(url, User.class);
return ResultBody.success(res);
}
/**
* EurekaClient + ip + 端口 方式直连请求,这种方式没有走负载均衡
*/
@GetMapping("/client")
public Object ekClient() {
InstanceInfo info = client.getNextServerFromEureka(PROVIDER_SERVICE_APPLICATION_NAME, false);
if (InstanceInfo.InstanceStatus.UP.equals(info.getStatus())) {
String url = "http://" + info.getHostName() + ":" + info.getPort() + "/user/get";
log.info("服务端接口地址url : " + url);
RestTemplate restTemplate = new RestTemplate();
User res = restTemplate.getForObject(url, User.class);
return ResultBody.success(res);
}
return ResultBody.error("-1", "失败");
}
}
总结:
消费者请求服务提供者有多种调用方式
(1)EurekaClient + ip + 端口 的方式,该种方式相当于直连,没有走ribbon
负载均衡
(2)RestTemplate + @LoadBalanced注解 方式实现负载均衡
(3) RestTemplate + LoadBalancerClient 方式实现负载均衡
使用 ribbon
做客户端负载均衡时,使用RestTemplate + @LoadBalanced注解的方式请求服务提供者时url
不能使用端口+ip
,应该将端口+ip
替换成服务提供者的spring.application.name
属性值
Spring Cloud
2020 版本之后,spring-cloud-starter-netflix-eureka-client
中不再有ribbon
的jar
,如果使用高版本,需要单独引入
dependency>
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
2.2.10.RELEASE