超詳細的Ribbon原始碼解析

女友在高考發表於2021-10-18

Ribbon簡介

什麼是Ribbon?

Ribbon是springcloud下的客戶端負載均衡器,消費者在通過服務別名呼叫服務時,需要通過Ribbon做負載均衡獲取實際的服務呼叫地址,然後通過httpclient的方式進行本地RPC遠端呼叫。

Ribbon原理

Ribbon負載均衡演算法主要是輪詢演算法,分為以下幾步:

  1. 根據服務別名,從eureka獲取服務提供者的列表
  2. 將列表快取到本地
  3. 根據具體策略獲取服務提供者

Ribbon的核心是負載均衡管理,另還有5個大功能點。如下圖:

原始碼分析

事前準備

  1. 先搭建一個SpringCloud的專案,也可以從我的github上下載。地址:https://github.com/mmcLine/spring-cloud-study

  2. 拷貝以下程式碼

@Configuration
public class RestTemplateConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return  new RestTemplate();
    }
}
 @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/testRibbon/{id}")
    public User getTodayStatistic(@PathVariable("id") Integer id){
        String url  ="http://STUDY-USER/user/getUserById?id="+id;
        return restTemplate.getForObject(url, User.class);
    }

程式碼都準備好了,可以開始分析了。

  1. 執行呼叫

http://localhost:8005/trade/testRibbon/2

為什麼這麼就能呼叫到服務提供者的方法?

打斷點,可以看到restTemplate裡有兩個攔截器,根據名字可以推斷RetryLoadBalancerInterceptor是關鍵。

跟蹤到RetryLoadBalancerInterceptor類

@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
										final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		//獲取到service的name
		final String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		//根據serviceName和LoadBalancerClient,LoadBalancedRetryPolicy裡面包含了RibbonLoadBalancerContext和ServiceInstanceChooser
		final LoadBalancedRetryPolicy retryPolicy = lbRetryFactory.createRetryPolicy(serviceName,
				loadBalancer);
		RetryTemplate template = createRetryTemplate(serviceName, request, retryPolicy);
		//執行方法會進入到doExecute方法
		return template.execute(context -> {
			ServiceInstance serviceInstance = null;
			if (context instanceof LoadBalancedRetryContext) {
				LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
				serviceInstance = lbContext.getServiceInstance();
			}
			if (serviceInstance == null) {
				serviceInstance = loadBalancer.choose(serviceName);
			}
			ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute(
					serviceName, serviceInstance,
					requestFactory.createRequest(request, body, execution));
			int statusCode = response.getRawStatusCode();
			if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
				byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
				response.close();
				throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
			}
			return response;
		}, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
			//This is a special case, where both parameters to LoadBalancedRecoveryCallback are
			//the same.  In most cases they would be different.
			@Override
			protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) {
				return response;
			}
		});
	}

doExecute方法:

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
			RecoveryCallback<T> recoveryCallback, RetryState state)
			throws E, ExhaustedRetryException {
        //省略部分程式碼

			/*
			 * We allow the whole loop to be skipped if the policy or context already
			 * forbid the first try. This is used in the case of external retry to allow a
			 * recovery in handleRetryExhausted without the callback processing (which
			 * would throw an exception).
			 */
			 //執行邏輯的關鍵方法
			while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

				}

繼續跟蹤canRetry方法

  @Override
    public boolean canRetry(RetryContext context) {
        LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
        if(lbContext.getRetryCount() == 0  && lbContext.getServiceInstance() == null) {
            //We haven't even tried to make the request yet so return true so we do
            //設定選中的服務提供者
            lbContext.setServiceInstance(serviceInstanceChooser.choose(serviceName));
            return true;
        }
        return policy.canRetryNextServer(lbContext);
    }

我們跟蹤serviceInstanceChooser.choose(serviceName)看看怎麼通過serviceName選服務提供者的。

@Override
	public ServiceInstance choose(String serviceId) {
	    //選擇server
		Server server = getServer(serviceId);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}

跟蹤getServer方法

protected Server getServer(ILoadBalancer loadBalancer) {
		if (loadBalancer == null) {
			return null;
		}
		//可以看出是loadBalancer在選擇
		return loadBalancer.chooseServer("default"); // TODO: better handling of key
	}

繼續深入

 public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        //有一個呼叫次數在+1
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                //委託給了IRule,所以Irule是負載均衡的關鍵,最後來總結
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

檢視Irule的實現

 public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        //lb.getAllServers裡面是所有的服務提供者列表
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }

跟蹤chooseRoundRobinAfterFiltering方法

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        //拿到篩選後的servers
        List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
        //incrementAndGetModulo方法拿到下標,然後根據list.get取到一個服務
        return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
    }

至此就拿到了具體的服務提供者。

但是到這裡還有個問題?

  1. 怎麼根據服務名拿到server的?

有一個ServerList介面是用於拿到服務列表的。我們使用的loadBalancer(ZoneAwareLoadBalancer)的父類DynamicServerListLoadBalancer類的構造方法裡,有一個restOfinit方法

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);
    }

跟蹤restOfInit方法

void restOfInit(IClientConfig clientConfig) {
        boolean primeConnection = this.isEnablePrimingConnections();
        // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
        this.setEnablePrimingConnections(false);
        enableAndInitLearnNewServersFeature();
        
        //用於獲取所有的serverList
        updateListOfServers();
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections()
                    .primeConnections(getReachableServers());
        }
        this.setEnablePrimingConnections(primeConnection);
        LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
    }

繼續跟蹤updateListOfServers方法

 public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            //查詢serverList
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        updateAllServerList(servers);
    }

繼續跟蹤原始碼到obtainServersViaDiscovery方法,

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
    //eurekaClientProvider.get()會去獲取EurekaClient
        if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList<DiscoveryEnabledServer>();
        }

        EurekaClient eurekaClient = eurekaClientProvider.get();
        //vipAddresses就是serviceName
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                // if targetRegion is null, it will be interpreted as the same region of client
                //此處獲取到服務的資訊
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                for (InstanceInfo ii : listOfInstanceInfo) {
                    if (ii.getStatus().equals(InstanceStatus.UP)) {

                        if(shouldUseOverridePort){
                            if(logger.isDebugEnabled()){
                                logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                            }

                            // copy is necessary since the InstanceInfo builder just uses the original reference,
                            // and we don't want to corrupt the global eureka copy of the object which may be
                            // used by other clients in our system
                            InstanceInfo copy = new InstanceInfo(ii);

                            if(isSecure){
                                ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                            }else{
                                ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                            }
                        }

                        DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
                        des.setZone(DiscoveryClient.getZone(ii));
                        serverList.add(des);
                    }
                }
                if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                    break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
                }
            }
        }
        return serverList;
    }

綜合上面可以看出,最終是通過eurekaClient去拿到服務列表的。

那麼如果服務列表發生變化怎麼重新整理呢?

是通過CacheRefreshThread在定時執行緒池裡面執行,核心拉取方法是fetchRegistry

Iping

Iping是用於探測服務列表中的服務是否正常,如果不正常,則從eureka拉取服務列表並更新。

在BaseLoadBalancer裡面有一個setupPingTask方法,啟動定時任務,30秒一次定時向EurekaClient傳送“ping”

public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
            IPing ping, IPingStrategy pingStrategy) {
	
        logger.debug("LoadBalancer [{}]:  initialized", name);
        
        this.name = name;
        this.ping = ping;
        this.pingStrategy = pingStrategy;
        setRule(rule);
        setupPingTask();
        lbStats = stats;
        init();
    }

Iping的具體邏輯在PingTask類裡。

Irule總結:

Irule是負載均衡的規則:

我這裡預設是使用的是ZoneAvoidanceRule,還有很多種策略:

  • RandomRule: 隨機
  • RoundRobinRule: 輪詢
  • RetryRule: 先按照RoundRobinRule的策略獲取服務,如果獲取服務失敗則在指定時間內會進行重試,獲取可用的服務
  • WeightedResponseTimeRule: 對RoundRobinRule的擴充套件,響應速度越快的例項選擇權重越大,越容易被選擇
  • BestAvailableRule:會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務
  • AvailabilityFilteringRule:先過濾掉故障例項,再選擇併發較小的例項
  • ZoneAvoidanceRule:預設規則,複合判斷server所在區域的效能和server的可用性選擇伺服器

properties配置方式如下:
STUDY-USER是服務名

STUDY-USER.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

相關文章