一、什么是负载均衡
所谓的负载均衡(loadBalance)就是一种分压机制,当客户端产生大量的请求(流量)时,如果将所有的请求都分配给同一个服务器,那么会带来相应延迟、宕机… 的风险。所以需要将请求(流量)通过某种规则进行分散处理(将请求分配给多台服务器),从而实现给服务器降压。常用的负载均衡机制有:线性轮询、按权重负载、按流量负载、随机负载。
负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
负载均衡又分为服务端的负载均衡和客户端的负载均衡。
Ribbon 适合与在微服务中 RPC/REST 调用实现本地服务负载均衡,比如 Dubbo、SpringCloud 中都是采用本地负载均衡。Nginx 适合于服务器端实现负载均衡,比如 Tomcat 。
二、使用 Ribbon 实现负载均衡
Ribbon通过自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相对繁琐。
我们的的目标是实现三个 注册中心、三个数据库、三个服务提供者、一个服务消费者
参考:EurekaServer端/Eureka注册中心 的集群搭建(快速入门)
因为我们想要实现三个 服务提供者 ,同时实现了 三个的数据库 ,既然是分步式开发,那么每个 服务提供 者都应存在自己的数据库。
第一步:搭建 springcloud-provider-dept-8081
参考:EurekaServer端/Eureka注册中心 的集群搭建(快速入门)的 【三】
第二步:搭建 springcloud-provider-dept-8082
参考:EurekaServer端/Eureka注册中心 的集群搭建(快速入门)的 【三】
第三步:搭建 springcloud-provider-dept-8083
参考:EurekaServer端/Eureka注册中心 的集群搭建(快速入门)的 【三】
第四步:修改 服务提供者的端口号
因为三个服务提供者需要同时开启,我们没有开虚拟机,所以需要给它们分配不同的端口号,来实现同时启动。
springcloud-provider-dept-8081(application.ymal):
server:
port: 8081
springcloud-provider-dept-8082(application.ymal):
server:
port: 8082
springcloud-provider-dept-8083(application.ymal):
server:
port: 8083
第五步:统一Application名称
我们要将三个服务提供者的 application.yaml 中的 spring. application.name 改为 springcloud-provider-dept 。
spring:
application:
name: springcloud-provider-dept
这样Eureka在识别服务注册时,就会将相同的application.nam的服务识别到一个服务列表中
,那么服务消费者通过applicationName(微服务名)进行访问时,就可以实现负载均衡。如下(提前演示):【我只开启了springcloud-provider-dept-8081和springcloud-provider-dept-8082】
第六步:修改数据库连接的url
每个提供者都应存在自己的数据库,所以我们需要修改每个提供者对应的数据库
。
springcloud-provider-dept-8081(application.ymal):
url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
springcloud-provider-dept-8082(application.ymal):
url: jdbc:mysql://localhost:3306/db02?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
springcloud-provider-dept-8082(application.ymal):
url: jdbc:mysql://localhost:3306/db03?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
第七步:修改与端口相关的信息(非必须)
第一步:创建服务消费者
参考:springcloud 学习环境搭建 中的【五】
我们需要修改一处:将原本 通过微服务地址实现访问
改为通过微服务的服务名进行访问
。这是因为在Eureka中三个服务提供者会被注册到一个Application列表中。
@RestController
public class ConsumerController {
// private static final String URL_HEAD = "http://localhost:8081/";
//我们应该通过微服务的服务名进行访问微服务中的服务,而不是通过一个确定的微服务地址(因为我们之前配置了注册中心)
private static final String URL_HEAD = "http://SPRINGCLOUD-PROVIDER-DEPT";
@Autowired
private RestTemplate restTemplatel;
略...
}
第二步:添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
<version>2.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
<version>2.2.5.RELEASEversion>
dependency>
第三步:开启负载均衡注解
在服务消费者中我们是通过 RestTemplate
来实现基于 HTTP 的REST 类型的通信的,但是这种通信并没有负载均衡的功能。我们需要通过 @LoadBalanced
开启负载均衡
的功能。
package com.tiger.config;
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;
@Configuration
public class BeanConfig {
//配合通过Ribbon实现负载均衡(@LoadBalanced)
@Bean
@LoadBalanced //开启负载均衡
public RestTemplate restTemplateBean(){
return new RestTemplate();
}
}
因为小编的电脑不是16G的,无法同时开启 三个注册中心、三个服务提供者和一个服务消费者,所以我就开启一个注册中心、两个服务提供者和一个服务消费者作为演示。
访问:http://eurekaserver7001.com:7001/
,查看服务注册情况
访问 http://localhost:8080/lists
第一次,获取department表中的信息
访问http://localhost:8080/lists
第二次,获取department表中的信息
三、使用 Feign 实现负载均衡
Feign 是在 Ribbon的基础上进行了一次改进,Feign 中是集成了 Ribbon,是一个使用起来更加方便的 HTTP 客户端。
Feign采用接口的方式, 只需要创建一个接口,然后在上面添加注解即可 ,将需要调用的其他服务的方法定义成抽象方法即可, 不需要自己构建http请求。
第一步:创建 Feign 接口(DeptClientService)
我们需要创建一个客户端的service,该service中的方法需要与服务端的controller相对应。
@FeignClient(value = “SPRINGCLOUD-PROVIDER-DEPT”):其作用就是取代在Ribbon中使用RestTemplate访问服务端中的controller对应的路由。在Feign中会根据该注解的value内容,自动拼接成服务端的访问地址(例如:http://SPRINGCLOUD-PROVIDER-DEPT/dept/insert/dName)。
package com.tiger.service;
import com.tiger.pojo.Department;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@Component
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT")
public interface DeptClientService {
@GetMapping("/dept/insert/{name}")
boolean addDept(@PathVariable("name") String dName);
@GetMapping("/dept/select/{id}")
Department queryOne(@PathVariable("id") int dNum);
@GetMapping("/dept/select")
List<Department> queryAll();
}
第二步:通过 DeptClientService 接口调用服务
package com.tiger.controller;
import com.tiger.pojo.Department;
import com.tiger.service.DeptClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ConsumerController {
@Autowired
private DeptClientService deptClientService;
@RequestMapping("/add/{name}")
public boolean addDept(@PathVariable("name") String dName){
return this.deptClientService.addDept(dName);
}
@RequestMapping("/list/{id}")
public Department queryOne(@PathVariable("id") int dNum){
return this.deptClientService.queryByNum(dNum);
}
@RequestMapping("/lists")
public List<Department> queryAll(){
return this.deptClientService.queryAll();
}
}
第三步:主启动类上添加 Feign 支持注解
@EnableFeignClients(basePackages = “com.tiger”):这个注解的作用是开启客户端 feign 支持。
这个注解的参数 basePackages 什么时候加上,是有要求的。当 第二步 中的service写在当前的服务(A)中,那么就不用添加 basePackages 也会自动扫描(@SpringBootApplication 可以扫描)。当 第二步 中的service被抽离出来,写在了其他的服务(B)中,时候就需要使用basePackages属性字段去指明应用程序A在启动的时候需要扫描服务B中的标注了@FeignClient注解的接口的包路径。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class SpringcloudConsumerDeptFeignApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudConsumerDeptFeignApplication.class, args);
}
}
测试
我们开启 springcloud-eureka-7001
、springcloud-provider-dept-8081
、springcloud-provider-dept-8082
和 springcloud-consumer-dept-feign
。
访问:http://localhost:8080/list/1
,很明显实现了负载均衡。
四、常用的负载均衡策略
此策略是Ribbon的默认策略,是按照顺序,依次对服务列表中所有的服务进行访问。
就和这个策略的名字一样,是对user的3个服务的随机调用,所以不存在规律,如下源码中int index = this.chooseRandomInt(serverCount);
通过随机数来选择下标,所以对服务列表中的服务的调用是随机的。
如果你要使用这种策略,你可以通过 @Bean 将其放在IoC容器中。
@Configuration
public class TigerRuleConfig{
@Bean
public IRule myRule(){
//使用此负载均衡策略
return new RoundRobinRule();
}
}
IOC注入时的注意事项:
第一
负载均衡策略的IOC注入是有要求的,它应该被应用程序上下文的@ComponentScan注解扫描到(不能再主程序的同级目录下),否则将被原有的载均衡策略所覆盖。所以我们应该在主程序的上级目录中进行自定义策略与IOC的注入。
第二
我们需要在主程序上是有@RibbonClient (name="", configuration=Xxx.class )实现对原有的策略的覆盖
configuration :IOC注入时候所用到的类。
@SpringBootApplication
@EnableEurekaClient
//这样就可以指定 SPRINGCLOUD-PROVIDER-DEPT服务采用 TigerRuleConfig 对应的 ribbon 策略
@RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT", configuration = TigerRuleConfig.class)
public class SpringcloudConsumerDept8080Application {
public static void main(String[] args) {
SpringApplication.run(SpringcloudConsumerDept8080Application.class, args);
}
}
根据服务列表中服务的响应时间来分配权重,响应时间越长的服务,权重越低,那么被调用的概率也就越低。相反,响应时间越短的服务,权重越高,被调用的概率也就越高
响应时间加权重策略的实现分为两步:
① WeightedResponseTimeRule实现类中默认情况下每隔30秒会统计一次每个服务的权重,在此30秒内,用的是轮询策略。
② 30秒之后,会根据统计的结果来分配每个实例的权重,然后根据权重来分配调用次数。
如果你要使用这种策略,你可以通过 @Bean 将其放在IoC容器中。
@Configuration
public class TigerRuleConfig{
@Bean
public IRule myRule(){
//使用此负载均衡策略
return new WeightedResponseTimeRule();
}
}
IOC注入时的注意事项: 同上
重试策略是指通过轮询策略选出一个实例,然后去访问,如果此实例为null或者已经失效,那么会重试其他的实例,answer = this.subRule.choose(key);【源码】
会根据轮询策略选择一个实例,然后if ((answer == null || !answer.isAlive()) && System.currentTimeMillis() < deadline)【源码】
判断如果实例为null或者失效,那么会重新选择。
如果你要使用这种策略,你可以通过 @Bean 将其放在IoC容器中。
@Configuration
public class TigerRuleConfig{
@Bean
public IRule myRule(){
//使用此负载均衡策略
return new RetryRule();
}
}
IOC注入时的注意事项: 同上
会根据每个服务实例的并发数量来决定,访问并发数最少的那个服务,int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); 【源码】
会获得当前遍历的实例的并发数,然后和其他的实例的并发数进行判断,最终访问并发量最少的那个实例。
如果你要使用这种策略,你可以通过 @Bean 将其放在IoC容器中。
@Configuration
public class TigerRuleConfig{
@Bean
public IRule myRule(){
//使用此负载均衡策略
return new BestAvailableRule();
}
}
IOC注入时的注意事项: 同上
此策略会过滤掉一直失败并被标记为circuit tripped
的服务,而且会过滤掉那些高并发的服务。
如果你要使用这种策略,你可以通过 @Bean 将其放在IoC容器中。
@Configuration
public class TigerRuleConfig{
@Bean
public IRule myRule(){
//使用此负载均衡策略
return new AvailabilityFilteringRule();
}
}
IOC注入时的注意事项: 同上
五、自定义负载均衡策略
我们随便点开一个负载均衡策略(RandomRule),发现其继承了一个 AbstractLoadBalancerRule 方法,这就意味着,我们完全可以照着源码中的策略编写自己的策略,然后注入到IOC容器中即可。
package com.tigerRule;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
public class MyRule extends AbstractLoadBalancerRule {
public MyRule() {
}
@SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
} else {
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;
}
//==================通过你【自定义】的规则获取【当前】要执行的服务器的【索引】====================
int index = 0;
//略 (编写你自己的规则) ....
//==========================================================================================
server = (Server)upList.get(index); // 当前要执行的服务
if (server == null) {
Thread.yield(); // 使当前线程由执行状态,变成为就绪状态,让出cpu时间
} else {
if (server.isAlive()) {
return server;
}
server = null;
Thread.yield();
}
}
return server;
}
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
我们可以在主程序上是有@RibbonClient 实现对原有的策略的覆盖
configuration :自定义策略注入IOC时候所用到的类。
@SpringBootApplication
@EnableEurekaClient
//这样就可以为指定的Ribbon Client:SPRINGCLOUD-PROVIDER-DEPT服务采用TigerRuleConfig对应的ribbon配置
@RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT", configuration = TigerRuleConfig.class)
public class SpringcloudConsumerDept8080Application {
public static void main(String[] args) {
SpringApplication.run(SpringcloudConsumerDept8080Application.class, args);
}
}
注意事项
我们自定义的负载均衡策略的IOC注入是有要求的,它应该被应用程序上下文的@ComponentScan注解扫描到(不能再主程序的同级目录下),否则将被原有的载均衡策略所覆盖。所以我们应该在主程序的上级目录中进行自定义策略与IOC的注入。