Spring Cloud学习|第二篇:负载均衡-Ribbon

1.简介

负载均衡是指将负载分摊至多个执行单元上,常见的负载均衡有如下两种

1.服务器负载均衡.如Nginx:通过Nginx负载均衡策略,将请求转发至后端服务,如下图所示

在这里插入图片描述

​ 2.客户端负载均衡:以代码的形式封装至服务消费者服务上,消费者维护一份服务提供者信息列表,通过负载均衡策略将分摊给多个服务提供者,从而达到负载均衡目的
在这里插入图片描述

2.使用RestTemplate与Ribbon进行消费服务

在上一篇基础上完成该案例演示,服务具体信息如下表所示

服务名服务端口作用
eureka-server8761注册中心
eureka-client8762,8763服务提供者
eureka-ribbon-client8764负载均衡客户端
  • 新建服务eureka-ribbon-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>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
  • 配置application.yml

    spring:
      application:
        name: eureka-ribbon-client
    
    server:
      port: 8764
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    
  • 书写启动类

    注意启动类中需加载RestTemplate

    @EnableEurekaClient
    @SpringBootApplication
    public class EurekaRibbonClientApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(EurekaRibbonClientApplication.class, args);
      }
    
      @LoadBalanced
      @Bean
      public RestTemplate restTemplate(){
        return new RestTemplate();
      }
    }
    
  • 写一个Restful API接口,用于远程调用87628763两个服务

    @Service
    public class RibbonService {
    
      @Resource
      private RestTemplate restTemplate;
    
      public String hi(String name){
        return restTemplate.getForObject("http://eureka-client/hi?name={1}", String.class, name);
      }
    }
    
  • 启动注册中心、两个服务提供者以及ribbon-client-service服务,查看注册中心
    在这里插入图片描述

  • 访问ribbon-client-service服务,会轮流输出两个服务提供者信息,表示负载均衡起作用了

3.LoadBalancerClient介绍

负载均衡器的核心类为LoadBalancerClient,我们可以通过LoadBalancerClient控制访问的服务,在此,新增一接口"/testLoadBalancer",通过LoadBalancerClient访问服务提供者。

  • 新增接口

    @RestController
    public class EurekaRibbonClient {
    
     @Resource
     private LoadBalancerClient loadBalancerClient;
    
     @GetMapping("/testLoadBalancer")
     public String testRibbon() {
       ServiceInstance instance = loadBalancerClient.choose("eureka-client");
       return instance.getHost() + ":" + instance.getPort();
     }
    }
    
  • 启动服务,访问http://localhost:8764/testLoadBalancer,输出结果如下

    127.0.0.1:8762
    127.0.0.1:8763
    
  • Ribbon禁止从Eureka注册中心获取服务注册信息,而是自己维护服务实例列表

    (1)配置application.yml

    ribbon: # 禁止Ribbon从Euraka获取注册信息
      eureka:
        enabled: false
    stores: # 设置本地服务注册信息
      ribbon:
        listOfServers: example.com,goole.com
    
    

    (2)书写程序

    @RestController
    public class EurekaRibbonClient {
    
      @Resource
      private LoadBalancerClient loadBalancerClient;
    
      @GetMapping("/testLoadBalancer")
      public String testRibbon() {
        ServiceInstance instance = loadBalancerClient.choose("stores");
        return instance.getHost() + ":" + instance.getPort();
      }
    }
    

    (3) 启动工程,访问http://localhost:8764/testLoadBalancer,结果展示如下

    example.com:80
    google.com:80
    
  • 结论

    (1) Ribbon通过LoadBalancerClient从注册中心获取所有注册服务信息,并缓存至Ribbon本地

    (2) LoadBalancerClient的choose根据传入的serviceId从注册服务列表中获取服务实例信息(ip及端口)

    (3) 如果禁止Ribbon从Eureka获取注册列表信息,则需自己维护一份服务注册列表信息,根据自己维护的注册列表信息,实现负载均衡

4.Ribbon源码简单分析

  • Ribbon负载均衡过程

    (1) 通过LoadBalancerAutoConfiguration类中如下代码,维护一个RestTemplate列表,同时初始化时,给每个restTemplate对象增加一个拦截器

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    

    (2) 拦截器主要对每个请求路径进行解析,最后将解析出的serviceId(serviceName)将给LoadBalancerClient处理

    @Override
    	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    			final ClientHttpRequestExecution execution) throws IOException {
    		final URI originalUri = request.getURI();
    		String serviceName = originalUri.getHost();
    		Assert.state(serviceName != null,
    				"Request URI does not contain a valid hostname: " + originalUri);
    		return this.loadBalancer.execute(serviceName,
    				this.requestFactory.createRequest(request, body, execution));
    	}
    

    (3) LoadBalancerClient则会根据传入的serviceId获取服务注册列表、缓存服务注册列表、检测服务注册列表是否变化、ping下游服务是否可用、根据配置的负载均衡策略进行服务调用

  • LoadBalancerClient实现功能过程

    (1) 核心类LoadBalancerClient的choose()最终通过ILoadBalancer的实现类DynamicServerListLoadBalancer实现

    (2) DynamicServerListLoadBalancer构造方法中需要初始:IClientConfigIRuleIPingServerListServerListFilter五个属性

    属性类作用
    IClientConfig获取配置负载均衡客户端
    IRule配置负载均衡策略
    IPing检测负载均衡的服务是否可用
    ServerList获取注册中心服务注册列表
    ServerListFilter根据配置去过滤或动态获取符合条件的server列表方法

    (3) 负载均衡策略

    负载均衡策略的选择是根据IRule子类完成的,默认走的是轮询策略,常见的几个实现类如下表所示:

    类名作用
    BestAvailableRule选择最小请求数
    ClientConfigEnabledRoundRobinRule轮询
    RandomRule随机
    RetryRule根据轮询方式重试
    WeightedResponseTimeRule根据响应时间分配权重,权重越低,被选择可能性越低
    ZoneAvoidanceRule根据server的zone区域和可用性来轮训选择

    IRule有3个方法,分别是choose(serviceId),setLoadBalancer(),getLoadBalancer()三个方法,分别是根据servcieId获取服务实例信息,设置和获取ILoadBalancer

    (4) 检测要负载均衡的服务是否可用(IPing),Iping通过其子类完成服务检测,主要由如下几个子类实现:

    类名作用
    PingUrl真实地去ping某个url
    PingConstant固定返回某个服务是否可用,默认为true
    NoOpPing不去ping,直接返回true,即可用
    DummyPing直接返回true,并实现了initWithNiwsConfig方法
    NIWSDiscoveryPing根据DiscoveryEnabledServer的instanceInfo的InstanceStatus去判断,如果InstanceStatus.UP,则可用,否则不可用

    (5) 获取注册服务列表(serverList)

    方法名作用
    DynamicServerListLoadBalancer构造函数初始化上述所需属性
    DynamicServerListLoadBalancer->initWithNiwsConfig()初始化信息
    initWithNiwsConfig->restOfInit->updateListOfServers()获取服务注册列表
    updateListOfServers->serverListImpl.getUpdatedListOfServers()具体获取注册列表服务实现
    serverListImpl实现ServerList,实现类DiscoveryEnabledServer->obtainServersViaDiscovery->eurekaClientProvider.get()获取Eureka中注册的服务
    LegacyEurekaClientProvider->get()具体实现

    最终获取服务列表代码

    class LegacyEurekaClientProvider implements Provider<EurekaClient> {
    
        private volatile EurekaClient eurekaClient;
    
        @Override
        public synchronized EurekaClient get() {
            if (eurekaClient == null) {
                eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
            }
    
            return eurekaClient;
        }
    }
    

    (6) 负载均衡获取服务服务注册时间
    BaseLoadBalancer构造函数中有一方法setupPingTask(),方法具体实现如下,根据如下代码可知,每隔10秒向EurekaClient发送一次心跳检测。

    void setupPingTask() {
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                                           true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
        forceQuickPing();
    }
    

    (7) 查看定时任务代码,分析心跳检测具体实现
    查看pingTask()方法,主要实现逻辑在runPinger(),而在runPinger()方法中通过pingerStrategy.pingServers(ping, allServers)获取服务可用性,检测是否与之前相同,如果相同则不拉取,如果不同则调用notifyServerStatusChangeListener(changedServers);向注册中心拉取服务列表,最终实现本地服务注册列表的更新

    class PingTask extends TimerTask {
        public void run() {
            try {
                new Pinger(pingStrategy).runPinger();
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Error pinging", name, e);
            }
        }
    }
    ----------------------------------------------------------------------------------------
     public void runPinger() throws Exception {
        	...省略
            results = pingerStrategy.pingServers(ping, allServers);
    
            final List<Server> newUpList = new ArrayList<Server>();
            final List<Server> changedServers = new ArrayList<Server>();
    
            for (int i = 0; i < numCandidates; i++) {
                boolean isAlive = results[i];
                Server svr = allServers[i];
                boolean oldIsAlive = svr.isAlive();
    
                svr.setAlive(isAlive);
    
                if (oldIsAlive != isAlive) {
                    changedServers.add(svr);
                    logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", 
                                 name, svr.getId(), (isAlive ? "ALIVE" : "DEAD"));
                }
    
                if (isAlive) {
                    newUpList.add(svr);
                }
            }
            upLock = upServerLock.writeLock();
            upLock.lock();
            upServerList = newUpList;
            upLock.unlock();
    
            notifyServerStatusChangeListener(changedServers);
        } finally {
            pingInProgress.set(false);
        }
    }
    

5.Ribbon配置

5.1 设置全局策略

1.创建配置类
2.根据需求创建所需策略类

public class RibbonConfiguration{
	@Bean
	public IRule ribbonRule(){
	 	return new RandomRule();
}
}

5.2 定制化策略

1.创建配置类
2.自定义注解
3.启动类中排除自定义配置类

  • 配置类
    @Configuration
    @AvoidScan
    public class RibbonConfiguration {
    
      @Bean
      public IRule ribbonRule(){
        return new RandomRule();
      }
    }
    
  • 启动类,排除自定义配置
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    @RibbonClient(name="provider-service",configuration = RibbonConfiguration.class)
    @ComponentScan(excludeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,value = {AvoidScan.class})})
    public class RibbonFeignApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(RibbonFeignApplication.class, args);
      }
    }
    

5.3 配置方式配置策略

下表为配置策略相关类

配置项说明
clientName.ribbon.NFLoadBalancerClassName指定ILoadBalancer的实现类
clientName.ribbon.NFLoadBalancerRuleClassName指定IRule的实现类
clientName.ribbon.NFLoadBalancerPingClassName指定IPing的实现类
clientName.ribbon.NFWSServerListClassName指定ServerList的实现类
clientName.ribbon.NIWSServerListFilterClassName指定ServerListFilter的实现类
provider-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    ConnectTimeout: 30000
    ReadTimeout: 30000
    MaxAutoRetries: 1 #对第一次请求的服务重试次数
    MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)
    OkToRetryOnAllOperations: true
ribbon:
  eager-load: # ribbon的饥饿加载,应对第一次调用加载时间长,超时而调用不成功情况
    enabled: true
    clients: provider-service

6.参考资料

  • 《重新定义Springcloud实战》
  • 《Springcloud微服务实战》
  • 《深入理解Spring Cloud与微服务构建》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值