微服務通訊之ribbon實現原理

泥粑發表於2020-11-04

前言

上一篇我們知道了feign呼叫實現負載均衡是通過整合ribbon實現的。也較為詳細的瞭解到了整合的過程。現在我們看一下ribbo是如何實現負載均衡的。寫到這裡我尚未去閱讀原始碼,我在這裡盲猜一下: 他肯定是有一個從註冊中心拉取配置的模組,一個選擇呼叫服務的模組。然後我們就帶著這樣的指導思想去看原始碼。

一、ribbo是何時從eurake載入的服務列表?

從上一篇文章我們知道,feign呼叫實際上呼叫的是AbstractLoadBalancerAwareClient.executeWithLoadBalancer(...)方法,我們看一下該方法做了那些事情:


public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            // 省略
        }
        
    }


可以看到上述程式碼主要是建立了一個負載均衡執行命令類,然後執行請求。我們看看提交請求後執行的具體邏輯:


public Observable<T> submit(final ServerOperation<T> operation) {
        final ExecutionInfoContext context = new ExecutionInfoContext();
        

        // Use the load balancer
        Observable<T> o = 
                (server == null ? selectServer() : Observable.just(server))
                .concatMap(...);

       // 省略部分程式碼
    }

這裡可以看到它使用了RXjava,rxjava主要是利用觀察者思想實現的鏈式呼叫,其原始碼的實現稍顯複雜,自己使用需要謹慎,借用Uncle Ben的一句話 ”with great power comes great responsibility “。我們關心一下上面的selectServers()方法,從方法名我們可以大致猜出,它是在選擇呼叫的服務。我們看一下此方法的實現:


    /**
     * Return an Observable that either emits only the single requested server 
     * or queries the load balancer for the next server on each subscription
     */
    private Observable<Server> selectServer() {
        return Observable.create(new OnSubscribe<Server>() {
            @Override
            public void call(Subscriber<? super Server> next) {
                try {
                    Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception e) {
                    next.onError(e);
                }
            }
        });
    }

顯然核心邏輯在loadBalancerContext.getServerFromLoadBalancer(...)裡面,我們繼續向下看:


public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
       

        // 省略部分程式碼
        ILoadBalancer lb = getLoadBalancer();
        if (host == null) {
            // Partial URI or no URI Case
            // well we have to just get the right instances from lb - or we fall back
            if (lb != null){
                Server svc = lb.chooseServer(loadBalancerKey);
            } 
        }

        // 省略部分程式碼
        return new Server(host, port);
    }

非常戲劇性的是,我們看到他選擇一個服務是通過ILoadBalancer進行的,那我們的看一下ILoadBalancer是怎麼建立的。首先ILoadBalancer是屬於Ribbon提供的類,其建立我們先看Ribbon自身的ILoadbalance建立過程,發現其是通過RibbonClientConfiguration的下述方法建立:


        @Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

可以看到上述方法先判斷name對應的ILoadBalancer是否存在,不存在就建立了一個。我們看一下這個name代表的是什麼:


	@RibbonClientName
	private String name = "client";

可以看到它取得的是客戶端名稱,其實就是@FeignClient的name屬性的值,這裡可以直接告訴你怎麼實現動態給這裡的name賦值的:在生成feign的代理物件過程的實現,會將feign通過@FeingClient的contentId屬性進行Context隔離,在載入配置的時候會將@RibbonClientName屬性的@Value屬性的環境變數的值設定成當前@FeignClient的name值,具體可以直看NamedContextFactory.createContext()方法。言歸正轉,ZoneAwareLoadBalancer的構建顯然是持有了Config,Rule,Ping,ServerList,ServerListFilter,ServerListUpdater。
現在我們看一下這四個類的分工與職責。

  • Config

config 主要是配置了服務名稱,服務讀取的一些配置比如重新整理服務間隔等

  • Rule

選用服務的規則,他決定了選用哪個服務。

  • Ping

主要用於探測服務列表中的服務是否可用

  • ServerList

它主要是提供最新服務列表:包括上線、下線的服務(預設實現DiscoveryEnabledNIWSServerList)

  • ServerListFilter

用於過濾服務,do what you want to do.

  • ServerListUpdater

用於更新服務列表(當前的預設實現是啟動定時器,然後呼叫ServerList獲取服務列表,將當前負載均衡可用服務列表全覆蓋)。

知道了上面的職責劃分,我們看到ZoneAwareLoadBalancer最終初始化服務列表的方法:


    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            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);
    }

基於上述我們可以清晰的瞭解到,獲取服務列表發生在ServerList的實現中(serverListImpl.getUpdatedListOfServers())。

二、ribbo配合Eurake的ServerList實現

上面serverListImpl 實現類就是DiscoveryEnabledNIWSServerList,當呼叫getUpdatedListOfServers()最終呼叫到了如下所述方法,我們看一下他幹了些什麼:

// 省略了部分程式碼
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
        EurekaClient eurekaClient = eurekaClientProvider.get();
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                for (InstanceInfo ii : listOfInstanceInfo) {
                    if (ii.getStatus().equals(InstanceStatus.UP)) {
                        DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
                        serverList.add(des);
                    }
                }
            }
        }
        return serverList;
    }

上述程式碼主要是通過EurakeClient 獲取對應服務例項,然後將服務例項向DiscoveryEnabledServer進行對映。至此從服務列表的獲取過程就完成了。

三、 Rule 服務的選擇

知道了服務列表的來源,接下來就是最關鍵的一步了,選擇一個服務呼叫。我們先看一下預設情況使用的是什麼規則(RibbonClientConfiguration):


        @Bean
	@ConditionalOnMissingBean
	public IRule ribbonRule(IClientConfig config) {
		if (this.propertiesFactory.isSet(IRule.class, name)) {
			return this.propertiesFactory.get(IRule.class, config, name);
		}
		ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
		rule.initWithNiwsConfig(config);
		return rule;
	}

顯然預設使用的是ZoneAvoidanceRule。 這裡我們看一下IRule都有哪些實現,然後闡述一下各個演算法實現(這裡沒有再看原始碼的必要了,演算法就那些):

  • ZoneAvoidanceRule
    基於可用性與區域的複合預測規則
  • RoundRobinRule
    輪詢
  • WeightedResponseTimeRule
    根據響應時間選擇
  • RandomRule
    隨機規則

小結

總結起來,ribbon的核心組成就是ZoneAwareLoadBalancer的組成。其分工明確,服務列表的維護、服務的可用性、服務的選擇都有專門的類去負責。下一篇將會著重講一下Feign、Ribbon如何做到基於服務的配置隔離的,會適當配合實戰,比如怎麼自定義一個全域性生效的路由規則,自定義一個基於服務名的生效的路由規則等等。

相關文章