Java面试题019:一文深入了解微服务之负载均衡Ribbon

欢迎大家关注我的JAVA面试题专栏,该专栏会持续更新(第一目标100节),从原理角度覆盖Java知识体系的方方面面。

一文吃透JAVA知识体系(面试题)https://blog.csdn.net/wuxinyan123/category_7521898.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=7521898&sharerefer=PC&sharesource=wuxinyan123&sharefrom=from_link

1、Ribbon简介

Ribbon是Netflix下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,主要实现中间层应用层析的负载均衡。Ribbon提供以下特性:

  • 负载均衡器,可支持插拔式的负载均衡规则。
  • 对多种协议提供支持,例如HTTP、TCP、UDP。
  • 集成了负载均衡功能的客户端。

Spring Cloud将Ribbon的API进行了封装,使用者可以使用封装后的API来实现负载均衡,也可以直接使用Ribbon的原生API。

Ribbon主要有以下三大子模块:

  • ribbon-core:项目核心,包括负载均衡器接口定义、客户端接口定义、内置的负载均衡实现等API。
  • ribbon-eureka:为Eureka客户端提供的负载均衡实现类。
  • ribbon-httpclient:对Apache的HttpClient进行封装,还提供负载均衡功能的REST客户端。

2、常见负载均衡算法

  • **随机法:**通过随机选择服务进行执行,一般这种方式使用较少。

  • **轮询法:**负载均衡默认实现方式,请求来之后排队处理。

将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

对于当前轮询的位置变量pos,为了保证服务器选择的顺序性,需要对其在操作时加上synchronized锁,使得同一时刻只有一个线程能够修改pos的值,否则当pos变量被并发修改,将无法保证服务器选择的顺序性,甚至有可能导致keyList数组越界。

使用轮询策略的目的是,希望做到请求转移的绝对均衡,但付出的代价性能也是相当大的。为了保证pos变量的并发互斥,引入了重量级悲观锁synchronized,将会导致该轮询代码的并发吞吐量明显下降。

  • **加权轮询法:**通过对服务器性能的分型,给高配置,低负载的服务器分配更高的权重,均衡各个服务器的压力。
java 复制代码
public static String testWeightRandom() {
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    List<String> serverList = new ArrayList<String>();
    Iterator<String> it = keySet.iterator();
     
    while (it.hasNext()) {
        String server = it.next();
        Integer weight = serverMap.get(server);
        for (int i=0; i<weight; i++) {
            serverList.add(server);
        }
    }
 
    Random random = new Random();
    int randomPos = random.nextInt(serverList.size());
 
    String server = serverList.get(randomPos);
 
    return server;
}
  • **源地址哈希法:**通过客户端请求的地址的HASH值取模映射进行服务器调度。

源地址哈希法的思想是根据服务消费者请求客户端的IP地址,通过哈希函数计算得到一个哈希值,将此哈希值和服务器列表的大小进行取模运算,得到的结果便是要访问的服务器地址的序号。采用源地址哈希法进行负载均衡,相同的IP客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。

java 复制代码
public static String testConsumerHash(String remoteIp) {
 
    // 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题
    Map<String, Integer> serverMap = new HashMap<String, Integer>();
    serverMap.putAll(serviceWeightMap);
 
    //取得IP地址list
    Set<String> keySet = serverMap.keySet();
    ArrayList<String> keyList = new ArrayList<String>();
    keyList.addAll(keySet);
     
    int hashCode = remoteIp.hashCode();
    int pos = hashCode % keyList.size();
     
    return keyList.get(pos);
}
  • 最小连接数: 即使请求均衡了,压力不一定会均衡,最小连接数法就是根据服务器的情况,比如请求积压数等参数,将请求分配到当前压力最小的服务器上。

最小连接数法比较灵活和智能,由于后台服务器的配置不尽相同,对请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。

  • **加权随机法:**加权随机法跟加权轮询法类似,根据后台服务器不同的配置和负载情况,配置不同的权重。不同的是,它是按照权重来随机选取服务器的,而非顺序。

  • **可用性过滤器:**先过滤掉不可用的服务器,然后在剩下的服务器中选择一个。

  • **区域感知:**结合了可用性过滤和区域感知,优先选择同一区域内的服务器。

3、Ribbon负载均衡原理

Ribbon通常和Http请求结合,对Http请求进行负载均衡;通过注入RestTemplate,并且打上@LoadBlanced注解,即可得到一个带有负载均衡效果的RestTemplate。

java 复制代码
@Configuration
public class HttpConfiguration {
    @Bean
    @LoadBalanced
    public RestTempttpTlate restTemplate() {
        return new RestTemplate();
    }
}
  • RestTemplate在发送请求过程中,会构造一条具有多个拦截器的执行链,Ribbon可以借助拦截器,在RestTemplate中加入一个LoadBalancerInterceptor拦截器;
  • 请求经过拦截器,Ribbon就可以根据请求的URL中的主机名(即服务名, 上面的mall-order),去注册中心拿到提供该服务的所有主机
  • 根据负载均衡策略,选择其中一个,然后把服务名替换为真正的IP,接着继续执行下一个拦截器,最终发送请求

负载均衡流程源码核心类

  • LoadBalanceClient:上面说到Http请求发送时,会经过Ribbon的LoadBalancerInterceptor拦截器进行负载均衡,该拦截器会把请求交给LoadBalancerClient进行负载均衡;该类是负载均衡客户端,也可以看成负载均衡的入口;采用的实现类是RibbonLoadBalancerClient。
java 复制代码
//RibbonLoadBalancerClient#execute
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
    throws IOException {
    //根据serviceId拿到ILoadBalancer,交由它进行负载均衡
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    //根据loadBalancer拿到真正的服务提供者
    Server server = getServer(loadBalancer, hint);
    if (server == null) {
        throw new IllegalStateException("No instances available for " + serviceId);
    }
    //包装Server
    RibbonServer ribbonServer = new RibbonServer(serviceId, server,
                                                 isSecure(server, serviceId),
                                                 serverIntrospector(serviceId).getMetadata(server));

    //执行请求
    return execute(serviceId, ribbonServer, request);
}

//RibbonLoadBalancerClient#getLoadBalancer
protected ILoadBalancer getLoadBalancer(String serviceId) {
	//从clientFactory中获得该服务对应的ILoadBalancer	
    return this.clientFactory.getLoadBalancer(serviceId);
}
public ILoadBalancer getLoadBalancer(String name) {
    return getInstance(name, ILoadBalancer.class);
}

public <T> T getInstance(String name, Class<T> type) {
    //获取服务名对应的ApplicationContext
    AnnotationConfigApplicationContext context = getContext(name);
    if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                                                            type).length > 0) {
        //从ApplicationContext中后去ILoadBalancer类型的bean
        return context.getBean(type);
    }
    return null;
}

protected AnnotationConfigApplicationContext getContext(String name) {
    //大致逻辑就是直接容map中拿,没有则创建一个并缓存
    if (!this.contexts.containsKey(name)) {
        synchronized (this.contexts) {
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, createContext(name));
            }
        }
    }
    return this.contexts.get(name);
}
  • ILoadBalancer:RibbonLoadBalancerClient内部的execute方法会以Http请求的服务名为key,找到ILoadBalancer对象,这个ILoadBalancer就是专门负责服务的负载均衡。
  • IRule:代表负载均衡策略,ILoadBalancer的负载均衡由它进行处理,Ribbon内置了多种负载均衡策略
java 复制代码
//BaseLoadBalancer#chooseServer
public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    //如果没有配置rule则返回
    if (rule == null) {
        return null;
    } else {
        try {
            //由rule执行真正的负载均衡
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}

以RandomRule为例看一下choose方法

java 复制代码
//RandomRule#choose
public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        return null;
    }
    Server server = null;

    while (server == null) {
        //... 从ILoadBalancer中拿到该服务对应的所有Server
        List<Server> upList = lb.getReachableServers();
        List<Server> allList = lb.getAllServers();
		//... 产生随机数,并拿到对应Server
        int index = chooseRandomInt(serverCount);
        server = upList.get(index);
		//...

        if (server.isAlive()) {
            return (server);
        }
		//...
    }
    return server;
}

负载均衡流程

(1)Http请求经过LoadBalancerInterceptor拦截器,它将调用LoadBalancerClient进行处理;

(2)LoadBalancerClient(实现类Ribbon``LoadBalancerClient)根据服务名拿到对应的ApplicationContext,并从容器中拿到ILoadBalancer(实际类ZoneAwareLoadBalancer)

(3)从ILoadBalancer中获取服务提供者Server,具体是调用chooseServer方法,内部会使用IRule进行负载均衡,并返回合适的Server

(4)IRule才是真正的负载均衡实现接口,Ribbon内置多种默认负载均衡策略

主机列表信息的更新维护

  • DynamicServerListLoadBalance: 负责服务对应的主机列表信息。具有ServerList功能的ILoadBalance
java 复制代码
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                     ServerList<T> serverList, ServerListFilter<T> filter,
                                     ServerListUpdater serverListUpdater) {
    super(clientConfig, rule, ping);
    this.serverListImpl = serverList;
    this.filter = filter;
    this.serverListUpdater = serverListUpdater;
    if (filter instanceof AbstractServerListFilter) {
        ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
    }
    //调用该方法进行主机列表初始化
    restOfInit(clientConfig);
}

void restOfInit(IClientConfig clientConfig) {
    //...
    updateListOfServers();
    //...
}

//这是另一个构造方法,可能是使用自定义配置时
public DynamicServerListLoadBalancer(IClientConfig clientConfig) {
    initWithNiwsConfig(clientConfig);
}
    
//DynamicServerListLoadBalancer#initWithNiwsConfig
public void initWithNiwsConfig(IClientConfig clientConfig) {
    try {
        super.initWithNiwsConfig(clientConfig);
        //从配置文件中拿到NIWSServerListClassName对应的值,应该是拿到配置的ServerList实现类,负责维护服务信息
        //当注册中心不同时,应该可以动态替换
        String niwsServerListClassName = clientConfig.getPropertyAsString(
            CommonClientConfigKey.NIWSServerListClassName,
            DefaultClientConfigImpl.DEFAULT_SEVER_LIST_CLASS);
		//实例化ServerList
        ServerList<T> niwsServerListImpl = (ServerList<T>) ClientFactory
            .instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig);
        //保存
        this.serverListImpl = niwsServerListImpl;
		//...
		//拿到ServerListUpdaterClassName配置的类
        String serverListUpdaterClassName = clientConfig.getPropertyAsString(
            CommonClientConfigKey.ServerListUpdaterClassName,
            DefaultClientConfigImpl.DEFAULT_SERVER_LIST_UPDATER_CLASS
        );
		//实例化
        this.serverListUpdater = (ServerListUpdater) ClientFactory
            .instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig);
		//在该函数中,会进行服务初始化
        restOfInit(clientConfig);
    } catch (Exception e) {
        throw new RuntimeException(
            "Exception while initializing NIWSDiscoveryLoadBalancer:"
            + clientConfig.getClientName()
            + ", niwsClientConfig:" + clientConfig, e);
    }
}
  • ServerList接口:用于表示向注册中心拉取服务信息,维护和更新本服务对应的所有主机信息。
java 复制代码
public interface ServerList<T extends Server> {
    public List<T> getInitialListOfServers();
    
    public List<T> getUpdatedListOfServers();   
}

Ribbon服务列表更新是通过定时任务来完成的。

4、修改Ribbon默认负载均衡策略

Ribbon默认负载均衡策略是**ZoneAvoidanceRule,**复合判断server所在区域的性能和server的可用性选择服务器。

第一步:新建一个不会被@ComponentScan组件扫描到的包,如:com.teh

第二步:在该包下新建自己的负载均衡算法的规则类

java 复制代码
package teh;
 
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 RibbonRuleConfig {
    //方法名一定要为iRule
    @Bean
    public IRule iRule(){
        return new RandomRule();
    }
}

第三步:主启动类上添加注解:@RibbonClient

java 复制代码
package teh;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import ribbon.RandonRuleConfig;
 
@SpringBootApplication
@RibbonClients(value = {@RibbonClient(name = "stock-service",configuration = RibbonRuleConfig.class)}) //配置负载均衡策略
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
 
    @Bean
    @LoadBalanced // 负载均衡器注解,nacos的服务调用依赖于负载均衡(nacos无法将服务名称转化为服务地址,需要使用负载均衡器,默认使用轮询的方式)
    public RestTemplate restTemplate(RestTemplateBuilder builder){
        RestTemplate RestTemplate = builder.build();
        return RestTemplate;
    }
}

第四步,配置文件修改负载均衡策略

java 复制代码
stock-service: #在服务消费者配置服务提供者的服务名
  ribbon:
   NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

NFLoadBalancerRuleClassName后必须写全路径,上面代码修改策略为随机

5、@LoadBalanced注解

将该注解加在RestTemplate的Bean上,就可以实现负载均衡。

java 复制代码
@Configuration
public class CustomConfiguration {
    @Bean
    @LoadBalanced // 开启负载均衡能力
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

源码分析:

java 复制代码
/**
 * Annotation to mark a RestTemplate or WebClient bean to be configured to use a
 * LoadBalancerClient.
 * 
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {

}

接口LoadBalanced的定义上,添加了@Qualifier注解。

当SpringIOC容器中有多个同类型的bean时,在使用@Autowired进行装配时,就无法完成自动装配,原因是@Autowired是按bean的类型来装配的,Spring也不知道我们到底要装配哪个bean,@Qualifier的出现就是为了解决这个问题。@Qualifier是根据bean的名称来进行装配。

Qualifier是合格者的意思,表示为多个实现类选一个合格者注入。

java 复制代码
@Autowired
@Qualifier("AUserServiceImpl")
private  UserService userService;

LoadBalancer的自动配置类LoadBalancerAutoConfiguration,

@LoadBalanced出现在了SpringCloud的底层代码中,这里会筛选出添加了@LoadBalanced的RestTemplate,并装配到restTemplates中。

相关推荐
张哈大2 分钟前
【 java 虚拟机知识 第二篇 】
java·开发语言·jvm·笔记
知其然亦知其所以然4 分钟前
只会写 Mapper 就想进大厂?MyBatis 原理你真懂了吗?
java·后端·面试
前端小巷子8 分钟前
WebSQL:浏览器端的 SQL 数据库
前端·javascript·面试
九月十九12 分钟前
java操作word里的表格
java·word
北京_宏哥12 分钟前
🔥《刚刚问世》系列初窥篇-Java+Playwright自动化测试-20- 操作鼠标拖拽 - 上篇(详细教程) 草稿
java·前端·前端框架
%d%d222 分钟前
Unable to make field long java.nio.Buffer.address accessible:
java·开发语言·nio
学无止境的子戌25 分钟前
RAG、FunctionCall和MCP的简单介绍
java·人工智能·后端
Lanqing_076026 分钟前
京东开放平台获取京东商品详情API接口操作解答
java·前端·python·api·电商·电商数据
Lenyiin27 分钟前
第 87 场周赛:比较含退格的字符串、数组中的最长山脉、一手顺子、访问所有节点的最短路径
java·c++·python·leetcode·周赛·lenyiin
forestsea29 分钟前
Spring 路由匹配机制详解:时间复杂度从 O(n) 降至 O(log n)
java·后端·spring·路由匹配