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