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,将会导致该轮询代码的并发吞吐量明显下降。
-
加权轮询法: 通过对服务器性能的分型,给高配置,低负载的服务器分配更高的权重,均衡各个服务器的压力。
public static String testWeightRandom() {// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题Map<String, Integer> serverMap = new HashMap<String, Integer>();serverMap.putAll(serviceWeightMap);//取得IP地址listSet<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客户端,如果服务器列表不变,将映射到同一个后台服务器进行访问。
public static String testConsumerHash(String remoteIp) {// 重新创建一个map,避免出现由于服务器上线和下线导致的并发问题Map<String, Integer> serverMap = new HashMap<String, Integer>();serverMap.putAll(serviceWeightMap);//取得IP地址listSet<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。
@Configuration
public class HttpConfiguration {@Bean@LoadBalancedpublic RestTempttpTlate restTemplate() {return new RestTemplate();}
}
- RestTemplate在发送请求过程中,会构造一条具有多个拦截器的执行链,Ribbon可以借助拦截器,在RestTemplate中加入一个LoadBalancerInterceptor拦截器;
- 请求经过拦截器,Ribbon就可以根据请求的URL中的主机名(即服务名, 上面的mall-order),去注册中心拿到提供该服务的所有主机
- 根据负载均衡策略,选择其中一个,然后把服务名替换为真正的IP,接着继续执行下一个拦截器,最终发送请求
负载均衡流程源码核心类
- LoadBalanceClient:上面说到Http请求发送时,会经过Ribbon的LoadBalancerInterceptor拦截器进行负载均衡,该拦截器会把请求交给LoadBalancerClient进行负载均衡;该类是负载均衡客户端,也可以看成负载均衡的入口;采用的实现类是RibbonLoadBalancerClient。
//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);}//包装ServerRibbonServer 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) {//获取服务名对应的ApplicationContextAnnotationConfigApplicationContext context = getContext(name);if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,type).length > 0) {//从ApplicationContext中后去ILoadBalancer类型的beanreturn 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内置了多种负载均衡策略
//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方法
//RandomRule#choose
public Server choose(ILoadBalancer lb, Object key) {if (lb == null) {return null;}Server server = null;while (server == null) {//... 从ILoadBalancer中拿到该服务对应的所有ServerList<Server> upList = lb.getReachableServers();List<Server> allList = lb.getAllServers();//... 产生随机数,并拿到对应Serverint 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
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);//实例化ServerListServerList<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接口:用于表示向注册中心拉取服务信息,维护和更新本服务对应的所有主机信息。
public interface ServerList<T extends Server> {public List<T> getInitialListOfServers();public List<T> getUpdatedListOfServers();
}
Ribbon服务列表更新是通过定时任务来完成的。
4、修改Ribbon默认负载均衡策略
Ribbon默认负载均衡策略是ZoneAvoidanceRule,复合判断server所在区域的性能和server的可用性选择服务器。
第一步:新建一个不会被@ComponentScan组件扫描到的包,如:com.teh
第二步:在该包下新建自己的负载均衡算法的规则类
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@Beanpublic IRule iRule(){return new RandomRule();}
}
第三步:主启动类上添加注解:@RibbonClient
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;}
}
第四步,配置文件修改负载均衡策略
stock-service: #在服务消费者配置服务提供者的服务名ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
NFLoadBalancerRuleClassName后必须写全路径,上面代码修改策略为随机
5、@LoadBalanced注解
将该注解加在RestTemplate
的Bean上,就可以实现负载均衡。
@Configuration
public class CustomConfiguration {@Bean@LoadBalanced // 开启负载均衡能力public RestTemplate restTemplate() {return new RestTemplate();}
}
源码分析:
/*** 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是合格者的意思,表示为多个实现类选一个合格者注入。
@Autowired
@Qualifier("AUserServiceImpl")
private UserService userService;
LoadBalancer的自动配置类LoadBalancerAutoConfiguration,
@LoadBalanced
出现在了SpringCloud的底层代码中,这里会筛选出添加了@LoadBalanced
的RestTemplate,并装配到restTemplates中。