负载均衡

负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

一个没有负载均衡的 web 架构类似下面这样:

应用负载均衡硬件_负载均衡

在这里用户直连 web 服务器,如果这个服务器宕机了,那么用户自然也就没办法访问了。另外,如果同时有很多用户试图访问服务器,超过了其能处理的极限,就会出现加载速度缓慢或根本无法连接的情况。

而通过在后端引入一个负载均衡器和至少一个额外的 web 服务器,可以缓解这个故障。通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。

应用负载均衡硬件_spring cloud_02

Spring Cloud 入门 ---- Ribbon 负载均衡组件
简介

Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡工具。简单的说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件中列出 Load Balancer(简称LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(如:简单轮询,随机连接等)去连接这些机器。我们很容易使用 Ribbon 实现自定义的负载均衡算法。

官网:https://github.com/Netflix/ribbon/wiki

概念:

LB 负载均衡 (Load Balance) 是什么

简单的所就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件 F5等。

Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡的区别

Nginx 是服务端负载均衡,客户端的所有请求都会交给 nginx,然后由 nginx 实现转发请求。即负载是由服务端实现的。

Ribbon 本地负载均衡,在调用微服务接口时,会在注册中心上获取服务的注册信息列表之后缓存到 JVM 本地【即将服务别名 以及对应的提供服务的机器信息缓存到本地,当调用服务时根据负载均衡策略选择一个提供服务的机器并调用它】,从而在本地实现 RPC 远程服务调用技术。

LB分类:

  • 集中式LB

即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,如F5,也可以是软件,如 nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

  • 进程内LB

将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,他只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

总结:

Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。

Eureka + Ribbon 架构图:

以 EurekaServer 作为注册中心,当服务提供者启动时,会将自身服务信息【IP,port等】以别名的方式注册到 EurekaServer【同一种服务别名一样】,服务消费者会从 EurekaServer 查询服务列表【服务别名与其下的节点信息】并缓存到本地,当消费者需要调用服务提供者的服务时,会根据负载均衡策略选择一个节点并调用它;

应用负载均衡硬件_应用负载均衡硬件_03

Ribbon 在工作时分为两步:

第一步:选择EurekaServer,它优先选择在同一个区域内负载较少的server。

第二步:在根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权等。

创建演示项目

为了方便我们介绍和演示 Ribbon 负载均衡的效果,我们需要创建 服务消费者 与 服务提供者模块【集群-两个节点】,至于注册中心就用之前的 Eureka 即可。

创建服务提供者

引入 pom 依赖,由于这次使用了数据库,所以需要引入 db 相关依赖

<!-- 引入 eureka client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

创建 yaml 配置文件,由于要演示负载均衡效果,为了启动两个服务,我们这里需要配置两个配置文件,由于 application-one.ymlapplication-two.yml 配置差不多一样,我们给出 application-one.yml 完整的配置,而 application-two.yml 只给出不一样的配置。

application-one.yml

server:
  port: 8010

spring:
  application:
    name: ribbon-payment-provider
  security:
    # 配置spring security登录用户名和密码
    user:
      name: akieay
      password: 1qaz2wsx
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3600/cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500


eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: payment8010
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30


#mybatis-plus
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.akieay.cloud.rbpayment.entity;
  configuration:
    #是否开启驼峰命名自动映射
    map-underscore-to-camel-case: true
    #全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
    cache-enabled: false
    #指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法
    call-setters-on-nulls: true
    #指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

application-two.yml

server:
  port: 8011
  
eureka:
 instance:
    instance-id: payment8011

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@MapperScan(value = "com.akieay.cloud.rbpayment.mapper")
public class RibbonPaymentApplication {

    public static void main(String[] args) {
        SpringApplication.run(RibbonPaymentApplication.class, args);
    }
}

业务类,这里只介绍主要相关的业务类,其它细节请自行补充,或直接去 gitee 上拉取代码查阅。

应用负载均衡硬件_应用负载均衡硬件_04

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {

    @Override
    public int create(UserEntity userEntity) {
        return baseMapper.insert(userEntity);
    }

    @Override
    public UserEntity getById(int id) {
        return baseMapper.selectById(id);
    }
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class UserEntity implements Serializable {

    private static final long serialVersionUID=1L;

    /**
     * 用户ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 创建时间
     */
    private Date createTime;
}
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${server.port}")
    private String serverPort;

    @PostMapping(value = "/create")
    public CommonResult create(@RequestBody UserEntity userEntity) {
        int result = userService.create(userEntity);
        log.info("*****插入结果:"+result);

        if (result > 0) {
            return new CommonResult(200, "插入数据库成功, serverPort:"+serverPort, result);
        } else {
            return new CommonResult(444, "插入数据库失败");
        }
    }

    @GetMapping(value = "/get/{id}")
    public CommonResult<UserEntity> getUserById(@PathVariable("id") Long id) {
        UserEntity userEntity = userService.getById(id);
        log.info("*****查询结果:"+userEntity);

        if (null != userEntity) {
            return new CommonResult(200, "查询数据成功, serverPort:"+serverPort, userEntity);
        } else {
            return new CommonResult(400, "没有对应的记录,查询ID:"+id);
        }
    }
}

创建两个启动服务,具体细节已经在前面的注册中心模块做了介绍,这里就不介绍了

应用负载均衡硬件_负载均衡_05

应用负载均衡硬件_spring cloud_06

创建成功后启动服务,打开 Eureka 注册中心,输入账号密码登录上去,即可看到 提供者模块已经注册进入了 Eureka 注册中心,如下图:

应用负载均衡硬件_应用负载均衡硬件_07

至此 服务提供者创建完成

创建服务消费者

引入 pom 依赖,由于该版本的 eureka 自带了 ribbon 依赖,所以我们不需要单独引入 ribbon 依赖。

应用负载均衡硬件_客户端_08

<!-- 引入 eureka client -->
<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: 80
spring:
  application:
    name: ribbon-order-consumer
  security:
    # 配置spring security登录用户名和密码
    user:
      name: akieay
      password: 1qaz2wsx

eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: order80
    prefer-ip-address: true

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class RibbonOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(RibbonOrderApplication.class, args);
    }
}

配置类

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced  //赋予默认的负载均衡规则
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

业务类

@RestController
@Slf4j
public class UserConcumerController {

    /**
     * payment-provider:服务别名
     */
    public static final String PAYMENT_URL = "http://RIBBON-PAYMENT-PROVIDER";

    @Resource
    private RestTemplate restTemplate;

    /**
     * getForObject
     * @param id
     * @return
     */
    @GetMapping("/consumer/user/getObject/{id}")
    public CommonResult getObject(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
    }

}

至此,服务消费者基础模块完成,启动服务,打开 Eureka 注册中心,即可看到服务已经注册到了其中,并且访问:http://localhost//consumer/user/getObject/1 可以发现使用默认的 轮询 负载均衡规则轮流调用服务提供者 8010 与 8011。

应用负载均衡硬件_负载均衡_09

至此,演示模块的基础创建完成。

Ribbon 负载均衡与 RestTemplate 调用

由于我们该模块是使用的 Ribbon + RestTemplate 来实现的负载均衡与服务调用,所以这里先介绍一下 RestTemplate ;RestTemplate 是一个 HTTP 客户端工具,主要为我们调用 Http 接口提供方便的,支持 GET、POST、PUT、DELEFT …等方法,我们这里主要介绍 GET 与 POST。

GET 请求

主要是 getForObject 与 getForEntity 方法,两者的区别为:

  • getForObject 方法:返回对象为响应体中数据转换成的对象,基本可以理解为Json【在前面的案例中我们已经使用过了,具体的返回类型根据我们给定的 class 类型确定】。
  • getForEntity 方法:返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如:响应头、响应状态码、响应体等。

演示:

修改服务消费者的业务类:UserConcumerController,由于之前已经存在了 getForObject 的调用,我们这只需要添加 getForEntity 方法的调用即可。

/**
 * getForEntity
 * @param id
 * @return
 */
@GetMapping("/consumer/user/getEntity/{id}")
public CommonResult getEntity(@PathVariable("id") Long id) {
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
    // 打印响应code 与 响应头信息
    log.info(entity.getStatusCode() + "  ----   " + entity.getHeaders());
    if (entity.getStatusCode().is2xxSuccessful()) {
        return entity.getBody();
    } else {
        return new CommonResult(444, "操作失败");
    }
}

重启服务,并分别调用 http://localhost//consumer/user/getObject/1 与 http://localhost//consumer/user/getEntity/1 可以发现两个返回给客户端的响应结果一致,也就是说 getForEntity 的响应实体内容 entity.getBody() 与 getForObject 返回的内容一致。这就验证了我们之前所说的 getForEntity 与 getForObject 的不同之处在于,getForEntity 不仅会返回相应的实体数据,还会返回响应状态码,响应头信息等,如下面我们控制台打印的这条信息。

应用负载均衡硬件_spring_10

应用负载均衡硬件_客户端_11

应用负载均衡硬件_spring_12

POST 请求

主要是 postForObject 与 postForEntity 方法,两者区别为: postForEntity 方法可以设置 请求的 header 属性,而 postForObject 不能指定请求的 header,当需要指定 header 的属性值的时候,使用postForEntity方法。

演示:

新增 insertObjectinsertEntity 方法,我们在 insertEntity 中设置了请求头信息

@PostMapping("/consumer/user/insertObject")
public CommonResult insertObject(@RequestBody Map<String, Object> user) {
    return restTemplate.postForObject(PAYMENT_URL + "/user/create", user, CommonResult.class);
}

@PostMapping("/consumer/user/insertEntity")
public CommonResult insertEntity(@RequestBody Map<String, Object> user) {
    HttpHeaders headers = new HttpHeaders();
    //添加请求头参数 认证token
    headers.add("Authorization", "Bearer 05ce151b-8574-4411-a9c2-baaa4cd17e59");
    headers.add("Content-Type", "application/json");

    HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(user,headers);
    ResponseEntity<CommonResult> responseEntity = restTemplate.postForEntity(PAYMENT_URL + "/user/create", httpEntity, CommonResult.class);

    log.info(responseEntity.getStatusCode() + "  ----   " + responseEntity.getStatusCodeValue() + "  ----   " + responseEntity.getHeaders());
    return responseEntity.getBody();
}

重启服务,并测试这两个接口;可以看到返回结果差不多;postForEntitypostForObject 的不同之处除了可以设置 headers 外,返回的结果也不一样,postForObject 只会返回响应实体部分,而 postForEntity 会返回 响应码、响应头、响应体等信息;

postForObject

应用负载均衡硬件_应用负载均衡硬件_13

postForEntity

应用负载均衡硬件_spring_14

应用负载均衡硬件_spring_15

至此关于RestTemplate 的介绍结束,至于其它的请求方法的介绍请自行查阅。

Ribbon自带的负载均衡规则
Ribbon的核心组件IRule

根据特定算法从服务器列表中选取一个要访问的服务

应用负载均衡硬件_spring cloud_16

自带的负载均衡算法
  • RoundRobinRule: 轮询获取服务实例。
  • RandomRule: 随机获取服务实例。
  • RetryRule: 先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。
  • WeightedResponseTimeRule: 对于 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择。
  • BestAvailableRule: 会先过滤由于多次访问故障而处于熔断跳闸状态的服务,然后选择一个并发量最小的服务。
  • AvailabilityFilteringRule: 先过滤掉故障实例,再选择并发较小的实例。
  • ZoneAvoidanceRule: 采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。
替换负载均衡规则

替换负载均衡规则需要我们自定义一个负载均衡规则,这个规则的放置的位置是有限制的;官方文档明确给出警告:这个自定义配置类不能放在 @ComponentScan 所扫描的当前包以及其子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。即替换的负载均衡规则如果放置在启动类包或其子包下时,这个规则对所有服务生效。

应用负载均衡硬件_spring cloud_17

正确的放法如下:

应用负载均衡硬件_spring cloud_18

自定义负载均衡规则:

@Configuration
public class MySelfRule {
    
    /**
     * 随机
     * @return
     */
    @Bean
    public IRule myRule(){
        return new RandomRule();
    }
}

修改主启动,添加注解 @RibbonClient

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name = "RIBBON-PAYMENT-PROVIDER", configuration = MySelfRule.class)
public class RibbonOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(RibbonOrderApplication.class, args);
    }
}

name:服务别名 configuration:替换的负载均衡规则;重启服务,访问:http://localhost/consumer/user/getObject/1 多次访问,通过返回消息中的端口,我们可用看到访问规则变成了 随机,有的时候 8010 经常出现,有的时候 8011 经常出现访问完全随机。至于其它的负载均衡策略这里不做介绍,可自行修改测试。

另一种修改负载均衡规则的方式【yml 配置】:注释掉启动类上的负载均衡规则配置【避免影响】,yml 新增配置如下:

// 注意服务名与 Eureka 保持一致
RIBBON-PAYMENT-PROVIDER:
  ribbon:
    ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
    ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
    OkToRetryOnAllOperations: true #对超时请求启用重试机制
    MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
    MaxAutoRetries: 1 # 切换实例后重试最大次数
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法

若是需要配置全局的负载均衡规则,将之前我们自定义的负载均衡策略 MySelfRule 添加到 主启动所在的包 或 其子包下即可;

Ribbon 默认负载均衡策略【RoundRobinRule】原理

rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次重启服务后,rest 接口计数重置为1。

List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER")

如:List[0] instanceA = 127.0.0.1:8011 List[1] instanceB = 127.0.0.1:8010

8010 + 8011 组合成为集群,它们共计 2 台机器,集群总数为 2,按轮询算法原理:

当请求总数为 1 时: 1 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010

当请求总数为 2 时: 2 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011

当请求总数为 3 时: 3 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010

当请求总数为 4 时: 4 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011

如此类推… 服务重启后重置为 1。

RoundRobinRule 源码分析

public class RoundRobinRule extends AbstractLoadBalancerRule {

    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;

    private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

    public RoundRobinRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        setLoadBalancer(lb);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            //获取可达的机器
            List<Server> reachableServers = lb.getReachableServers();
            //获取所有机器
            List<Server> allServers = lb.getAllServers();
            //可达的机器数量
            int upCount = reachableServers.size();
            //总机器数量
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            //获取下一次调用服务器的下标[CAS + 自旋锁]
            int nextServerIndex = incrementAndGetModulo(serverCount);
            //获取下一次调用的服务机器
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}

nextServerCyclicCounter:记录接口第几次请求。

以上为 RoundRobinRule 负载均衡的源码,其核心是 choose 方法用来选择提供服务的机器;该方法会首先判断 lb 【负载均衡器】是否为空,若为空抛出异常,然后 while 循环 10 次获取 server 若获取到不为空的 server 则跳出循环,返回 server,否则循环 10 次后,抛出没有可用服务器的异常;

获取 server 的流程:获取可达【正常可用】的服务机器列表,获取总服务机器列表;若两者有一方为空,则抛出 没有可用服务 的异常;通过 CAS + 自旋锁 获取下一个可用的服务节点的下标【轮询算法是通过 接口请求次数 % 服务节点数量 = 访问的节点下标】,通过下标获取下一个可用的服务节点,若服务节点能够正常使用则返回,否则将其置空 进入下一次循环判断。

Ribbon—手写轮询算法

服务提供者 新增业务,然后重启服务

/**
* 自定义轮询负载均衡规则测试
* @return
*/
@GetMapping("/lb")
public String getUserLb(){
    return "self RoundRobinRule 访问端口:" + serverPort;
}

服务消费者 手写轮询算法

首先为了避免原有的负载均衡规则的影响;需要注释掉原有的 负载均衡配置

应用负载均衡硬件_负载均衡_19

应用负载均衡硬件_spring cloud_20

自定义负载均衡规则

public interface LoadBalancer {

    /**
     * 从服务主机列表根据 负载均衡规则 获取主机
     * @param serviceInstances
     * @return
     */
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
@Component
public class SelfRoundRobinRule implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        } while (!this.atomicInteger.compareAndSet(current, next));
        System.out.println("第几次访问,访问次数为:" + next);
        return next;
    }

    /**
     * 轮询负载均衡算法:rest接口第几次请求 % 服务器集群总数量 = 实际调用服务器位置下标, 每次服务重启动后rest接口计数从1开始
     * @param serviceInstances
     * @return
     */
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}
@Resource
private SelfRoundRobinRule selfRoundRobinRule;
@Resource
private DiscoveryClient discoveryClient;

@GetMapping("/consumer/user/lb")
public String getUserLb() {
    //获取指定服务的节点列表
    List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER");
    if (null == instances || instances.size() <= 0) {
        return null;
    }

    ServiceInstance instance = selfRoundRobinRule.instances(instances);
    return restTemplate.getForObject(instance.getUri() + "/user/lb", String.class);
}

重启服务,测试访问:http://localhost/consumer/user/lb ,可以看到 8010 与 8011 轮流调用;

应用负载均衡硬件_客户端_21

应用负载均衡硬件_spring_22