深度学习 SpringCloud - Ribbon 组件
1、关于 Ribbon
Ribbon 是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了一份服务提供者的信息列表,有了信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
当前 Ribbon 用于生产的模块有一下三个:
ribbon-loadbalancer
: 可以独立使用或与其他模块一起使用的负载均衡器API.ribbon-eureka
: Ribbon结合Eureka 客户端的API,为负载均衡器提供动态服务注册列表信息。ribbon-core
:Ribbon 的核心 API.
LoadBalanceClient:
1、负载均衡器LoadBalanceClient 是从 Eureka Client 获取服务注册列表信息的,并将服务注册列表信息缓存了一份。
2、在LoadBalancerClient 调用 choose() 方法时,根据负载均衡策略选择一个服务实例的信息,从而进行了负载均衡。
3、LoadBalancerClient 也可以不从Eureka Client 获取注册列表信息,这时需要自己维护一份服务注册列表信息。
自己维护一份服务注册列表信息,需要新建一个服务。在 application.yml 文件中加入如下代码:
stores:
ribbon:
# 将example1.com,example2.com这两个服务实例的URL配置到注册列表
listOfServers:example1.com,example2.com
ribbon:
eureka:
enable: false # 禁止调用Eureka Client 获取注册列表
2、Ribbon + RestTemplate 消费服务
这里假设已经有一个eureka服务(eureka-server),一个服务提供者(ribbion-server),现在新建一个服务(工程名为:eureka-ribbon-client)。服务提供者(ribbion-server)提供了一个叫 /icao 的API接口。
新建eureka-ribbon-client工程:
pom 文件:
<!-- 注册中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.5.RELEASE</version>
</dependency>
<!-- 负载均衡组件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<!-- 本地需要用spring-boot自带的tomcat容器,而服务器上有tomcat,所以打到服务器上要排除 -->
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
application.yml 文件:
spring:
application:
name: eureka-ribbon-client
## eureka 服务注册中心配置
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient // 注册服务 开启 EurekaClient 功能
public class EurakaRibbonClientApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(EurakaRibbonClientApplication .class, args);
}
}
RibbionConfig.java 类:
在程序的IOC容器中注入一个 restTemplate 的 Bean ,并在这个 Bean 上加上 @LoadBalance 注解,此时 RestTemplate 就结合了 Ribbon 开启了负载均衡功能。
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @ClassName: RibbionConfig
* @Description:
* @author
* @Date:2020/4/16 23:09
**/
@Configuration
public class RibbionConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
编写调用 ribbon-client 服务接口的业务层和控制层
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* @ClassName: RibbonService
* @Description:
* @author:
* @Date:2020/4/16 23:17
**/
@Service
public class RibbonService {
@Autowired
private RestTemplate restTemplate;
public String callIcao(String name) {
// 这里的 eureka-client 是服务名
return restTemplate.getForObject("http://eureka-client/icao?name="+name, String.class);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName: RibbonController
* @Description:
* @author:
* @Date:2020/4/16 23:22
**/
@RestController
public class RibbonController {
@Autowired
private RibbonService ribbonService;
@RequestMapping("/testCallIcaoApi")
public String testCallIcaoApi(@RequestParam(required = false,
defaultValue = "forezp") String name) {
return ribbonService.callIcao(name);
}
}
3、源码解析 Ribbon
3.1 分析 LoadBalancerClient
查看 LoadBalancerClient 的父类和实现类
LoadBalancerClient 是一个接口类,其继承了 ServiceInstanceChooser,它的实现类为 RibbonLoadBalancerClient
,其类图关系如下:
分析LoadBalancerClient 类源码
查看LoadBalancerClient 类源码可以发现,其包含三个方法。其中有两个 excute() 方法,均用于执行请求, reconstructURI() 用于重构 Url.
LoadBalancerClient 是一个负载均衡的客户端。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.loadbalancer;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;
public interface LoadBalancerClient extends ServiceInstanceChooser {
<T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException;
<T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException;
URI reconstructURI(ServiceInstance var1, URI var2);
}
3.2 分析 ServiceInstanceChooser
ServiceInstanceChooser 接口类有一个方法,是根据 serviceId 获取 ServiceInstance,即通过服务名来选择服务实例。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.client.loadbalancer;
import org.springframework.cloud.client.ServiceInstance;
public interface ServiceInstanceChooser {
ServiceInstance choose(String var1);
}
3.3 分析 RibbionLoadBalancerClient
LoadBalancerClient 的实现类是 RibbionLoadBalancerClient. 需要知道的是,最终负载均衡的请求处理都是由RibbionLoadBalancerClient类来执行的。分析 RibbionLoadBalancerClient 源码可以看出,choose() 方法用于选择具体服务实例。该方法通过 getServer() 方法来获取实例,追踪原代码不难发现,其最终交给了 ILoadBalancer 类去选择服务实例。
以下是 RibbionLoadBalancerClient 部分源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.cloud.netflix.ribbon;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerRequest;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
public class RibbonLoadBalancerClient implements LoadBalancerClient {
…… // 省略代码
public ServiceInstance choose(String serviceId) {
Server server = this.getServer(serviceId);
return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
}
protected Server getServer(String serviceId) {
return this.getServer(this.getLoadBalancer(serviceId));
}
protected Server getServer(ILoadBalancer loadBalancer) {
return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
}
3.4 分析 ILoadBalancer
分析 ILoadBalancer 类源码,ILoadBalancer 接口方法中的中文注释,由本博主后期加上的。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.netflix.loadbalancer;
import java.util.List;
public interface ILoadBalancer {
// 该方法用于添加一个 Server 集合
void addServers(List<Server> var1);
// 该方法用于根据 Key 去获取 Server
Server chooseServer(Object var1);
// 该方法用于标记某个服务下线
void markServerDown(Server var1);
/** @deprecated 表示该方法已过时 */
@Deprecated
List<Server> getServerList(boolean var1);
// 该方法用于获取可用的 Server 集合
List<Server> getReachableServers();
// 该方法用于获取所有的 Server 集合
List<Server> getAllServers();
}
ILoadBalancer 类相关类图:
由类图可以看出 ILoadBalancer 类的子类为 BaseLoadBalancer 类,BaseLoadBalancer 的实现类为 DynamicServerListLoadBalancer 类。
3.5 分析 DynamicServerListLoadBalancer
分析 DynamicServerListLoadBalancer 源码中的构造方法
public DynamicServerListLoadBalancer(IClientConfig clientConfig,
IRule rule, IPing ping,
ServerList<T> serverList,
ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
// 省略代码 ……
}
可以看出,DynamicServerListLoadBalancer 需要配置:
IClientConfig
:用于配置负载均衡的客户端,默认实现类为 DefaultClientConfgImpl.IRule
:用于配置负载均衡的策略,IRule有三个方法,其中 choose() 是根据 key 来获取 server 实例的,setLoadBalancer() 和 getLoadBalancer() 是用来设置和获取 ILoadBalancer 的。IPing
:用来向 server 发送 “ping”,来判断该 server 是否有响应,从而判断该 server 是否可用。ServerList
:是定义获取所有 server 的注册列表信息的接口。ServerListFilter
:该接口定义了可根据配置,去过滤或者特性动态地获取符号条件的 server 列表的方法。ServerListUpdater
:这个类用于 server 列表的更新。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.netflix.loadbalancer;
import com.google.common.annotations.VisibleForTesting;
import com.netflix.client.ClientFactory;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ServerListUpdater.UpdateAction;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.annotations.Monitor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicServerListLoadBalancer.class);
boolean isSecure;
boolean useTunnel;
protected AtomicBoolean serverListUpdateInProgress;
volatile ServerList<T> serverListImpl;
volatile ServerListFilter<T> filter;
protected final UpdateAction updateAction;
protected volatile ServerListUpdater serverListUpdater;
public DynamicServerListLoadBalancer() {
this.isSecure = false;
this.useTunnel = false;
this.serverListUpdateInProgress = new AtomicBoolean(false);
this.updateAction = new UpdateAction() {
public void doUpdate() {
DynamicServerListLoadBalancer.this.updateListOfServers();
}
};
}
/** @deprecated */
@Deprecated
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter) {
this(clientConfig, rule, ping, serverList, filter, new PollingServerListUpdater());
}
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter, ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.isSecure = false;
this.useTunnel = false;
this.serverListUpdateInProgress = new AtomicBoolean(false);
this.updateAction = new UpdateAction() {
public void doUpdate() {
DynamicServerListLoadBalancer.this.updateListOfServers();
}
};
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter)filter).setLoadBalancerStats(this.getLoadBalancerStats());
}
this.restOfInit(clientConfig);
}
public DynamicServerListLoadBalancer(IClientConfig clientConfig) {
this.isSecure = false;
this.useTunnel = false;
this.serverListUpdateInProgress = new AtomicBoolean(false);
this.updateAction = new UpdateAction() {
public void doUpdate() {
DynamicServerListLoadBalancer.this.updateListOfServers();
}
};
this.initWithNiwsConfig(clientConfig);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
try {
super.initWithNiwsConfig(clientConfig);
String niwsServerListClassName = clientConfig.getPropertyAsString(CommonClientConfigKey.NIWSServerListClassName, "com.netflix.loadbalancer.ConfigurationBasedServerList");
ServerList<T> niwsServerListImpl = (ServerList)ClientFactory.instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig);
this.serverListImpl = niwsServerListImpl;
if (niwsServerListImpl instanceof AbstractServerList) {
AbstractServerListFilter<T> niwsFilter = ((AbstractServerList)niwsServerListImpl).getFilterImpl(clientConfig);
niwsFilter.setLoadBalancerStats(this.getLoadBalancerStats());
this.filter = niwsFilter;
}
String serverListUpdaterClassName = clientConfig.getPropertyAsString(CommonClientConfigKey.ServerListUpdaterClassName, "com.netflix.loadbalancer.PollingServerListUpdater");
this.serverListUpdater = (ServerListUpdater)ClientFactory.instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig);
this.restOfInit(clientConfig);
} catch (Exception var5) {
throw new RuntimeException("Exception while initializing NIWSDiscoveryLoadBalancer:" + clientConfig.getClientName() + ", niwsClientConfig:" + clientConfig, var5);
}
}
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
this.setEnablePrimingConnections(false);
this.enableAndInitLearnNewServersFeature();
this.updateListOfServers();
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections().primeConnections(this.getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
public void setServersList(List lsrv) {
super.setServersList(lsrv);
Map<String, List<Server>> serversInZones = new HashMap();
Iterator var4 = lsrv.iterator();
while(var4.hasNext()) {
Server server = (Server)var4.next();
this.getLoadBalancerStats().getSingleServerStat(server);
String zone = server.getZone();
if (zone != null) {
zone = zone.toLowerCase();
List<Server> servers = (List)serversInZones.get(zone);
if (servers == null) {
servers = new ArrayList();
serversInZones.put(zone, servers);
}
((List)servers).add(server);
}
}
this.setServerListForZones(serversInZones);
}
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
LOGGER.debug("Setting server list for zones: {}", zoneServersMap);
this.getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
}
public ServerList<T> getServerListImpl() {
return this.serverListImpl;
}
public void setServerListImpl(ServerList<T> niwsServerList) {
this.serverListImpl = niwsServerList;
}
public ServerListFilter<T> getFilter() {
return this.filter;
}
public void setFilter(ServerListFilter<T> filter) {
this.filter = filter;
}
public ServerListUpdater getServerListUpdater() {
return this.serverListUpdater;
}
public void setServerListUpdater(ServerListUpdater serverListUpdater) {
this.serverListUpdater = serverListUpdater;
}
public void forceQuickPing() {
}
public void enableAndInitLearnNewServersFeature() {
LOGGER.info("Using serverListUpdater {}", this.serverListUpdater.getClass().getSimpleName());
this.serverListUpdater.start(this.updateAction);
}
private String getIdentifier() {
return this.getClientConfig().getClientName();
}
public void stopServerListRefreshing() {
if (this.serverListUpdater != null) {
this.serverListUpdater.stop();
}
}
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList();
if (this.serverListImpl != null) {
servers = this.serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
if (this.filter != null) {
servers = this.filter.getFilteredListOfServers((List)servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
}
}
this.updateAllServerList((List)servers);
}
protected void updateAllServerList(List<T> ls) {
if (this.serverListUpdateInProgress.compareAndSet(false, true)) {
try {
Iterator var2 = ls.iterator();
while(var2.hasNext()) {
T s = (Server)var2.next();
s.setAlive(true);
}
this.setServersList(ls);
super.forceQuickPing();
} finally {
this.serverListUpdateInProgress.set(false);
}
}
}
public String toString() {
StringBuilder sb = new StringBuilder("DynamicServerListLoadBalancer:");
sb.append(super.toString());
sb.append("ServerList:" + String.valueOf(this.serverListImpl));
return sb.toString();
}
public void shutdown() {
super.shutdown();
this.stopServerListRefreshing();
}
@Monitor(
name = "LastUpdated",
type = DataSourceType.INFORMATIONAL
)
public String getLastUpdate() {
return this.serverListUpdater.getLastUpdate();
}
@Monitor(
name = "DurationSinceLastUpdateMs",
type = DataSourceType.GAUGE
)
public long getDurationSinceLastUpdateMs() {
return this.serverListUpdater.getDurationSinceLastUpdateMs();
}
@Monitor(
name = "NumUpdateCyclesMissed",
type = DataSourceType.GAUGE
)
public int getNumberMissedCycles() {
return this.serverListUpdater.getNumberMissedCycles();
}
@Monitor(
name = "NumThreads",
type = DataSourceType.GAUGE
)
public int getCoreThreads() {
return this.serverListUpdater.getCoreThreads();
}
}
3.5.1 IRule
IRule 源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.netflix.loadbalancer;
public interface IRule {
// 该方法是根据 key 来获取 server 实例的
Server choose(Object var1);
// 该方法是用来设置 ILoadBalancer 的。
void setLoadBalancer(ILoadBalancer var1);
// 该方法是用来获取 ILoadBalancer 的。
ILoadBalancer getLoadBalancer();
}
IRule 及其实现类:
IRule有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡的策略。
IRule的默认实现类有以下7种。在大多数情况下,这些默认的实现类是可以满足需求的,如
果有特殊的需求,可以自己实现。IRule和其实现类之间的关系如图所示。
BestAvailableRule
: 选择最小请求数。ClientConfigEnabledRoundRobinRule
: 轮 询 。RandomRule
: 随机选择一个 server。RoundRobinRule
: 轮询选择 server。RetryRule
: 根据轮询的方式重试。WeightedResponseTimeRule
: 根据响应时间去分配一个weight,weight越低,被选
择的可能性就越低。ZoneAvoidanceRule
: 根据 server 的 zone 区域和可用性来轮询选择。
3.5.2 定义负载均衡策略
1、全局策略设置
@Configuration
public class RibbonRuleConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}
2、基于注解的策略设置
如果我们想要针对某一个源服务设置其特有的策略,可以通过使用@RibbonClient 注解。
@Configuration
@AvoidScan
public class RibbonRuleConfiguration {
@Autowired
IClientConfig config
@Bean
public IRule ribbonRule(IClientConfig config) {
return new RandomRule();
}
}
上述代码中的@AviodScan 注解是一个空的声明。注入的IClientConfig 是针对客户端的配置管理器,使用 @RibbonClient 注解时尤其需要注意它的作用。
然后,我们需要在启动类上加@RibbonClient 注解,来对源服务进行负载约束。启动类上注解如下:
@RibbonClient(name = "client-a", configuration = RibbonRuleConfiguration.calss)
@ComponentScan(excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,value = {
AvoidScan.class})})
上面注解中 client-a 是服务名,以上注解的意思是对 client-a 服务使用的策略是经过 RibbonRuleConfiguration 所配置的。这里使用@ComponentScan 注解的意思是让 Spring 不去扫描被 @AvoidScan 注解标记的配置类,因为我们的配置是对单个源服务生效的,所以不能应用于全局,如果不排除,启动就会报错。
需要特别提醒的是:也可以使用@RibbonClients 注解来对多个源服务进行策略制定。
见如下代码:
@RibbonClients(value = {
@RibbonClient(name = "client-a", configuration = RibbonRuleConfiguration.calss),
@RibbonClient(name = "client-b", configuration = RibbonRuleConfiguration.calss),
})
3、基于配置文件的策略设置
application.yml 文件:
client-a: # 这里是服务名
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
4、Ribbon 的饥饿加载
Ribbon 在进行客户端负载均衡的时候,并不是在启动时就加载上下文,而在实际请求的时候才去创建,这个特性往往会让用户的第一次调用显得极度不友好,严重的时候还会引起调用超时。
为了解决这个问题,我们需要通过指定 Ribbon 具体的客户端名称来开启饥饿加载,即在启动的时候,就加载所有配置项的应用程序上下文。具体配置见以下代码:
ribbon:
eager-load:
enabled: true
clients: client-a,client-b,client-c ## 服务名
5、Ribbon 超时与重试
使用 HTTP请求时,或多或少会经历网络等极端环境。此时对调用进行时限控制,以及时限之后的重试尤为重要。当前 F版本中的 Ribbon的重试机制是默认开启的,需要添加对于超时时间与重试策略的配置。
具体见以下代码:
client-a: # 服务名
ribbon:
ConnectTimeout: 3000
ReadTimeout: 3000
MaxAutoRetries: 1 #对第一次请求的服务重试次数
MaxAutoRetriesNextServer: 1 # 要重试的下一次服务的最大数量(不包括第一个服务)
OkToRetryOnAllOperations: true
6、总结
Ribbon的负载均衡主要是通过 LoadBalancerClient 来实现的,而 LoadBalancerClient 具体交给了 ILoadBalancer 来处理, ILoadBalan 通过配置 IRule、IPing等,向EurekaClient 获取注册列表的信息, 默认每10秒向 EurekaClien 发送一次 "ping" ,进而检查是否需要更新服务的注册列表信息。最后, 在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡。
而 RestTemplate 加上@LoadBalance注解后,在远程调度时能够负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给该列表中的 RestTemplate 对象添加了拦截器。在拦截器的方法中, 将远程调度方法交给了Ribbon 的负载均衡器 LoadBalancerClient 去处理,从而达到了负载均衡的目的。
.