负载均衡原理
- 客户端负载均衡,通过LoadBalancerclient来实现,ILoadBalancer 通过配置IRule 和IPin 来实现
- ILoadBalancer 通过每10s 获取一次Eureka 注册地址,获取到注册列表后按照IRule 注册规则进行负载均衡
核心原理-拦截器
- ribbon 的核心其实就是代理,通过拦截器的方式
- 拦截器实现的功能1:通过对请求的拦截,获取url ,解析hostname ,通过hostname 再到Eureka 拿取真实的ip 端口,建立连接发送数据
- 拦截器实现的功能2:拿到目标服务的列表后,按照Rule规则选择具体的目标服务,从而实现负载均衡
负载均衡算法
- 预置可选择轮询算法:
- 轮询策略(RoundRobinRule):轮询;
- 随机策略(RandomRule):随机;
- 可用性敏感策略(AvailabilityFilteringRule):会先过滤掉由于多次访问故障而处于断路器状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问;
- 权重策略(WeightedResponseTimeRule):根据平均响应时间计算所有服务的权重,响应时间越快的服务权重越大被选中的概率越大。刚启动时如果统计信息不足,则使用RoundRobinRule(轮询)策略,等统计信息足够,会切换到WeightedResponseTimeRule;
- 重试策略(RetryRule):先按照RoundRobinRule(轮询)策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务;
- 最小连接数策略(BestAvailableRule):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务;
- 区域敏感策略(ZoneAvoidanceRule):复合判断Server所在区域的性能和Server的可用性选择服务器;
- 自定义算法
- 在主启动类加入@RibbonClient(name = “所要使用当前算法的微服务名称”,configuration = MySelfRule.class)代表使用自定义的java文件
- 在其他的包下建立算法类,需要符合IRule类的规则,需要extends AbstractLoadBalancerRule
- 复写Irule 接口的choose方法,将自己的算法写进去。
常用负载均衡策略和场景
- 轮循均衡(Round Robin)
- 原理:如果给服务器从 0 到 N 编号,轮询均衡策略会从 0 开始依次选择一个服务器作为处理本次请求的服务器
- 场景:适合所有服务器都有相同的软硬件配置,且请求频率相对平衡。
- 权重轮询均衡(Weighted Round Robin)
- 原理:按照服务器的不同处理能力,给服务器分配不同的权重,然后请求会按照权重分配给不同的服务器
- 场景:服务器的性能不同,充分利用高性能的服务器,同时也能照顾到低性能的服务器。
- 随机均衡(Random)
- 原理:将请求随机分配给不同的服务器。
- 场景:适合客户端请求的频率比较随机的场景。
- 响应速度均衡(Response Time)
- 原理:负载均衡设备对每个服务器发送一个探测请求,看看哪台服务器的响应速度更快,
- 场景:适合服务器的响应性能不断变化的场景。
- 注意:响应速度是针对负载均衡设备和服务器之间的。
负载均衡器 LoadBalancer(核心)
-
Ribbon实现负载均衡是通过ILoadBalancer接口来实现的
-
AbstractLoadBalancer是 ILoadBalancer 的接口抽象实现类,它有三个方法,维护不同的功能:
- chooseServer()
- 选择合适的实例并返回
- getServerList(ServerGroup var1)
- 根据分组的类型来维护不同的服务列表
- ALL 所有
- STATUS_UP 正常服务的实例
- STATUS_NOT_UP 停止服务的实例
- 根据分组的类型来维护不同的服务列表
- getLoadBalancerStats()
- 获取个个实力的服务当前属性和统计信息(这些信息是制定负载均衡策略的一句)
- chooseServer()
-
负载均衡器的基础实现类是 BaseLoadBalancer ,BaseLoadBalancer 的属性值如下:
-
除了继承AbstractLoadBalancer类的维护实例和实例状态容器的方法并重写后,BaseLoadBalancer还有几个属性和功能值得关注
- Pinger (服务实例状态监测机制类)
- IPing(检查接口是否存活)
- IRule(负载均衡规则)
- PingerStartegy (定义策略,去ping服务实例)
-
负载均衡器如何维护服务实例server列表
- ILoadBalancer是和eruka结合通过服务发现的方式来维护server列表的
- 负载均衡器扩展类DynamicServerListLoadBalancer实现服务实例清单在运行期的动态更新
- 在这个类里定义了服务实例操作对象ServerList,serverListImpl
- ServerList接口实现了初始化服务实例和更新服务实例的功能
- List getInitialListOfServers();
- List getUpdatedListOfServers();
-
负载均衡器如何更新服务实例
- 在 DynamicServerListLoadBalancer 类中有一个 serverListUpdater 对象实例属性,这个就是更新的 serverList,他有两个实现类来通过不同的策略实现serverList的更新
- PollingServerListUpdater
- 通过定时任务拉取服务列表(默认以这种策略为更新方式)
- EurekaNotificationServerListUpdater
- 通过eureka的Spring Event时间更新
- PollingServerListUpdater
- 在 DynamicServerListLoadBalancer 类中有一个 serverListUpdater 对象实例属性,这个就是更新的 serverList,他有两个实现类来通过不同的策略实现serverList的更新
-
如何从ServerList中挑选一个合适的服务实例
- 将返回的ServerList通过ServerListFilter过滤一次,返回满足条件的列表
- 根据Rule规则,结合LoadBalancerStats信息,挑选一个合适的服务实例
Java实现Ribbon
- 创建一个user-service模块
- 在pom.xml中添加相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 在application.yml进行配置
server:
port: 8201
spring:
application:
name: user-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/
- 添加实体User
public class User {
private Long id;
private String username;
private String password;
public User() {
}
public User(Long id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
- 添加统一返回对象CommonResult
public class CommonResult<T> {
private T data;
private String message;
private Integer code;
public CommonResult(){}
public CommonResult(T data, String message, Integer code) {
this.data = data;
this.message = message;
this.code = code;
}
public CommonResult(String message, Integer code) {
this(null, message, code);
}
public CommonResult(T data) {
this(data, "操作成功", 200);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
- 添加UserService
public interface UserService {
void create(User user);
User getUser(Long id);
void update(User user);
void delete(Long id);
User getByUsername(String username);
List<User> getUserByIds(List<Long> ids);
}
- 添加UserServiceImpl
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
private List<User> userList;
@Override
public void create(User user) {
userList.add(user);
}
@Override
public User getUser(Long id) {
List<User> findUserList = userList.stream().filter(userItem -> userItem.getId().equals(id)).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(findUserList)) {
return findUserList.get(0);
}
return null;
}
@Override
public void update(User user) {
userList.stream().filter(userItem -> userItem.getId().equals(user.getId())).forEach(userItem -> {
userItem.setUsername(user.getUsername());
userItem.setPassword(user.getPassword());
});
}
@Override
public void delete(Long id) {
User user = getUser(id);
if (user != null) {
userList.remove(user);
}
}
@Override
public User getByUsername(String username) {
List<User> findUserList = userList.stream().filter(userItem -> userItem.getUsername().equals(username)).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(findUserList)) {
return findUserList.get(0);
}
return null;
}
@Override
public List<User> getUserByIds(List<Long> ids) {
return userList.stream().filter(userItem -> ids.contains(userItem.getId())).collect(Collectors.toList());
}
@PostConstruct
public void initData() {
userList = new ArrayList<>();
userList.add(new User(1L, "zhangsan", "123456"));
userList.add(new User(2L, "lisi", "123456"));
userList.add(new User(3L, "wangwu", "123456"));
}
}
- 添加UserController用于提供调用接口
@RestController
@RequestMapping("/user")
public class UserController {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserService userService;
@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
userService.create(user);
return new CommonResult("操作成功", 200);
}
@GetMapping("/{id}")
public CommonResult<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
LOGGER.info("根据id获取用户信息,用户名称为:{}",user.getUsername());
return new CommonResult<>(user);
}
@GetMapping("/getUserByIds")
public CommonResult<List<User>> getUserByIds(@RequestParam List<Long> ids) {
List<User> userList= userService.getUserByIds(ids);
LOGGER.info("根据ids获取用户信息,用户列表为:{}",userList);
return new CommonResult<>(userList);
}
@GetMapping("/getByUsername")
public CommonResult<User> getByUsername(@RequestParam String username) {
User user = userService.getByUsername(username);
return new CommonResult<>(user);
}
@PostMapping("/update")
public CommonResult update(@RequestBody User user) {
userService.update(user);
return new CommonResult("操作成功", 200);
}
@PostMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
userService.delete(id);
return new CommonResult("操作成功", 200);
}
}
- 创建一个ribbon-server模块
- 在pom.xml中添加相关依赖
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
- 在application.yml进行配置
server:
port: 8301
spring:
application:
name: ribbon-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/
service-url:
user-service: http://user-service
- 使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 添加UserRibbonController类
- 注入RestTemplate,使用其调用user-service中提供的相关接口
@RestController
@RequestMapping("/user")
public class UserRibbonController {
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;
@GetMapping("/{id}")
public CommonResult getUser(@PathVariable Long id) {
return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
}
@GetMapping("/getByUsername")
public CommonResult getByUsername(@RequestParam String username) {
return restTemplate.getForObject(userServiceUrl + "/user/getByUsername?username={1}", CommonResult.class, username);
}
@GetMapping("/getEntityByUsername")
public CommonResult getEntityByUsername(@RequestParam String username) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(userServiceUrl + "/user/getByUsername?username={1}", CommonResult.class, username);
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult("操作失败", 500);
}
}
@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/create", user, CommonResult.class);
}
@PostMapping("/update")
public CommonResult update(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/update", user, CommonResult.class);
}
@PostMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
return restTemplate.postForObject(userServiceUrl + "/user/delete/{1}", null, CommonResult.class, id);
}
}
- 启动eureka-server于8001端口
- 启动user-service于8201端口和8202端口
- 启动ribbon-server于8301端口
- 调用接口进行测试:http://localhost:8301/user/1
- 可以发现运行在8201和8202的user-service控制台交替打印如下信息:
ribbon常用配置
- 全局配置
ribbon:
#服务请求连接超时时间(毫秒)
ConnectTimeout: 1000
#服务请求处理超时时间(毫秒)
ReadTimeout: 3000
#对超时请求启用重试机制
OkToRetryOnAllOperations: true
#切换重试实例的最大个数
MaxAutoRetriesNextServer: 1
# 切换实例后重试最大次数
MaxAutoRetries: 1
#修改负载均衡算法
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- 指定服务进行配置
user-service:
ribbon:
#服务请求连接超时时间(毫秒)
ConnectTimeout: 1000
#服务请求处理超时时间(毫秒)
ReadTimeout: 3000
#对超时请求启用重试机制
OkToRetryOnAllOperations: true
#切换重试实例的最大个数
MaxAutoRetriesNextServer: 1
# 切换实例后重试最大次数
MaxAutoRetries: 1
#修改负载均衡算法
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule