Ribbon使用手册

如何实现负载均衡?

如何实现负载均衡?

  • 服务器端负载均衡
    • Nginx
  • 客户端侧负载均衡
    • 客户端编写负载均衡算法,动态选择微服务实例
  • 手写一个客户端侧负载均衡器

Ribbon 与 Spring Cloud Loadbalancer 现状

Ribbon

  • 成熟、流行
  • 被 Spring Cloud 列入维护模式

Spring Cloud Loadbalancer

  • 下一代客户端侧负载均衡器
  • 过于简陋
  • 暂时没有找到成功案例
  • 目前默认的负载均衡器依然是Ribbon

使用 Ribbon

加依赖

spring-cloud-starter-consul-discovery 中已经有了 spring-cloud-starter-netflix-ribbon 的依赖。

加注解 @LoadBalanced

@EnableCaching
@EnableScheduling
@SpringBootApplication
public class ZhibeiApplication {

    @PostConstruct
    void started() {
        // 设置时区为东八区
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
        // TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
    }

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

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

}

第三步,写代码

获取全部实例,并筛选

@RestController
public class TestController {

  @Autowired
  private DiscoveryClient discoveryClient;

  @GetMapping("/test")
  public List<ServiceInstance> test() {
    List<ServiceInstance> instances = discoveryClient.getInstances("micro-user");
    return instances.stream().filter(instance -> {
      Map<String, String> metadata = instance.getMetadata();
      String engineRoom = metadata.get("engineRoom");
      return "本地".equals(engineRoom);
    }).collect(Collectors.toList());
  }
}

使用 Ribbon 实现负载均衡

本质上就是再一个List中选择一个来调用。

// micro-class
@GetMapping("/test/{userId}")
public UserDTO test(@PathVariable(value = "userId") String userId) {
  return restTemplate.getForObject(
      "http://micro-user/users/{userId}",
      UserDTO.class,
      userId
  );
}
// micro-user
@GetMapping("/users/{id}")
public User findById(@PathVariable Integer id) {
  try {
    return userService.findByUserId(id);
  } catch (IllegalAccessException e) {
    e.printStackTrace();
    return null;
  }
}

Ribbon 核心组件

接口 作用 默认值
IClientConfig 读取配置 DefaultClientConfigImpl
IRule 负债均衡规则,选择实例 ZoneAvoidanceRule
IPing 筛选掉ping不通的实例 DummyPing
ServerList<Server> 交给 Ribbon 的实例列表 Ribbon:ConfigurationBasedServerList Spring Cloud Consul Discovery:ConsulServerList
ServerListFilter<Server> 过滤掉不符合条件的实例 ZonePerferenceServerListFilter
ILoadBalancer Ribbon 的入口 ZoneAwareLoadBalancer
ServerListUpdater 更新交给 Ribbon 的 List 的策略 PollingServerListUpdater

Ribbon 内置负载均衡规则

规则名称 特点
AvailabilityFilter 过滤掉一直连接失败的被标记为 circuit tripped 的后端 Server ,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来过滤 server 的逻辑,其实就是检查 status 里记录的各个 server 的运行状态
BestAvailableRule 选择一个最小的并发请求的 Server,逐个考察 Server,如果 Server 被 tripped 了,则跳过
RandomRule 随机选择一个 Server
RetryRule 对选定的负载均衡策略机制上重试机制,在一个配置时间段内当选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 server
RoundRobinRule 轮询选择
WeightedResponseTimeRule 根据响应时间加权,响应时间越长,权重越小,被选中的可能性就越低
ZoneAvoidanceRule 复合判断 Server 所在的 Zone 的性能和 Server 的可用性选择 Server,在没有 Zone 的环境下,类似于轮询(RoundRobinRule)

细粒度配置自定义

Java 代码方式

package cn.sbx0.micro.classes.configuration;

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

@Configuration
@RibbonClient(name = "micro-user", configuration = RibbonConfiguration.class)
public class MicroUserConfiguration {

}
package ribbon.configurantion;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfiguration {

  @Bean
  public IRule rule() {
    return new RandomRule();
  }
}

父子上下文的坑

注意 RibbonConfiguration 所在的包 ribbon.configurantionMicroClassApplication 所在的包 cn.sbx0.micro.classes 是同级的。如果将 RibbonConfiguration 放在 MicroClassApplication 可以扫描到的包路径,该 RibbonConfiguration 将变成全局配置,被所有 Ribbon 所共享,也就无法达到细粒度配置了。

配置属性方式

<clientName>.ribbon.NFLoadBalancerRuleClassName=负载规制的全路径
micro-user:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Java 代码方式 vs 配置属性方式

配置方式 优点 缺点
代码方式 基于代码,更加灵活 有小坑(父子上下文)
线上修改得重新打包、发布
配置属性 易上手
配置更加直观
线上修改无需重新打包、发布
优先级更高
极端情况下没有代码配置方式灵活
  • 尽量使用属性配置,属性配置方式实现不了得情况下再考虑使用代码配置方式
  • 在同一个微服务内尽量保持单一性
    • 比如统一使用属性配置,不建议两种方式混用,增加定位代码的复杂性

全局配置

有”聪明“的小伙伴已经猜到可以使用之前提到的 父子上下文 这一小坑来实现全局配置。但是,并不推荐使用这种方式,因为很可能导致整个项目启动都启动不起来。正确的全局配置方式是什么样的呢?

正确的全局配置

package cn.sbx0.micro.classes.configuration;

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

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

}

目前,全局配置只能使用 Java 代码来实现,不能使用配置属性的方式。

Ribbon 支持的配置项

Java 代码方式

package ribbon.configurantion;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfiguration {

  @Bean
  public IRule rule() {
    return new RandomRule();
  }

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

配置属性方式

<clientName>.ribbon 如下属性:

  • NFLoadBalancerClassName : ILoadBalancer 实现类
  • NFLoadBalancerRuleClassName : IRule 实现类
  • NFLoadBalancerPingClassName : IPing 实现类
  • NIWSServerListClassName : ServerList 实现类
  • NIWSServerListFilterClassName : ServerListFilter 实现类

饥饿加载

Ribbon 默认是懒加载,如何将它设置为饥饿加载呢?

ribbon:
  eager-load:
    enabled: true
    clients: micro-uesr,micro-class

优先调用同机房实例

package cn.sbx0.micro.classes.ribbon;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
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.util.CollectionUtils;

public class ShortestRule extends AbstractLoadBalancerRule {

  private final String TAG = "JF";

  @Autowired
  private ConsulDiscoveryProperties consulDiscoveryProperties;

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

  @Override
  public Server choose(Object o) {
    // 1. 获取想要调用微服务的实例列表
    ILoadBalancer loadBalancer = this.getLoadBalancer();
    List<Server> reachableServers = loadBalancer.getReachableServers();
    // 2. 筛选出机房相同的实例列表
    // 课程微服务所配置的tags
    // spring.cloud.consul.discovery.tags: engineRoom=local
    Map<String, String> metadata = consulDiscoveryProperties.getMetadata();
    List<Server> matchServer = reachableServers.stream().filter(server -> {
      ConsulServer consulServer = (ConsulServer) server;
      Map<String, String> consulServerMetadata = consulServer.getMetadata();
      return Objects.equals(metadata.get(TAG), consulServerMetadata.get(TAG));
    }).collect(Collectors.toList());
    // 3. 随机返回一个实例
    if (CollectionUtils.isEmpty(matchServer)) {
      return this.randomChoose(reachableServers);
    }
    return this.randomChoose(matchServer);
  }

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

import cn.sbx0.micro.classes.ribbon.ShortestRule;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfiguration {

  @Bean
  public IRule rule() {
    return new ShortestRule();
  }

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

元数据还可以做很多事情,比如控制微服务的版本,灰度发布。

在 yml 中的某个配置上 按 Ctrl + 单击 可以看到这个属性是在哪个类中定义的。

如何解决当某个微服务 down 机,导致调用失败的问题

  • 将 PolingServerListUpdater 更新 ServerList 定时任务周期缩短
# 五秒
<clientName>.ribbon.ServerListRefreshInterval = 5000

并不建议更改这个属性,因为可能会导致性能问题。

  • 配置 IPing ,将不可用的实例筛选掉

并不是每次调用之前都 ping 一下,而是定时去 ping 。

  • 直接使用 Consul API 直接从 Consul 查询
    package cn.sbx0.micro.classes.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.HealthServicesRequest;
    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 java.util.List;
    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.util.CollectionUtils;
    
    public class EveryTimeRule extends AbstractLoadBalancerRule {
    
      private final String TAG = "JF";
    
      @Autowired
      private ConsulDiscoveryProperties consulDiscoveryProperties;
    
      @Autowired
      private ConsulClient consulClient;
    
      @Override
      public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // 读取配置
      }
    
      @Override
      public Server choose(Object o) {
        // 1. 获取想要调用微服务的实例列表
        ILoadBalancer loadBalancer = this.getLoadBalancer();
        ZoneAwareLoadBalancer zoneAwareLoadBalancer = (ZoneAwareLoadBalancer) loadBalancer;
        // 想要调用的微服务的名称 micro-user
        String name = zoneAwareLoadBalancer.getName();
        List<String> tags = consulDiscoveryProperties.getTags();
        String tagName = tags.stream().filter(tag -> tag.startsWith(TAG)).findFirst().orElse(null);
        // 2. 筛选出机房相同的实例列表
        Response<List<HealthService>> serviceResponses = this.consulClient.getHealthServices(name,
            HealthServicesRequest.newBuilder().setTag(tagName).setPassing(true)
                .setQueryParams(QueryParams.DEFAULT).build());
        // 当前健康的微服务实例
        List<HealthService> healthServiceList = serviceResponses.getValue();
        // 3. 随机返回一个实例
        if (CollectionUtils.isEmpty(healthServiceList)) {
          healthServiceList = this.consulClient.getHealthServices(name,
              HealthServicesRequest.newBuilder().setTag(null).setPassing(true)
                  .setQueryParams(QueryParams.DEFAULT).build()).getValue();
        }
        List<ConsulServer> matchServer = healthServiceList.stream().map(ConsulServer::new)
            .collect(Collectors.toList());
        return this.randomChoose(matchServer);
      }
    
      public Server randomChoose(List<ConsulServer> servers) {
        int i = ThreadLocalRandom.current().nextInt(servers.size());
        return servers.get(i);
      }
    }

如果 Consul 挂了,这个就挂了。每次请求都要调用 Consul ,增加 Consul 的负载。

没有绝对完美的方案,只能根据实际项目来进行取舍。

参考文献

BAT架构师带你从零打造微服务项目