负载均衡 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备和服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。
分类 | 实现方式 | 优点 | 缺点 |
---|---|---|---|
软件 | 在一台或多台服务器相应的操作系统上安装一个或多个附加软件 | 基于特定环境,配置简单,使用灵活,成本低廉 | 软件本身耗费资源、可扩展性不好、受操作系统限制、安全问题 |
硬件 | 直接在服务器和外部网络间安装负载均衡设备 | 独立于操作系统、多样化的负载均衡策略、智能化的流量管理、性能高 | 成本昂贵 |
本地 | 对本地的服务器群做负载均衡 | 能有效地解决数据流量过大、网络负荷过重的问题、不需要购置昂贵的服务器 | |
全局 | 对分别放置在不同的地理位置、有不同网络结构的服务器群间作负载均衡 | 使全球用户只以一个IP地址或域名就能访问到离自己最近的服务器 |
Netflix Ribbon 以及 被SpringCloud 封装过的Ribbon,本质上都是客户端负载均衡的方式。
但凡客户端负载均衡,大抵可以分为两个过程:
在SpringCloud中,服务注册列表的信息来自 EurekaClient。
在Ribbon的整个调度过程中,LoadBalancerClient,是负责整个过程的执行者。
抛开SpringCloud中一大堆的框架代码,直接找到 DynamicServerListLoadBalancer 类中的 updateListOfServers() 方法:
@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);
}
继续根据 serverListImpl.getUpdatedListOfServers() 顺藤摸瓜,找到类 DiscoveryEnabledNIWSServerList ,其中有 getInitialListOfServers() 和 getUpdatedListOfServers() 方法:
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
return obtainServersViaDiscovery();
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
这里定义了两个方法,一个是获取初始服务注册列表的方法,一个是获取更新后的服务注册列表的方法,其实是同一个方法……具体实现:
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList();
if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
if (this.vipAddresses != null) {
String[] var3 = this.vipAddresses.split(",");
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String vipAddress = var3[var5];
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
//省略部分代码
}
}
}
}
上述方法分两个部分:
其中,eurekaClientProvider 的实现类是 LegacyEurekaClientProvider,它是一个获取 eurekaClient 类,通过静态的方法去获取 eurekaClient ,其代码如下:
class LegacyEurekaClientProvider implements Provider<EurekaClient> {
private volatile EurekaClient eurekaClient;
@Override
public synchronized EurekaClient get() {
if (eurekaClient == null) {
eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
}
return eurekaClient;
}
}
在BaseLoadBalancer类下,BaseLoadBalancer的构造函数,该构造函数开启了一个PingTask任务:
public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
IPing ping, IPingStrategy pingStrategy) {
//省略部分代码
setupPingTask();
//省略部分代码
}
void setupPingTask() {
//省略部分代码
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
//省略部分代码
}
在BaseLoadBalancer的构造函数中有一个参数IPing ping,它定义了服务的可用性:
public interface IPing {
boolean isAlive(Server var1);
}
这里仅仅设置定义了一个布尔值,真正使用的地方,在Pinger的runPinger()方法中:
public void runPinger() throws Exception {
//省略部分代码
boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
//省略部分代码
}
简单来说,pingerStrategy.pingServers()方法会返回一个布尔值,代表服务注册列表是否有变化
public interface IPingStrategy {
boolean[] pingServers(IPing var1, Server[] var2);
}
实现其实很简单,获取新的服务注册列表的 Ping 列表,与已经获取的进行比较:
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
//省略部分代码
for(int i = 0; i < numCandidates; ++i) {
results[i] = false;
try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} //省略部分代码
return results;
}
由此可见,LoadBalancerClient 会向Eureka 获取服务注册列表,并且通过10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以进行负载均衡。
简而言之,真正的负载均衡是委托给 IRule 实现的:
public Server chooseServer(Object key) {
//省略部分代码
this.counter.increment();
//省略部分代码
return this.rule.choose(key);
//省略部分代码
}
IRule 通过 AbstractLoadBalancerRule 加载过滤规则:
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
private ILoadBalancer lb;
public AbstractLoadBalancerRule() {
}
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
public ILoadBalancer getLoadBalancer() {
return this.lb;
}
}
本质上通过随机数随机获取一个服务实例,在index上随机,选择index对应位置的server,但如果根据随机数获取不到实例,则会出现BUG
按照线性轮询的方式依次选择每个服务的实例,如果超过10次获取不到服务,则尝试结束,并抛出警告。
在 RoundRobinRule 基础上增加了重试机制,在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
该方式是 RoundRobinRule 的扩展,增加了根据实例的运行情况来计算权值,并根据权值来选择实例,其实现主要分3个步骤:
定时任务
权值计算
权值的值是总响应时间与实例自身平均响应时间的差所累加而得
举例:假设有四个实例A、B、C、D,他们的平均响应时间为10、40、80、100,所以总响应时间是230,每个实例的权值如下:
这些权值代表的一个区间,总区间是 [0 ,690)
实例选择
逐个考察实例,如果实例被过滤掉了,则忽略,再选择其中并发请求数最小的实例。
该策略有两个过程,先以线性方式选择一个实例,再判断是否满足,如果满足就选择,不满足就继续下一个
..ActiveConnectionsLimit
来修改和 AvailabilityFilteringRule 不一样的是,ZoneAvoidanceRule是先进行过滤,再轮询选择,过滤的条件和 AvailabilityFilteringRule 一样,不过是先通过过滤把所有的服务找出来,然后再去轮询选择。
使用,增加一个注解 @LoadBalance
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
源码分析,SpringBoot的一个核心功能就是自动配置,其中以 XXXAutoConfiguration 类为最关键:
找到 LoadBalanceAutoConfiguration 类定义如下:
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
}
在该类中,首先维护了一个被@LoadBalanced修饰的RestTemplate对象的List,在初始化的过程中,通过调用customizer.customize(restTemplate)方法来给RestTemplate增加拦截器LoadBalancerInterceptor。
而LoadBalancerInterceptor,用于实时拦截,在LoadBalancerInterceptor实现来负载均衡。LoadBalancerInterceptor的拦截方法如下:
@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, requestFactory.createRequest(request, body, execution));
}
综上所述,Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。
而RestTemplate 被@LoadBalance注解后,能过用负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。