负载均衡 LoadBalance

问题引入

我们一个服务可能会进行多机部署,也就说多台服务器组成的集群共同对外提供一致的服务,那么我们的微服务的代码就需要拷贝多份,部署到不同的机器上。

我们使用 IDEA 来开启多个相同的服务

这里以 product-service 为例:

找到 services 按键,点开来:

找到我们需要多机部署的服务,右键然后点击 Copy Configuration ,复制这个服务的所有配置。

之后就是给我们新的服务命名,然后点击 Modify options 修改配置信息。

点击 Add VM options

在 VM options 添加端口信息:

-Dserver.port=端口号

注意由于我们是在本机部署多个服务,所以端口号需要修改,避免端口的冲突

最后点击 Apply ,然后 OK,就可以创建成功了。


然后我们启动所有的服务,在 Eureka 界面可以看到我们的 product-service 有多个注册信息

这里使用 order-service 来注册发现 product-service:

java 复制代码
@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    public OrderInfo getOrderById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectById(orderId);
//        String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
        //从 eureka 中获取服务信息
        List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
        String uri = instances.get(0).getUri().toString();
        String url = uri + "/product/" + orderInfo.getProductId();
        log.info("访问的资源路径:" + url);
        ProductDetailInfo productDetailInfo = restTemplate.getForObject(url, ProductDetailInfo.class);
        orderInfo.setProductDetailInfo(productDetailInfo);
        return orderInfo;
    }
}

但是我们发现多个请求,即使 product-service 有三个一样的服务,但是使用的都是 9092,如果我们的请求类一旦上升,就可能会导致 9092这个服务器崩溃,我们应该要做到均衡地将这些请求发送给 product-service 的三个不同的服务器中,这就是我们本章要提到的负载均衡

负载均衡

概念

负载均衡(Load Balance,简称LB),是高并发, 高可用系统必不可少的关键组件.

当服务流量增大时,通常会采用增加机器的方式进行扩容,负载均衡就是用来在多个机器或者其他资源中,按照一定的规则合理分配负载.

分类

负载均衡分为服务端负载均衡和客户端负载均衡

服务端负载均衡,主要使用的是负载均衡器 Nginx,请求先到达负载均衡器,然后通过负载均衡算法在多个服务器之间选择一个进行访问


客户端负载均衡:

将负载均衡的功能以库的方式集成到客户端中,而不是由一台负载均衡设备集中提供

模拟实现

这里我们使用原子类,避免发生线程不安全,通过原子类的数值和我们获取到的服务注册列表的长度进行取余获取下标,以轮询的方式来访问 product-service 服务端。

java 复制代码
@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    private static final AtomicInteger count = new AtomicInteger(1);

    private List<ServiceInstance> instances;

    @PostConstruct
    public void init() {
        instances = instances = discoveryClient.getInstances("product-service");
    }

    public OrderInfo getOrderById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectById(orderId);
//        String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
        int index = count.getAndIncrement() % instances.size();
        String uri = instances.get(index).getUri().toString();
        String url = uri + "/product/" + orderInfo.getProductId();
        log.info("访问的资源路径:" + url);
        ProductDetailInfo productDetailInfo = restTemplate.getForObject(url, ProductDetailInfo.class);
        orderInfo.setProductDetailInfo(productDetailInfo);
        return orderInfo;
    }
}

但是这个实现方式也有缺陷,就是如果后续有新的服务注册或者旧的服务崩溃的话,我们的 order-service 就不会获得到最新的注册列表,导致后续出现 bug

即使你采用下面的方式,每次 order-service 处理请求都要进行重新获取服务列表,也还是会出现 bug ,那就是如果旧的服务崩溃,可能无法即使获取,导致出现ConnectException

java 复制代码
java.net.ConnectException: Connection refused: connect
java 复制代码
@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    private static final AtomicInteger count = new AtomicInteger(1);

//    private List<ServiceInstance> instances;
//
//    @PostConstruct
//    public void init() {
//        instances = instances = discoveryClient.getInstances("product-service");
//    }

    public OrderInfo getOrderById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectById(orderId);
//        String url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();
        List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
        int index = count.getAndIncrement() % instances.size();
        String uri = instances.get(index).getUri().toString();
        String url = uri + "/product/" + orderInfo.getProductId();
        log.info("访问的资源路径:" + url);
        ProductDetailInfo productDetailInfo = restTemplate.getForObject(url, ProductDetailInfo.class);
        orderInfo.setProductDetailInfo(productDetailInfo);
        return orderInfo;
    }
}

LoadBalance

SpringCloud从2020.0.1版本开始,移除了Ribbon组件,使用Spring Cloud LoadBalancer组件来代替Ribbon实现客户端负载均衡

使用

添加注解 @LoadBalanced

在 RestTemplate 上添加 @LoadBalanced

将 RestTemplate 交给 LoadBalance 管理

java 复制代码
@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

修改远程调用的代码

修改 String url = "http://product-service/product/" + orderInfo.getProductId();

将 ip 和端口号修改为 服务名称【product-service】,这样 LoadBalance会自动为我们提供服务端

java 复制代码
@Slf4j
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    public OrderInfo getOrderById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectById(orderId);
        String url = "http://product-service/product/" + orderInfo.getProductId();
        log.info("访问的资源路径:" + url);
        ProductDetailInfo productDetailInfo = restTemplate.getForObject(url, ProductDetailInfo.class);
        orderInfo.setProductDetailInfo(productDetailInfo);
        return orderInfo;
    }
}

负载均衡策略

负载均衡策略是一种思想,无论是哪种负载均衡器,它们的负载均衡策略都是相似的 SpringCloud

LoadBalancer仅支持两种负载均衡策略:轮询策略和随机策略

1.轮询(RoundRobin):轮询策略是指服务器轮流处理用户的请求.这是一种实现最简单,也最常用的策略.生活中也有类似的场景,比如学校轮流值日,或者轮流打扫卫生.

2.随机选择(Random):随机选择策略是指随机选择一个后端服务器来处理新的请求

自定义负载均衡策略

SpringCloud LoadBalancer默认负载均衡策略是轮询策略,实现是RoundRobinLoadBalancer,如果服务的消费者如果想采用随机的负载均衡策略,也非常简单。

官方链接:SpringCloud LoadBalancer

java 复制代码
public class CustomLoadBalancerConfiguration {
	@Bean
	ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
															LoadBalancerClientFactory loadBalancerClientFactory) {
		String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
		return new RandomLoadBalancer(loadBalancerClientFactory
				.getLazyProvider(name, ServiceInstanceListSupplier.class),
				name);
	}
}

CustomLoadBalancerConfiguration 这个类不用加类注解【@Configuration】
因为这个类是在组件的扫描范围内

java 复制代码
@LoadBalancerClient(name = "product-service", configuration = CustomLoadBalancerConfiguration.class)
@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

我们在 RestTemplate 上添加 @LoadBalancerClient 注解,将服务名称和负载策略填写进去