SOFA 原始碼分析 — 預熱權重

莫那·魯道發表於2019-02-25
SOFA  原始碼分析 — 預熱權重

前言

SOFA-RPC 支援根據權重對服務進行預熱功能,具體地址:預熱權重.

引用官方文件:

預熱權重功能讓客戶端機器能夠根據服務端的相應權重進行流量的分發。該功能也常被用於叢集內少數機器的啟動場景。利用流量權重功能在短時間內對服務端機器進行預熱,然後再接收正常的流量比重。 執行機制如下:

SOFA  原始碼分析 — 預熱權重

1.服務端服務在啟動時會將自身的預熱時間,預熱期內權重,預熱完成後的正常權重推送給服務註冊中心。如上圖 ServiceB 指向 Service Registry 。

2.客戶端在引用服務的時候會獲得每個服務例項的預熱權重資訊。如上圖 Service Registry 指向 client 。

3.客戶端在進行呼叫的時候會根據服務所在地址的預熱時期所對應的權重進行流量分發。如上圖 client 指向 ServiceA 和 ServiceB 。 ServiceA 預熱完畢,權重預設 100 , ServiceB 處於預熱期,權重為 10,因此所承受流量分別為 100%110 和 10%110 。

如何使用

該功能使用方式如下。

ProviderConfig<HelloWordService> providerConfig = new ProviderConfig<HelloWordService>() 
            .setWeight(100) 
            .setParameter(ProviderInfoAttrs.ATTR_WARMUP_WEIGHT,"10") 
            .setParameter(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME,"12000");
複製程式碼

如上,該服務的預熱期為12s,在預熱期內權重為10,預熱期結束後的正常權重為100。如果該服務一共釋出在兩個機器A,B上,A機器正處於預熱期內,並使用上述配置,B已經完成預熱,正常權重為200。那麼客戶端在呼叫的時候,此時流量分發的比重為10:200,A機器預熱結束後,流量分發比重為100:200。 在SOFABoot中,如下配置預熱時間,預熱期間權重和預熱完後的權重即可。

<sofa:reference id="sampleRestFacadeReferenceBolt" interface="com.alipay.sofa.endpoint.facade.SampleFacade">
    <sofa:binding.bolt>
         <sofa:global-attrs weight="100" warm-up-time="10000" warm-up-weight="1000"/>
     </sofa:binding.bolt>
</sofa:reference>
複製程式碼

再來看看原始碼實現。

原始碼分析

從 demo 中看,SOFA 需要在 ProviderConfig 中配置屬性,而這些屬性都是儲存在一個 Map 中。

程式碼:

 public S setParameter(String key, String value) {
        if (parameters == null) {
            parameters = new ConcurrentHashMap<String, String>();
        }
        if (value == null) {
            parameters.remove(key);
        } else {
            parameters.put(key, value);
        }
        return castThis();
    }
複製程式碼

當釋出服務的時候,這個 Map 會被髮布到註冊中心。具體程式碼如下:

    protected void doRegister(String appName, String serviceName, ProviderInfo providerInfo) {
        if (LOGGER.isInfoEnabled(appName)) {
            LOGGER.infoWithApp(appName, LogCodes.getLog(LogCodes.INFO_ROUTE_REGISTRY_PUB, serviceName));
        }
        //{service : [provider...]}
        ProviderGroup oldGroup = memoryCache.get(serviceName);
        if (oldGroup != null) { // 存在老的key
            oldGroup.add(providerInfo);
        } else { // 沒有老的key,第一次加入
            List<ProviderInfo> news = new ArrayList<ProviderInfo>();
            news.add(providerInfo);
            memoryCache.put(serviceName, new ProviderGroup(news));
        }
        // 備份到檔案 改為定時寫
        needBackup = true;
        doWriteFile();

        if (subscribe) {
            notifyConsumerListeners(serviceName, memoryCache.get(serviceName));
        }
    }
複製程式碼

上面的程式碼中,提供者會將 providerInfo 的資訊寫到本地檔案(註冊中心)中。

而消費者則會從註冊中心訂閱服務列表的資訊。具體程式碼如下:

    @Override
    public List<ProviderGroup> subscribe(ConsumerConfig config) {
        String key = LocalRegistryHelper.buildListDataId(config, config.getProtocol());
        List<ConsumerConfig> listeners = notifyListeners.get(key);
        if (listeners == null) {
            listeners = new ArrayList<ConsumerConfig>();
            notifyListeners.put(key, listeners);
        }
        listeners.add(config);
        // 返回已經載入到記憶體的列表(可能不是最新的)
        ProviderGroup group = memoryCache.get(key);
        if (group == null) {
            group = new ProviderGroup();
            memoryCache.put(key, group);
        }
        return Collections.singletonList(group);
    }
複製程式碼

上面這段程式碼會被 DefaultConsumerBootstrap 呼叫,根據消費者的配置資訊,生成一個 key,然後將消費者新增到通知列表中(當資料變化時,通知消費者,由定時任務執行)。

然後,從記憶體中取出key 對應的服務分組,並返回集合(就是提供者註冊的資訊)。

這段程式碼會在 AbstractCluster 的 init 方法中呼叫—— List<ProviderGroup> all = consumerBootstrap.subscribe();

服務分組的資料結構是 ProviderInfo,是一個抽象的服務提供列表,其中包含服務的資訊,比如地址,協議型別,主機地址,埠,路徑,版本,動態引數,靜態引數,服務狀態等等,其中就包括權重

獲取權重的方法如下:

public int getWeight() {
    ProviderStatus status = getStatus();
    if (status == ProviderStatus.WARMING_UP) {
        try {
            // 還處於預熱時間中
            Integer warmUpWeight = (Integer) getDynamicAttr(ProviderInfoAttrs.ATTR_WARMUP_WEIGHT);
            if (warmUpWeight != null) {
                return warmUpWeight;
            }
        } catch (Exception e) {
            return weight;
        }
    }
    return weight;
}
複製程式碼

注意 getStatus 方法:

public ProviderStatus getStatus() {
    if (status == ProviderStatus.WARMING_UP) {
        if (System.currentTimeMillis() > (Long) getDynamicAttr(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME)) {
            // 如果已經過了預熱時間,恢復為正常
            status = ProviderStatus.AVAILABLE;
            setDynamicAttr(ProviderInfoAttrs.ATTR_WARM_UP_END_TIME, null);
        }
    }
    return status;
}
複製程式碼

邏輯如下:

獲取服務狀態,如果是預熱狀態,則獲取預熱狀態的權重值,反之,如果不是,反之正常值(預設 100)。

獲取狀態的方法則是判斷時間,如果當前時間大於預熱時間,則修改狀態為可用。並刪除動態引數列表中的“預熱時間”。

那麼,什麼時候會獲取權重呢?

如果看過之前文章的同學肯定知道,在負載均衡的時候,會呼叫。

我們看看預設的隨機均衡演算法。還記得當時,樓主有個地方不是很明白,我們要根據權重隨機,當時看來,並沒有什麼用處,今天明白了。再上一遍程式碼吧:

@ AbstractLoadBalancer.java
protected int getWeight(ProviderInfo providerInfo) {
    // 從provider中或得到相關權重,預設值100
    return providerInfo.getWeight() < 0 ? 0 : providerInfo.getWeight();
}
複製程式碼

獲取權重,預設 100.

再看隨機演算法的 doSelect 方法。

@ RandomLoadBalancer.java
@Override
public ProviderInfo doSelect(SofaRequest invocation, List<ProviderInfo> providerInfos) {
    ProviderInfo providerInfo = null;
    int size = providerInfos.size(); // 總個數
    int totalWeight = 0; // 總權重
    boolean isWeightSame = true; // 權重是否都一樣
    for (int i = 0; i < size; i++) {
        int weight = getWeight(providerInfos.get(i));
        totalWeight += weight; // 累計總權重
        if (isWeightSame && i > 0 && weight != getWeight(providerInfos.get(i - 1))) {
            isWeightSame = false; // 計算所有權重是否一樣
        }
    }
    if (totalWeight > 0 && !isWeightSame) {
        // 如果權重不相同且權重大於0則按總權重數隨機
        int offset = random.nextInt(totalWeight);
        // 並確定隨機值落在哪個片斷上
        for (int i = 0; i < size; i++) {
            offset -= getWeight(providerInfos.get(i));
            if (offset < 0) {
                providerInfo = providerInfos.get(i);
                break;
            }
        }
    } else {
        // 如果權重相同或權重為0則均等隨機
        providerInfo = providerInfos.get(random.nextInt(size));
    }
    return providerInfo;
}
複製程式碼

首先判斷各個服務的權重是否相同,如果不同,進入第二個 if。

關鍵點來了,如果權重不同,那麼從總的權重中,隨機一個數,一次從服務列表的權重遞減。知道該值小於0,那麼就使用該服務。

這樣就能大致保證權重小的被擊中的機率較小。具體取決於 Java 的隨機演算法,但是我們還是比較相信 Java 的。

我們來推倒一下這個演算法。

假設有 A, B, C, 3 個服務,每個服務預設權重 100,其中 C 現在處於預熱階段,則 C 的權重等於 10.

那麼總權重 210。

如果C落在第一位,那麼一定會選中C的情況是權重落在0-9之間;
如果C落在第二位,那麼一定會選中C的情況是權重落在100-109之間;
如果C是在第三位,那麼一定會選中C的情況是權重落在200-209;

符合權重。

總結

現在看來,預熱權重還是挺簡單的,主要在負載均衡出進行處理就行。

今天就到這裡,bye!!!

相關文章