简介
负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
一个没有负载均衡的 web 架构类似下面这样:
上述图例中,用户通过网络访问 Web 服务器,如果服务器宕机了,那么整个系统都无法使用。另外如果有很多用户同时访问该服务器,由于服务器的性能有限,用户可能会遇到加载缓慢或根本无法连接的情况。
如果此时在后端增加一个负载均衡器和至少一个额外的 Web 服务器,则可以缓解该故障。一般来说,后端服务器会采用 A(一致性) 原则,提供相同的数据内容,以便于用户无论访问到哪台服务器得到一致的数据。
如上图所示,多台服务器提供服务,用户发送请求到负载均衡器,经由负载均衡器转发给任一服务器,这样加大了系统的负载能力,还提升了性能。
目前业界主流的负载均衡方案可分为两类:
集中式负载均衡:即在 consumer 和 provider 中间使用独立的负载均衡设施(可以是硬件,如F5,也可以是软件,如:Nginx),由该设施把访问请求通过某种策略转发到 provider。
进程内负载均衡:将负载均衡的逻辑集成到 consumer,consumer 从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择一个合适的 provider。
关于负载均衡分类想要深入了解的同学,可以参考负载均衡技术原理及分类
Ribbon 属于后者,它只是一个类库,集成在 consumer 中,consumer 通过它来获得合适的 provider 地址。
两种负载均衡方式架构图
Spring Cloud Ribbon 是一个基于 Http 和 TCP 的客户端负载均衡工具,它是基于Netflix Ribbon实现的。它不像服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个微服务的基础设施中。理解 Ribbon 对于我们使用 Spring Cloud 来讲非常的重要,因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。
Ribbon 可以基于某种负载均衡算法,如轮询(默认)、随机、加权轮询、加权随机等自动帮助服务消费者调用接口, 甚至包含自定义的负载均衡算法。
在 Spring Cloud 中,有两种服务调用方式,一种是 Ribbon+RestTemplate,另一种是 Feign。
当 Ribbon 与 Eureka 配合使用时,Ribbon 可自动从 Eureka Server 获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。
我们继续复用前两节讲到的项目,在此基础上进行扩展,首先我们回忆一下之前的项目。
父模块:
接下来我们针对 springcloud-consumer-dept-80 项目进行修改。
1、导入依赖
org.springframework.cloud
spring-cloud-starter-ribbon
1.4.6.RELEASE
org.springframework.cloud
spring-cloud-starter-eureka
1.4.6.RELEASE
2、修改 application.yml
server:
port: 80
#eureka配置
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001:7001/eureka/,http://eureka7002:7002/eureka/,http://eureka7003:7003/eureka/
3、配置负载均衡
@Configuration
public class MyConfig {
//配置负载均衡实现RestTemplate
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
4、修改 DeptConsumerController 文件中的服务调用名称
// private static final String REST_URL_PREFIX = "http://localhost:8001";
//通过服务名进行调用
private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT";
上述代码中的 SPRINGCLOUD-PROVIDER-DEPT 为服务提供者在注册中心注册的名称,如下图所示:
5、入口类上增加注解@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer_80 {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer_80.class,args);
}
}
6、启动3个服务注册中心项目,以及服务提供者和服务消费者,访问 http://localhost/consumer/dept/list,页面正常显示数据。
新建两个数据库,分别为 db01 和 db02,以及同样的 dept 表,SQL 语句如下:
CREATE DATABASE `db01`;
USE `db01`;
CREATE TABLE `dept` (
`deptId` bigint(20) NOT NULL AUTO_INCREMENT,
`dpName` varchar(60) NOT NULL,
`dbSource` varchar(60) NOT NULL,
PRIMARY KEY (`deptId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into dept (dpname, dbsource) values ('开发部',DATABASE());
insert into dept (dpname, dbsource) values ('人事部',DATABASE());
insert into dept (dpname, dbsource) values ('财务部',DATABASE());
insert into dept (dpname, dbsource) values ('市场部',DATABASE());
insert into dept (dpname, dbsource) values ('后勤部',DATABASE());
数据库建立完毕,我们再来新建两个服务提供者,复制 springcloud-provider-dept-8001 项目,修改名称为 springcloud-provider-dept-8002 和 springcloud-provider-dept-8003,注意修改项目中配置文件、入口类和 mapper 映射文件。
每个服务提供者对应连接一个数据库,示例如下:
server:
port: 8002
spring:
datasource:
username: root
password: mysql521695
url: jdbc:mysql://localhost:3306/db01?useUnicode=true&useSSL=true&serverTimezone=UTC&characterEncoding=utf-8
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: springcloud-provider-dept
mybatis:
type-aliases-package: com.msdn.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
#Eureka配置
eureka:
client:
service-url:
defaultZone: http://eureka7001:7001/eureka/,http://eureka7002:7002/eureka/,http://eureka7003:7003/eureka/
instance:
instance-id: springcloud-provider-dept-8002 # 修改eureka上的默认描述信息
prefer-ip-address: true # true,可以显示服务的IP地址
#info配置
info:
app.name: hresh-springcloud
company.name: blog.csdn.net/Herishwater
目前项目结构如下:
启动3个注册中心,3个服务提供者,最后启动服务消费者,首先访问 http://eureka7001:7001/,页面内容如下:
然后再访问 http://localhost/consumer/dept/list,不断刷新该页面,可以看到查询到的数据在变化。
// RoundRobinRule 轮询,默认策略,按序获取provider
// RandomRule 随机
// AvailabilityFilteringRule : 可用性敏感策略,会先过滤掉,跳闸,访问故障的服务~,对剩下的进行轮询~
// RetryRule : 会先按照轮询获取服务~,如果服务获取失败,则会在指定的时间内进行,重试
//WeightedResponseTimeRule 权重轮询策略,根据每个Provider的响应时间分配一个权重,响应时间越长权重越小,被选中的可能性就越低,初次会使用轮询策略,直到分配好权重
//BestAvailableRule 最少并发数策略,选择正在请求中并发数量小的provider,除非这个provider在熔断中
如果想要更换负载均衡策略,在本例中,需要修改服务消费者项目中的 MyConfig 配置文件,内容如下:
@Bean
public IRule getRule(){
return new RandomRule();
}
增加对 RandomRule 类的注入,使得 Ribbon 优先选择随机策略。
在实际生产中,我们可能需要根据公司实际需求设计专门的负载均衡策略,接下来我们来设计相关代码。
新建一个文件夹,结构如下:
模仿随机策略的类进行修改,自定义 MyRandomRule 类
public class MyRandomRule extends AbstractLoadBalancerRule {
private int count = 0; //每个服务执行次数
private int providerNum = 0; //当前哪个服务被执行
public MyRandomRule(){
}
@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 upList = lb.getReachableServers(); //获得活着的服务
List allList = lb.getAllServers(); //获得全部的服务
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
//核心部分
// int index = this.chooseRandomInt(serverCount);
// server = (Server)upList.get(index);
//我们定义属于自己的执行策略,目前我们有3个provider,那么决定每个provider执行3次,然后接着执行下一个provider
if(count<3){
server = (Server)upList.get(providerNum);
count++;
}else{
count = 0;
providerNum++;
if(providerNum>=serverCount){
providerNum = 0;
}
server = (Server)upList.get(providerNum);
}
if (server == null) {
Thread.yield();
} else {
if (server.isAlive()) {
return server;
}
server = null;
Thread.yield();
}
}
return server;
}
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
RuleConfig
@Configuration
public class RuleConfig {
@Bean
public IRule getRule(){
return new MyRandomRule();
}
}
最后在入口类上增加注解,启动该策略。
@SpringBootApplication
@EnableEurekaClient
//在微服务启动时去加载我们自定义的负载均衡策略
@RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT",configuration = RuleConfig.class)
public class DeptConsumer_80 {
public static void main(String[] args) {
SpringApplication.run(DeptConsumer_80.class,args);
}
}
关于上述 myrule 文件夹的位置,原因在于:RuleConfig 不能在主应用程序上下文的@ComponentScan
中,否则将由所有@RibbonClients
共享。如果您使用@ComponentScan
(或@SpringBootApplication
),则需要采取措施避免包含(例如将其放在一个单独的,不重叠的包中,或者指定要在@ComponentScan
)。
spring cloud feign
基于Netfix Feign
实现,整合了spring cloud Ribbon
和spring cloud Hystrix
, 是声明式的 web service 客户端,它让微服务之间的调用变得更简单了,类似 controller 调用 service。SpringCloud 集成了 Ribbon 和 Eureka,可在使用 Feign 时提供负载均衡的 http 客户端。
我们在上一节讲述 Ribbon 的使用时,利用它对 RestTemplate 的请求拦截来实现对依赖服务的接口调用,而 RestTemplate 已经实现了对 http 请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以我们需要针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用,模块化的代码重复出现在每一个客户端中,代码量会比较大。
比如我们之前案例中使用到的 springcloud-consumer-dept-80 项目,其中的 DeptConsumerController 类封装了对 http 请求的处理方法,如果我们此时再新建一个服务消费者,同样需要处理 dept 对象的相关逻辑,则需要复制一份 DeptConsumerController 代码。
而 spring cloud feign
在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在spring cloud feign
的实现下,我们只需创建一个接口并调用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用spring cloud ribbon
时自动封装服务调用客户端的开发量。
首先我们需要在 springcloud-api 项目中增加实现了 Feign 的接口类。
1、导入依赖
org.projectlombok
lombok
org.springframework.cloud
spring-cloud-starter-feign
1.4.6.RELEASE
2、增加一个 service 接口类
DeptFeignService
package com.msdn.service;
import com.msdn.pojo.Dept;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
@Service
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT")
public interface DeptFeignService {
@PostMapping("/dept/add")
boolean addDept(@RequestBody Dept dept);
@GetMapping("/dept/get/{id}")
Dept queryDept(@PathVariable("id") long id);
@GetMapping("/dept/list")
List queryAll();
}
参考 springcloud-consumer-dept-80 项目新建一个名为 springcloud-consumer-dept-80-feign 的 maven 项目,大体内容不变,修改少许内容。
1、导入依赖
com.msdn
springcloud-api
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
org.springframework.cloud
spring-cloud-starter-feign
1.4.6.RELEASE
org.springframework.cloud
spring-cloud-starter-ribbon
1.4.6.RELEASE
org.springframework.cloud
spring-cloud-starter-eureka
1.4.6.RELEASE
2、DeptConsumerController 文件
@RestController
public class DeptConsumerController {
@Autowired
DeptFeignService service;
@RequestMapping("/consumer/dept/get/{id}")
public Dept getDept(@PathVariable("id") long id) {
return service.queryDept(id);
}
@RequestMapping("/consumer/dept/list")
public List queryAll() {
return service.queryAll();
}
@RequestMapping(name = "/consumer/dept/add")
public boolean addDept(Dept dept) {
return service.addDept(dept);
}
}
3、入口类增加 EnableFeignClients 注解
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class FeignDeptConsumer_80 {
public static void main(String[] args) {
SpringApplication.run(FeignDeptConsumer_80.class,args);
}
}
4、启动3个注册中心,3个服务提供者,然后启动新的服务消费者,访问 http://localhost/consumer/dept/list,多次刷新该页面,发现 consumer 的调用顺序是按序执行的,即轮询策略。
详情代码访问: https://github.com/Acorn2/springcloud-eureka ,如有疑惑可联系我。
深入理解负载均衡
什么是负载均衡?
面试官:说说你知道的几种负载均衡分类
spring cloud feign学习一:快速入门
Spring-Cloud-Ribbon学习笔记(一):入门
Spring-Cloud-Eureka-学习笔记
SpringCloud学习1-服务注册与发现(Eureka)