Eureka中的负载均衡

Eureka中的负载均衡

前言

Eureka是Netflix开发的一种用于构建分布式系统中的微服务架构的服务发现组件。

Ribbon是一个基于HTTP和TCP协议的客户端负载均衡工具,它是Netflix开源的一个组件,旨在提供客户端侧负载均衡算法和服务实例的自动发现功能,以实现对服务调用的负载均衡。

Spring官方提供了一个Spring Cloud LoadBalancer来替代Ribbon它是基于Netflix Ribbon的一个新的客户端负载均衡组件,提供更加灵活的扩展性和可配置性。

而在我们的Eureka中实现服务的远程调用则是通过Spring Cloud LoadBalancer来完成。本文将结合源码分析一下Euraka拉取服务以及远程调用服务的过程。

Eureka与Ribbon的关系

Eureka组件只提供服务的注册和发现功能,它本身并不具备负载均衡的功能。但我们使用Eureka组件时却能实现服务请求的处理和分配(负载均衡),正是因为Eureka中集成了Ribbon负载均衡组件。

Ribbon是Netflix开源的一种用于构建分布式系统中的微服务架构的客户端负载均衡组件。Ribbon支持多种负载均衡算法,例如轮询、随机、加权轮询、加权随机等,并且支持自定义负载均衡算法。Ribbon可以与Eureka、Consul等服务发现组件集成,也可以与Spring Cloud LoadBalancer等负载均衡器集成,提供更加灵活和可扩展的负载均衡功能。

为什么要使用Eureka

现在有一个案例:在分布式的架构中,有两个节点order-service以及user-service。

在order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回。

image-20230706170322077

如果上图所示,我们在order-service服务中需要想user-service中发起一个请求,才能取到对应的用户信息。

要实现上述需求,我们可以这样做

  1. 在order-service中注册RestTemplate

    RestTemplate是Spring框架提供的一个用于发送HTTP请求的客户端库,主要用于调用RESTful Web服务时发送请求并接收响应。它可以发送GET、POST、PUT、DELETE等HTTP请求,并接收相应的响应。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package cn.itcast.order.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;

    /**
    * @Author: minster
    * @PACKAGE_NAME: cn.itcast.config
    * @NAME: RestConfiguration
    * @DATE: 2023/7/4
    */
    @Configuration
    public class RestConfiguration {

    @Bean
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }

    }
  2. 修改order-service中的order业务方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Resource
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
    // 1. 查询订单
    Order order = orderMapper.findById(orderId);
    // 2. 远程查询user信息
    String url = "http://localhost:8081/user/" + order.getUserId();
    // 3. 通过restTemplate发起调用
    User user = restTemplate.getForObject(url, User.class);
    // 4. 封装数据到order
    order.setUser(user);
    // 5. 返回
    return order;
    }

测试:

image-20230706171645353

如下图所示,我们的user-service提供了接口给外部使用,即服务提供者。而order-service使用了user-service,即服务消费者。

服务提供者与服务消费者是相对的,一个服务既有可能是服务提供者,也有可能是服务消费者

image-20230706141607328

虽然上述代码实现了需求,但是我们可以发现以下问题:

  • order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口?
  • 有多个user-service实例地址,order-service调用时该如何选择?
  • order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

上述问题都需要利用Spring Cloud中的注册中心来解决,其中Eureka便是一个注册中心。

下面我们使用一个案例,来演示基于Eureka组件的服务注册与发现以及服务调用请求的分配,分析Eureka是怎么解决上述三个问题的。

案例的架构如下:

image-20230706102828818

实现代码

新建项目eureka-server

image-20230706173731285

导入服务端依赖

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

编写配置信息。

1
2
3
4
5
6
7
8
9
10
# 这里配置了eureka-server的端口号
server:
port: 10086
spring:
application:
name: eureka-server # applicatio-name: 其实就是在注册中心的服务名字
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka

开启EurekaServer服务功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.itcast;

import com.netflix.discovery.EurekaNamespace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
* @Author: minster
* @PACKAGE_NAME: cn.itcast
* @NAME: EurekaApplication
* @DATE: 2023/7/5
*/
@SpringBootApplication
// @EnableEurekaServer 可以开启EurekaServer服务
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}

访问:Eureka

image-20230706174019102

我们可以发现eureka-server服务已经成功注册到我们的Eureka注册中心

同时,我们将order-service、user-service服务也注册到我们的Eureka注册中心

步骤如下:

  1. 导入客户端依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 编写配置文件

    1
    2
    3
    4
    5
    6
    7
    spring:
    application:
    name: userservice
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10086/eureka

注册步骤一样,这里不再演示。

同时,我们多启动一个user-service,构成一个user服务集群。

当前服务启动情况如下图所示:

image-20230706175121795

刷新一下Eureka,可以发现,我们都客户端服务都注册到我们的Eureka中

image-20230706175230190

下面我们修改order-service中的业务代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Resource
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
// 1. 查询订单
Order order = orderMapper.findById(orderId);
// 2. 远程查询user信息
String url = "http://userservice/user/" + order.getUserId();
// 3. 通过restTemplate发起调用
User user = restTemplate.getForObject(url, User.class);
// 4. 封装数据到order
order.setUser(user);
// 5. 返回
return order;
}

然后在order-service中的RestTemplate配置类中给Bean添加@LoadBalanced注解

1
2
3
4
5
6
7
8
9
10
@Configuration
public class RestConfiguration {

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

}

然后调用order服务接口,查看两个user-service中的控制台的日志

image-20230706175757006

我们可以发现,两个user-service服务都被调用到了!

Eureka是怎么做到的呢?我们可以通过下图分析

image-20230706180452345

在我们上述案例中,我们通过配置客户端配置文件中的

1
2
3
4
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka

分别将我们的order-service、user-service注册到注册中心当中去。

当我们需要调用的时候,我们的服务消费者只需要去注册中心拉取我们的服务信息,然后再进行远程调用即可,这也就解释我们上面的第一个问题:order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口的

然后当我们的服务注册到Eureka注册中心之后,我们的注册中心会定期检查我们服务的一个健康状态的,通常是30s,这样也就能解决我们上述的第三个问题:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

然后对于第二个问题:有多个user-service实例地址,order-service调用时该如何选择?在这里,我们可以看到在RestTemplate中加了一个注解@LoadBalanced,这个注解的作用其实就是做了一个负载均衡,具体的原理,如下图所示:

image-20230706195504888

Ribbon负载均衡在SpringCloud中的实现(源码分析)

在上述案列中,我们只加了一个@LoadBalanced注解,就实现了负载均衡,这个注解到底做了什么呢?

下面我们通过源码追踪,分析一下SpringCloud是如何实现Ribbon负载均衡的。

我们先定位到@LoadBalanced注解所在的包,发现在同包下有一个LoadBalancerInterceptor 类。

image-20230706200720713

顾名思义,这是一个负载均衡的拦截器,我们以这个类为起点,进行调试,源码追踪

进入到我们的LoadBalancerInterceptor 类中,发现其中有一个intercept 方法,我们打个断点,调试

image-20230706201039313

可以发现,intercept方法将我们order-service中发送到服务提供者user-service的请求拦截下来了,并且解析了我们的请求url,获取我们服务提供者的主机名。

我们继续追踪,查看loadBalancer.execute方法到底做了什么,追踪到LoadBalancerClient的一个实现类RibbonLoadBalancerClient

image-20230706201714764

我们可以发现,主机名:userservice作为参数传递到了getLoadBalancer方法中,我们追踪该方法,看一下里面到底做了什么。

image-20230706204336667

getLoadBalancer 方法最终返回了一个Feign Client实例,通过这个Feign Client我们可以从Eureka中拉取对应我们的serviceId对应的服务,也就是userservice服务。

在使用Feign Client进行服务调用时,Feign Client会通过负载均衡器来选择要调用的服务实例。

我们继续往下调试

image-20230706204944251

在通过getLoadBalancer(serviceId) 方法获取到的返回值loadBalancer中,可以看到我们的userservice对应的端口!也就是说我们的拉取服务是在这里完成的。

我们继续追踪源码,看一个execute 方法中的getServer()到底做了什么。可以发现,后面getServer()会调用到BaseLoadBalancer类下的chooseServer方法中。

image-20230706210502196

我们将这个方法分成两部分来看,分别解析:

第一部分:

在这里,我们做了一个计数器的自增操作,因为我们在我们的负载均衡的轮询策略中,是按照一定的顺序来依次执行的。每次选择服务实例时,计数器会自增1,从而实现轮询的效果。

例如,假设我们有三个服务A,B,C,我们的负载均衡采取了轮询的策略来进行服务的调用。

当我们进行第一次进行服务选择时,负载均衡器会选择我们的第0个服务,也就是A;

当我们进行第二次进行服务选择时,负载均衡器会选择我们的第1个服务,也就是B;

当我们进行第三次进行服务选择时,负载均衡器会选择我们的第2个服务,也就是C;

当我们进行第四次进行服务选择时,由于已经选择了所有的服务实例,负载均衡器会重新从第0个服务实例开始选择。

第二部分:

在这里,我们会根据指定的负载均衡策略对应的算法,对我们的服务进行一个选择操作,如果我们没有指定负载均衡策略,则会使用Ribbon默认的负载均衡策略——轮询。

上述的负载均衡策略是通过rule获取的,我们可以看一下,rule的类型以及实现类

image-20230706212556355

拓展:IRule的实现类及其代表的含义如下

  1. RoundRobinRule(轮询规则):按照顺序依次选择可用的服务器进行请求分发,实现简单的轮询负载均衡。
  2. WeightedResponseTimeRule(加权响应时间规则):根据每个服务器的平均响应时间和权重,选择一个性能较好的服务器进行请求分发。具有更低响应时间和较高权重的服务器将有更高的概率被选中。
  3. RandomRule(随机规则):随机选择一个可用的服务器进行请求分发。具有平均负载分布的特点,适用于无需特定负载均衡策略的场景。
  4. RetryRule(重试规则):在请求失败时,会尝试重新选择一个可用的服务器进行重试。该规则适用于具有容错需求的场景,可以增加系统的可靠性。
  5. AvailabilityFilteringRule(可用性过滤规则):根据服务器的可用性和运行状态,选择一个可用的服务器进行请求分发。它会排除掉那些因为故障或超负荷而不可用的服务器。
  6. BestAvailableRule(最佳可用规则):选择一个可用服务器中并发连接最低的服务器进行请求分发。这有助于减少请求被发送到负载较高的服务器上。
  7. ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。

总结

在最后,我们可以发现,我们使用SpringCloud整合Eureka注册中心实现服务的注册发现与处理分配时,其实就是通过我们的LoadBalancerInterceptor将我们的要调用的服务拦截下来,并获取我们服务请求url中的主机名,然后通过Feign Client和负载均衡器到我们的Eureka注册中心拉取服务列表。

拉取到服务列表之后,我们会调用getServer()方法去执行我们的负载均衡策略,通过我们设定的负载均衡策略(默认是轮询),执行对应的负载均衡算法,从而选择一个服务列表中选择一个适合的服务给我们的服务消费者使用 。