一,负载均衡
当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。
负载均衡可以分为硬件负载均衡和软件负载均衡:
- 硬件负载均衡:直接在服务器和外部网络间安装负载均衡硬件设备,这种设备我们通常称之为负载均衡器。由专门的设备完成,独立于操作系统,整体性能得到大量提高,加上更多的负载均衡策略,智能化的流量管理,可达到最佳的负载均衡需求。 一般来说,硬件负载均衡在功能、性能上优于软件方式,不过成本昂贵,很常见的有 F5负载均衡器。
- 软件负载均衡:指在服务器的操作系统上,安装软件,来实现负载均衡,如Nginx负载均衡。它的优点是基于特定环境、配置简单、使用灵活、成本低廉,可以满足大部分的负载均衡需求。
这里我们只关注软件负载均衡,软件负载均衡又分为服务端负载均衡和客户端负载均衡。
- 服务端负载均衡:客户端的请求信息并不会直接去请求服务实例,而是在到达负载均衡器的时候,通过负载均衡算法选择某一个服务实例,然后将请求转发到这个服务实例上。
- 客户端负载均衡:客户端请求不会再去负载均衡器上进行转发了,客户端自己维护了一套服务列表,要掉用的某个服务实例之前首先会通过负载均衡算法选择一个服务节点,直接将请求发送到该服务节点上。
我们要学的Ribbon使用的是客户端负载均衡。
二,Ribbon负载均衡
Ribbon是Netflix开发的客户端负载均衡器,为Ribbon配置服务提供者地址列表后,Ribbon就可以基于某种负载均衡策略算法,自动地帮助服务消费者去请求 提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。我们也可以实现自定义负载均衡算法。
Ribbon作为Spring Cloud的负载均衡机制的实现:
- Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。
- Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。
- Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。
1,Ribbon单独使用
1.1:第一种方式:使用配置文件配置服务列表
由于Eureka中默认集成了Ribbon,所以我们想要脱离Eureka,首先要去掉eureka-client的依赖,单独去依赖ribbon,pom文件如下:
application.yaml
# 定义ribbon服务id
test-ribbon:
ribbon:
# 定义负载均衡算法,随机
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 禁用Eureka
eureka:
enable: false
# 定义服务列表
listOfServers: euk-client1:7004,euk-client2:7005,euk-client3:7006
HelloController.java
@RestController
public class HelloController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/hello")
public String hello() {
ServiceInstance instance = loadBalancerClient.choose("test-ribbon");
System.out.println("host:" + instance.getHost() + " port:" + instance.getPort());
return "hello";
}
}
多次访问http://localhost:8080/hello输出:
host:euk-client1 port:7004
host:euk-client1 port:7004
host:euk-client2 port:7005
host:euk-client1 port:7004
host:euk-client3 port:7006
host:euk-client3 port:7006
host:euk-client1 port:7004
host:euk-client3 port:7006
host:euk-client3 port:7006
host:euk-client2 port:7005
1.2:第二种方式:使用java代码配置服务列表
HelloController.java
@RestController
public class HelloController2 {
@GetMapping("/hello2")
public String hello() {
//使用Ribbon原生API调用服务
//手动创建服务列表
List<Server> serverList = Arrays.asList(new Server("euk-client1", 7001),
new Server("euk-client2", 7002), new Server("euk-client3", 7003));
BaseLoadBalancer baseLoadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
//设置负载均衡策略IRule,默认使用轮询,此处我们设置为随机策略
baseLoadBalancer.setRule(new RandomRule());
LoadBalancerCommand.<String>builder().withLoadBalancer(baseLoadBalancer).build()
.submit(new ServerOperation<String>() {
@Override
public Observable<String> call(Server server) {
System.out.println("host:" + server.getHost() + " port:" + server.getPort());
return Observable.just("");
}
}).toBlocking().first();
return "hello";
}
}
访问地址:http://localhost:8080/hello2 输出:
host:euk-client1 port:7001
host:euk-client2 port:7002
host:euk-client2 port:7002
host:euk-client1 port:7001
host:euk-client1 port:7001
host:euk-client1 port:7001
host:euk-client1 port:7001
host:euk-client3 port:7003
host:euk-client3 port:7003
host:euk-client2 port:7002
2,Ribbon + RestTemplate
2.1:创建一个Eureka服务注册中心eureka-server
application.yaml
spring:
application:
name: server
---
server:
port: 7001
eureka:
instance:
hostname: euk-server1
client:
service-url:
defaultZone: http://euk-server1:7001/eureka/
spring:
profiles: 7001
启动类加@EnableEurekaServer
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
运行起来。
2.2:创建一个服务提供者eureka-provider
application.yaml
spring:
application:
name: eureka-provider
eureka:
client:
service-url:
defaultZone: http://euk-server1:7001/eureka/
instance:
hostname: euk-client1
HelloController.java用来接收服务调用方
@RestController
public class HelloController {
@Value("${server.port}")
private String port;
@GetMapping("/testRibbon")
public String testRibbon() {
System.out.println("port:" + port);
return "服务提供者,端口为:" + port;
}
}
启动类加@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
public class EurekaProviderApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaProviderApplication.class, args);
}
}
代码完成之后,我们在下图处分别配置8001,8002,8003三个端口启动
然后我们刷新之前打开的 服务注册中心页面,我们发现此时已经有三个服务名为eureka-provider,端口号分别8001,8002,8003的服务注册了进来:
好了,多个服务已经搭建好了,接下来,我们就要通过Ribbon+RestTemplate的方式从Eureka注册中心中获取服务列表,并通过负载均衡策略访问指定的服务节点。
2.3:创建一个服务消费者eureka-consumer
application.yaml
server:
port: 7008
spring:
application:
name: EurekaConsumer
eureka:
client:
service-url:
defaultZone: http://euk-server1:7001/eureka
instance:
hostname: localhost
RestTemplateConfig.java
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
我们发现,我添加了一个@LoadBalanced注解,添加了该注解后,我们就可以直接使用服务名并且自带Ribbon负载均衡功能去调用服务。
HelloController.java
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello() {
String forObject = restTemplate.getForObject("http://eureka-provider/testRibbon", String.class);
return forObject;
}
}
最后我们启动RibbonClient项目,由于我们并没有设置负载均衡策略,所以默认使用轮询策略来调度服务。
项目启动成功后,我们访问http://localhost:7008/hello,刷新三次页面我们,访问结果依次如下:
可能你的访问顺序并不是按照我这个顺序来的,但是一定是三个端口循环调用。
3,Ribbon+Fegin
3.1如上例子,我们依次开启Eureka注册中心,和三个服务提供者EurekaProvider端口依次为:8001,8002,8003。
访问Eureka注册中心:http://euk-server1:7001/
3.2:新建EurekaConsumer并且加入OpenFeign依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bobo</groupId>
<artifactId>eureka-consumer2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.0-M5</spring-cloud.version>
</properties>
<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>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>
启动类开启@EnableFeignClients注解:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaConsumer2Application {
public static void main(String[] args) {
SpringApplication.run(EurekaConsumer2Application.class, args);
}
}
FeignInterface.java:其中@FeignClient中指定服务提供者的服务名,即我们上面注册的三个服务提供者eureka-provider。
@FeignClient(value = "eureka-provider")
public interface FeignInterface {
@RequestMapping("/testRibbon")
public String testRibbon();
}
FeignController.java:对feign进行调用:
@RestController
public class FeignController {
@Autowired
private FeignInterface feignInterface;
@RequestMapping("/testFeign")
public String testFeign() {
return feignInterface.testRibbon();
}
}
依次访问http://localhost:7008/testFeign会依次调用三个服务提供者:
三,饥饿加载
我们在搭建完springcloud微服务时,经常会发生这样一个问题:我们服务消费方调用服务提供方接口的时候,第一次请求经常会超时,再次调用就没有问题了。
为什么会这样?
主要原因是Ribbon进行客户端负载均衡的Client并不是在服务启动的时候就初始化好的,而是在调用的时候才会去创建相应的Client,所以第一次调用的耗时不仅仅包含发送HTTP请求的时间,还包含了创建RibbonClient的时间,这样一来如果创建时间速度较慢,同时设置的超时时间又比较短的话,从而就会很容易发生请求超时的问题。
解决方法
所以我们可以通过设置下面两个属性来提前创建RibbonClient:
//开启Ribbon的饥饿加载模式
ribbon.eager-load.enabled=true
//指定需要饥饿加载的服务名
ribbon.eager-load.clients=eureka-consumer
四,负载均衡策略
1,Ribbon提供的负载均衡策略
其中用红色方框圈出来的叶子节点是现在还在使用的负载均衡算法:
RoundRobinRule和WeightedResponseTimeRule
首先说明下 RoundRobinRule(轮询)策略,虽然我没有圈出来但是他是很常用的负载均衡算法,表示表示每次都取下一个服务器。
线性轮询算法实现:每一次把来自用户的请求轮流分配给服务器,从1开始,直到N(服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
通过图上的继承关系我们可知RoundRobinRule和WeightedResponseTimeRule是继承和被继承的关系。
WeightedResponseTimeRule是根据平均响应时间计算所有服务的权重,响应时间越快的服务权重越大被选中的概率越大。
有一个默认每30秒更新一次权重列表的定时任务,该定时任务会根据实例的响应时间来更新权重列表。
但是由于刚启动时如果统计信息不足,则使用RoundRobinRule(轮询)策略,等统计信息足够,会切换到WeightedResponseTimeRule。
AvailabilityFilteringRule
AvailabilityFilteringRule会先过滤掉由于多次访问故障而处于断路器状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问。
ZoneAvoidanceRule
综合判断Server所在区域的性能和Server的可用性选择服务器。
BestAvailableRule
会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。
RandomRule
随机选取服务。
使用 ThreadLocalRandom.current().nextInt(serverCount);随机选择。
RetryRule
先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败侧在指定的时间会进行重试,继续获取可用的服务。
2,自定义负载均衡算法
自定义负载均衡算法主要分三步:
- 实现IRule接口或者继承AbstractLoadBalancerRule类
- 重写choose方法
- 指定自定义的负载均衡策略算法类
首先我们创建一个MyRule类,但是这个类不能随便乱放。
官方文档给出警告:这个自定义的类不能放在@ComponentScan所扫描的当前包以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就是我们达不到特殊化指定的目的了。
自定义Rule。实现:如果有端口以2结尾,则选择。没有顺序找一个。
MyCustomRole.java
public class MyCustomRole extends AbstractLoadBalancerRule {
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
//激活可用的服务
List<Server> upList = lb.getReachableServers();
//所有的服务
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
//选自定义元数据的server,选择端口以2结尾的服务。
for (int i = 0; i < upList.size(); i++) {
server = upList.get(i);
String port = server.getHostPort();
if(port.endsWith("2") || port.endsWith("0")) {
break;
}
}
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(),key);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
使用自定义负载均衡策略的方式:
2.1:第一种,直接在启动类上添加
RibbonConfiguration.java
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new MyCustomRole();
}
}
启动类上面添加@RibbonClient,name指的是服务名称,configuration指的是自定义算法类。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@RibbonClient(name = "eureka-provider",configuration = RibbonConfiguration.class)
public class EurekaConsumer1Application {
public static void main(String[] args) {
SpringApplication.run(EurekaConsumer1Application.class, args);
}
}
2.2:第二种,在配置文件中指定自定义的负载均衡算法类
# 定义ribbon服务id
eureka-provider:
ribbon:
# 定义负载均衡算法,随机
NFLoadBalancerRuleClassName: com.bobo.eurekaconsumer1.MyCustomRole