探究Spring Cloud Ribbon默認負載均衡策略實現的過程

1: 簡介

Spring Cloud Ribbon是一個基於HTTP和TCP的客戶端負載均衡工具,它基於Netflix Ribbon實現。那麼哪些路由配置會走負載均衡器呢?答案是配置具有lb://服務名,這樣的配置會走,因爲LoadBalancerClientFilter會進行判斷url的前綴是否含有lb

if (url == null||
 (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
	return chain.filter(exchange);
}

 

2: 查看自動配置文件

spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration

3: 分析RibbonAutoConfiguration

 第一步:分析SpringClientFactory

配置類實例化了工廠配置SpringClientFactory,而SpringClientFacory實現了spring cloud context的NamedContextFactory,NamedContextFactory是創建客戶端、負載平衡器和客戶端配置實例的工廠,爲每個實現者createContext創建一個SpringApplicationContext(AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();),並提取所需要的信息。也就是說可以在SpringClientFactory裏獲取配置 lb 的具體實例信息,在這個過程中,會調用該容器的refresh()刷新當前的容器實例信息,這個時候會觸發RibbonClientConfiguration配置,實例化負載均衡器ZoneAwareLoadBalancer、負載均衡規則 ZoneAvoidanceRule,並將ZoneAvoidanceRule注入到ZoneAwareLoadBalancer裏,默認通過ZoneAwareLoadBalancer的choose找到server信息

@Bean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}

 通過SpringClientFactory獲取負載均衡器

public ILoadBalancer getLoadBalancer(String name) {
		return getInstance(name, ILoadBalancer.class);
	}

getLoadBalancer裏的name指的是負載均衡lb的名字,如下面中的 opencloud-ram,負載均衡器是IloadBalancer,通過SpringClientFactory中的容器AnnotationConfigApplicationContext,調用getBean獲取具體的bean實例,即ZoneAwareLoadBalancer。

- id: ram_route
    uri: lb://opencloud-ram
    predicates:
      - Path=/v1/ram/**

第二步:分析RibbonLoadBalancerClient

RibbonAutoConfiguration也實例化了RibbonLoadBalancerClient客戶端,使其根據lb獲取負載均衡實例,
public ServiceInstance choose(String serviceId, Object hint) {
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
        // 在這裏我們看到,hint作爲負載均衡策略,如果不設置,則爲null,會使用默認的default側露
        // 也就是輪詢策略
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

此時程序執行到關鍵的步驟,使用ZoneAwareLoadBalancer負載均衡策略獲取server

 
public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        // 這的服務信息,將有配置中心獲取,所用的配置中心是Nacos
        List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
        return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
    }

其中算法如下,一個輪詢的算法

private final AtomicInteger nextIndex = new AtomicInteger();

private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextIndex.get();
            int next = (current + 1) % modulo;
            if (nextIndex.compareAndSet(current, next) && current < modulo)
                return current;
        }
    }

4: 配置分析完了之後,要分析下如何應用

LoadBalancerClientFilter作爲負載均衡過濾器,所有有效的請求會經過這裏的filter,然後根據負載策略選擇服務
@Override
	@SuppressWarnings("Duplicates")
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		addOriginalRequestUrl(exchange, url);

		log.trace("LoadBalancerClientFilter url before: " + url);

		final ServiceInstance instance = choose(exchange);

		if (instance == null) {
			throw NotFoundException.create(properties.isUse404(),
					"Unable to find instance for " + url.getHost());
		}

		URI uri = exchange.getRequest().getURI();

		// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
		// if the loadbalancer doesn't provide one.
		String overrideScheme = instance.isSecure() ? "https" : "http";
		if (schemePrefix != null) {
			overrideScheme = url.getScheme();
		}

		URI requestUrl = loadBalancer.reconstructURI(
				new DelegatingServiceInstance(instance, overrideScheme), uri);

		log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		return chain.filter(exchange);
	}

看了下Spring Cloud Gateway官網的原理圖,知道了filter應該是經由webhandler做的輪詢處理,找找代碼,看看是不是這麼回事

 

果不其然,列表中有個過濾器就是LoadBalancerClientFilter,而這裏的Mono.defer表示創建反應流,如果使用Mono.just會在聲明階段構造對象,只創建一次,但是Mono.defer卻是在subscribe階段纔會創建對應的對象,每次調用subscribe方法都會創建對象。

@Override
		public Mono<Void> filter(ServerWebExchange exchange) {
			return Mono.defer(() -> {
				if (this.index < filters.size()) {
					GatewayFilter filter = filters.get(this.index);
					DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this,
							this.index + 1);
					return filter.filter(exchange, chain);
				}
				else {
					return Mono.empty(); // complete
				}
			});
		}

那爲什麼使用defer每次subscribe被調用的時候會執行創建呢?因爲defer傳入了匿名類給Mono並賦值給了Supplier,Supplier什麼作用呢,只有調用Supplier裏的get方法,纔會真正構建對象,而MonoDefer正是在調用subscribe時候調用supplier.get()方法,此時也就解釋了只有調用subscribe時候纔會構建對象的說法,代碼如下:

public void subscribe(CoreSubscriber<? super T> actual) {
		Mono<? extends T> p;

		try {
			p = Objects.requireNonNull(supplier.get(),
					"The Mono returned by the supplier is null");
		}
		catch (Throwable e) {
			Operators.error(actual, Operators.onOperatorError(e, actual.currentContext()));
			return;
		}

		p.subscribe(actual);
	}

 

認真寫寫博客,寫寫生活點滴

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章