06 实现负载均衡 ribbon

如何实现负载均衡
服务器端负载均衡

比如NGINX --> 通过NGINX负载均衡

客户端侧负载均衡

在课程微服务中获取到所有的用户微服务的所有实例 在课程微服务中提供的算法来使用其中一个用户微服务

其中课程微服务相对于用户微服务来说处于客户端测,因此称为客户端侧负载均衡

Ribbon & Spring Cloud Loadbalancer对比与选择

都是客户端侧负载均衡

Ribbon现状

  1. 成熟 流行
  2. 维护模式(由于之前维护不给力 不会提供新功能 只会维护)

Spring Cloud Loadbalancer

  1. 下一代客户端侧负载均衡器

  2. 过于简陋 目前只提供了轮询算法的负载均衡模式

  3. 暂时没有找到成功案例

  4. 目前默认的负载均衡器依然是Ribbon

手写一个客户端负载均衡器

随机的负载均衡器

修改课程微服务如下:

package com.cloud.msclass.service;

import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;

@Service
public class LessonService {

	private static final Logger logger = LoggerFactory.getLogger(LessonService.class);

	@Autowired
	private LessonRepository lessonRepository;
	@Autowired
	private LessonUserRepository lessonUserRepository;

	@Autowired
	private RestTemplate restTemplate;

	@Autowired
	private DiscoveryClient discoveryClient;

	public Lesson buyById(Integer id) {
		// 1. 根据id查询lesson
		Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
		// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
		LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
		if (lessonUser != null) {
			return lesson;
		}
		// TODO  登录实现后需重构
		Integer userId = 1;
		// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功

		List<ServiceInstance> instances = this.discoveryClient.getInstances("ms-user");

		//		List<ServiceInstance> njInstances = instances.stream().filter(instance -> {
		//			Map<String, String> metadata = instance.getMetadata();
		//			String jiFang = metadata.get("JIFANG");
		//			if ("NJ".contentEquals(jiFang)) {
		//				return true;
		//			}
		//			return false;
		//		}).collect(Collectors.toList());

		int i = ThreadLocalRandom.current().nextInt(instances.size());

		// TODO 需要改进
		URI uri = instances.get(i).getUri();

		logger.info("当前选择的用户微服务实例的地址是 = {}", uri);

		UserDTO userDTO = restTemplate.getForObject(uri + "users/{userId}", UserDTO.class, userId);
		BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
		if (money.doubleValue() < 0) {
			throw new IllegalArgumentException("余额不足");
		}
		// TODO 购买逻辑 ... 1. 调用用户微服务的扣减金额接口  2.向lesson_user表插入数据
		return lesson;
	}
}

启动多个用户微服务:
在这里插入图片描述

调用课程微服务:http://localhost:8010/lesssons/buy/1

image-20200216141650103.png

使用Ribbon实现负载均衡

Netflix开源的客户端侧负载均衡器
在这里插入图片描述

添加注解

	/**
	 * spring web提供的轻量级http client
	 * @return
	 */
	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

修改代码

package com.cloud.msclass.service;

import java.math.BigDecimal;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;

@Service
public class LessonService {

	@Autowired
	private LessonRepository lessonRepository;
	@Autowired
	private LessonUserRepository lessonUserRepository;

	@Autowired
	private RestTemplate restTemplate;

	public Lesson buyById(Integer id) {
		// 1. 根据id查询lesson
		Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
		// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
		LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
		if (lessonUser != null) {
			return lesson;
		}
		// TODO  登录实现后需重构
		// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
		Integer userId = 1;
		UserDTO userDTO = restTemplate.getForObject("http://ms-user/users/{userId}", UserDTO.class, userId);
		BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
		if (money.doubleValue() < 0) {
			throw new IllegalArgumentException("余额不足");
		}
		// TODO 购买逻辑 ... 1. 调用用户微服务的扣减金额接口  2.向lesson_user表插入数据
		return lesson;
	}
}

Ribbon核心组件

在这里插入图片描述
Screenshot_20200216_150633_com.tencent.edu.jpg
Screenshot_20200216_151053_com.tencent.edu.jpg

Ribbon配置自定义-Java代码配置方式

将负载均衡规则改为随机,负载均衡可以细到微服务级别(针对特定的微服务采用特定的负载均衡算法)

package com.cloud.ribbonconfiguration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

@Configuration
public class RibbonConfiguration {

	@Bean
	public IRule ribbonRule() {
		return new RandomRule();
	}
}
package com.cloud.msclass.configuration;

import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;

import com.cloud.ribbonconfiguration.RibbonConfiguration;

/**
 * 设置用户微服务适用随机负载均衡策略
 * @project ms-class
 * @author guanglai.zhou
 * @date 2020年2月16日
 */
@Configuration
@RibbonClient(name = "ms-user", configuration = RibbonConfiguration.class)
public class MsUserRibbonConfiguration {

}

在这里插入图片描述

需要为ribbon配置文件单独定义一个包

现在课程微服务有两个上下文,一个是应用的主上下文(applicationContext),一个是ribbon的上下文,这个是子上下文,父子上下文扫描的包一旦重叠,会导致各种奇葩的问题。在xml配置spring的时代,springframework上下文是父上下文,而springmvc上下文是子上下文,一旦扫描包重叠,就可能导致事务不生效。只需要让spring不扫描包含controller注解的类,而spring-mvc只扫描包含@Controller注解的包,避免重复扫描

参考博客:https://blog.csdn.net/qq_32588349/article/details/52097943

Ribbon配置自定义-配置属性配置方式
  1. 注释掉代码定义
package com.cloud.msclass.configuration;

import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;

import com.cloud.ribbonconfiguration.RibbonConfiguration;

/**
 * 设置用户微服务适用随机负载均衡策略
 * @project ms-class
 * @author guanglai.zhou
 * @date 2020年2月16日
 */
//@Configuration
//@RibbonClient(name = "ms-user", configuration = RibbonConfiguration.class)
public class MsUserRibbonConfiguration {

}

  1. 在配置文件中添加如下配置
ms-user:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
细粒度配置最佳实践总结

代码配置:

基于代码,更加灵活

有小坑(父子上下文)、线上修改得重新打包、发布

属性配置

易上手 配置更加直观 线上修改无需重新打包、发布(配置consul可以实现自动刷新)、优先级更高

极端情况下没有代码配置方式灵活

  1. 尽量使用属性配置,属性方式实现不了再考虑使用代码配置

  2. 在同一个微服务内尽量保持单一性 比如统一使用属性配置 不建议两种方式混用 增加定位代码的复杂性

Ribbon全局配置

把ribbon配置类放到启动类所在的包或者子包,可以实现全局配置,但强烈不推荐使用,毕竟使父子上下文重叠是一种异常情况。事实上,很多时候都会导致应用无法启动。

首先注释掉之前的配置

#ms-user:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
package com.cloud.msclass.configuration;

import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;

import com.cloud.ribbonconfiguration.RibbonConfiguration;

@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class GlobalRibbonConfiguration {

}

目前ribbon的全局配置只能通过java代码的方式实现,不能通过配置方式来实现

Ribbon支持的配置项

Screenshot_20200216_154903_com.tencent.edu.jpg

package com.cloud.ribbonconfiguration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.RandomRule;

@Configuration
public class RibbonConfiguration {

	@Bean
	public IRule ribbonRule() {
		return new RandomRule();
	}
	
//	@Bean
//	public IPing ping() {
    //  在同一台设备上 导致无法找到服务 ping不通 ?
//		return new PingUrl();
//	}
}

在这里插入图片描述

饥饿加载

当第一次访问http://localhost:8010/lesssons/buy/1时,速度比较慢,以后再次访问,速度就比较快了。

因为ribbon默认情况下是懒加载的,当以下代码在第一次调用的时候:

UserDTO userDTO = restTemplate.getForObject("http://ms-user/users/{userId}", UserDTO.class, userId);

才会创建一个名为ms-user的ribbonClient,而创建那些ribbon的类需要时间

可以通过饥饿加载来解决上面的问题

ribbon:
  eager-load:
    enabled: true      # 开启饥饿加载
    clients: ms-user   # 更细粒度 但是目前不支持通配符 如果不配置 则全部使用饥饿加载
扩展Ribbon-优先调用同机房实例
  1. 同机房优先调用
  2. 随机负载均衡算法

首先在课程微服务中添加配置

spring.cloud.consul.discovery.tags JIFANG=NJ

定义一个RibbonRule的实现类

package com.cloud.msclass.ribbon;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServer;
import org.springframework.cloud.consul.discovery.ConsulServerUtils;
import org.springframework.util.CollectionUtils;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

public class MyRibbonRule extends AbstractLoadBalancerRule {

	@Autowired
	private ConsulDiscoveryProperties consulDiscoveryProperties;

	public static final String JIFANG = "JIFANG";

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig) {
		// 读取配置
	}

	@Override
	public Server choose(Object key) {
		// 1. 获得想要调用微服务的实例列表
		ILoadBalancer loadBalancer = this.getLoadBalancer();
		List<Server> servers = loadBalancer.getReachableServers();
		// 2. 筛选出机房相同的实例列表
		// 可以拿到课程微服务所配置的tag
		List<String> tags = consulDiscoveryProperties.getTags();
		// 课程微服务所配置的元数据
		Map<String, String> metadata = ConsulServerUtils.getMetadata(tags);
		List<Server> jiFangMatchServers = servers.stream().filter(server -> {
			ConsulServer consulServer = (ConsulServer) server;
			Map<String, String> targetMetadata = consulServer.getMetadata();
			return Objects.equals(metadata.get(JIFANG), targetMetadata.get(JIFANG));
		}).collect(Collectors.toList());
		// 3. 随机返回1个实例
		if(CollectionUtils.isEmpty(jiFangMatchServers)) {
			return this.randomChoose(servers);
		}
		return this.randomChoose(jiFangMatchServers);
	}

	private Server randomChoose(List<Server> servers) {
		int i = ThreadLocalRandom.current().nextInt(servers.size());
		return servers.get(i);
	}
}

配置RibbonRule:

package com.cloud.ribbonconfiguration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.cloud.msclass.ribbon.MyRibbonRule;
import com.netflix.loadbalancer.IRule;

@Configuration
public class RibbonConfiguration {

	@Bean
	public IRule ribbonRule() {
		return new MyRibbonRule();
	}

	//	@Bean
	//	public IPing ping() {
	//		return new PingUrl();
	//	}

}

对于以上实现存在一个漏洞,当在负载均衡时获取到一个服务器,但该服务器突然挂机(或者下线一段时间内),会导致请求失败

解决上面报错的解决思路(3种)

  1. 主要原因是ServerList的更新不及时。因此将PollingServerListUpdater更新ServerList的定时任务周期缩短

    配置 .ribbon.ServerListRefreshInterval=30000 实际项目中还是需要谨慎使用的 主要是系统性能的问题 即使这个时间设置的再小 仍然存在时间间隔

  2. 配置IPing,将不可用的实例删选掉,这种方法也不能彻底解决问题,因为ribbon不是调用之前去进行ping的,而是定时ping的;另一个时机是当ribbon感知到配置发生变化的时候

  3. 使用Consul API直接从Consul查询 可以彻底解决问题

编码解决问题

package com.cloud.msclass.ribbon;

import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.QueryParams;
import com.ecwid.consul.v1.Response;
import com.ecwid.consul.v1.health.model.HealthService;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAwareLoadBalancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServer;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

public class MyRibbonRuleV2 extends AbstractLoadBalancerRule {
	@Autowired
	private ConsulDiscoveryProperties consulDiscoveryProperties;
	@Autowired
	private ConsulClient consulClient;

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig) {
	}

	@Override
	public Server choose(Object key) {
		// 1. 获得想要调用微服务的实例列表
		ILoadBalancer lb = this.getLoadBalancer();
		ZoneAwareLoadBalancer loadBalancer = (ZoneAwareLoadBalancer) lb;
		// 想要调用的微服务的名称(ms-user)
		String name = loadBalancer.getName();
		// 本地配置的tag
		List<String> tags = consulDiscoveryProperties.getTags();
		// 筛选出JIFANG=NJ这条数据
		String jiFangTag = tags.stream().filter(tag -> tag.startsWith("JIFANG")).findFirst().orElse(null);
		// 2. 筛选出机房相同的实例列表
		Response<List<HealthService>> serviceResponse = this.consulClient.getHealthServices(name, jiFangTag, true,
				QueryParams.DEFAULT);
		// 当前健康的用户微服务实例
		List<HealthService> healthServices = serviceResponse.getValue();
		if (CollectionUtils.isEmpty(healthServices)) {
			Response<List<HealthService>> allHealthServiceResponse = this.consulClient.getHealthServices(name, null,
					true, QueryParams.DEFAULT);
			healthServices = allHealthServiceResponse.getValue();
		}
		List<ConsulServer> consulServers = healthServices.stream().map(ConsulServer::new).collect(Collectors.toList());
		if (CollectionUtils.isEmpty(consulServers)) {
			return null;
		}
		// 3. 随机返回1个实例
		return this.randomChoose(consulServers);
	}

	private Server randomChoose(List<ConsulServer> servers) {
		int i = ThreadLocalRandom.current().nextInt(servers.size());
		return servers.get(i);
	}
}

以上实现存在如下几个问题:

  1. 性能问题 持续请求consul服务器
  2. 不使用本地缓存 如果consul服务器崩溃 则课程微服务会失败 此时要保证Consul的高可用
  3. 最后使用以上方案仅适用于consul,不属于通用方案

因此没有完美的方案 根据项目要求进行权衡选择

本章总结
  1. 负载均衡的两种方式
  2. Ribbons是什么,如何使用
  3. Ribbon核心组件
  4. 配置自定义
  5. 如何扩展?
发布了21 篇原创文章 · 获赞 1 · 访问量 325

猜你喜欢

转载自blog.csdn.net/m0_37607945/article/details/104543228
今日推荐