彈力設(shè)計之重試設(shè)計

關(guān)于重試,這個模式應(yīng)該是一個很普遍的設(shè)計模式了。當(dāng)我們把單體應(yīng)用服務(wù)化,尤其是微服務(wù)化,本來在一個進(jìn)程內(nèi)的函數(shù)調(diào)用就成了遠(yuǎn)程調(diào)用,這樣就會涉及到網(wǎng)絡(luò)上的問題。

網(wǎng)絡(luò)上有很多的各式各樣的組件,如 DNS 服務(wù)、網(wǎng)卡、交換機(jī)、路由器、負(fù)載均衡等設(shè)備,這些設(shè)備都不一定是穩(wěn)定的。在數(shù)據(jù)傳輸?shù)恼麄€過程中,只要任何一個環(huán)節(jié)出了問題,最后都會影響系統(tǒng)的穩(wěn)定性。

重試的場景

所以,我們需要一個重試的機(jī)制。但是,我們需要明白的是,“重試”的語義是我們認(rèn)為這個故障是暫時的,而不是永久的,所以,我們會去重試。

設(shè)計重試時,我們需要定義出什么情況下需要重試,例如,調(diào)用超時、被調(diào)用端返回了某種可以重試的錯誤(如繁忙中、流控中、維護(hù)中、資源不足等)。

而對于一些別的錯誤,則最好不要重試,比如:業(yè)務(wù)級的錯誤(如沒有權(quán)限、或是非法數(shù)據(jù)等錯誤),技術(shù)上的錯誤(如:HTTP 的 503 等,這種原因可能是觸發(fā)了代碼的 bug,重試下去沒有意義)。

重試的策略

關(guān)于重試的設(shè)計,一般來說,都需要有個重試的最大值,經(jīng)過一段時間不斷的重試后,就沒有必要再重試了,應(yīng)該報故障了。在重試過程中,每一次重試失敗時都應(yīng)該休息一會兒再重試,這樣可以避免因?yàn)橹卦囘^快而導(dǎo)致網(wǎng)絡(luò)上的負(fù)擔(dān)加重。

在重試的設(shè)計中,我們一般都會引入,Exponential Backoff 的策略,也就是所謂的 " 指數(shù)級退避 "。在這種情況下,每一次重試所需要的休息時間都會成倍增加。這種機(jī)制主要是用來讓被調(diào)用方能夠有更多的時間來從容處理我們的請求。這其實(shí)和 TCP 的擁塞控制有點(diǎn)像。

如果我們寫成代碼應(yīng)該是下面這個樣子。

首先,我們定義一個調(diào)用返回的枚舉類型,其中包括了 5 種返回錯誤——成功 SUCCESS、維護(hù)中 NOT_READY、流控中 TOO_BUSY、沒有資源 NO_RESOURCE、系統(tǒng)錯誤 SERVER_ERROR。

public enum Results { SUCCESS, NOT_READY, TOO_BUSY, NO_RESOURCE, SERVER_ERROR}

接下來,我們定義一個 Exponential Backoff 的函數(shù),其返回 2 的指數(shù)。這樣,每多一次重試就需要多等一段時間。如:第一次等 200ms,第二次要 400ms,第三次要等 800ms……

public static long getWaitTimeExp(int retryCount) { 
long waitTime = ((long) Math.pow(2, retryCount) ); 
return waitTime;
}

下面是真正的重試邏輯。我們可以看到,在成功的情況下,以及不屬于我們定義的錯誤下,我們是不需要重試的,而兩次重試間需要等的時間是以指數(shù)上升的。


public static void doOperationAndWaitForResult() {
    
    // Do some asynchronous operation.
long token = asyncOperation();

    int retries = 0;
    boolean retry = false;

    do {
        // Get the result of the asynchronous operation.
        Results result = getAsyncOperationResult(token);

        if (Results.SUCCESS == result) {
            retry = false;
        } else if ( (Results.NOT_READY == result) ||
                      (Results.TOO_BUSY == result) ||
                      (Results.NO_RESOURCE == result) ||
                      (Results.SERVER_ERROR == result) ) {
            retry = true;
        } else {
            retry = false;
        }
        if (retry) {
            long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);
            // Wait for the next Retry.
            Thread.sleep(waitTime);
        }
    } while (retry && (retries++ < MAX_RETRIES));
}

重試設(shè)計的重點(diǎn)

  • 要確定什么樣的錯誤下需要重試;
  • 重試的時間和重試的次數(shù)。這種在不同的情況下要有不同的考量。有時候,而對一些不是很重要的問題時,我們應(yīng)該更快失敗而不是重試一段時間若干次。比如一個前端的交互需要用到后端的服務(wù)。這種情況下,在面對錯誤的時候,應(yīng)該快速失敗報錯(比如:網(wǎng)絡(luò)錯誤請重試)。而面對其它的一些錯誤,比如流控,那么應(yīng)該使用指數(shù)退避的方式,以避免造成更多的流量。
  • 如果超過重試次數(shù),或是一段時間,那么重試就沒有意義了。這個時候,說明這個錯誤不是一個短暫的錯誤,那么我們對于新來的請求,就沒有必要再進(jìn)行重試了,這個時候?qū)π碌恼埱笾苯臃祷劐e誤就好了。但是,這樣一來,如果后端恢復(fù)了,我們怎么知道呢,此時需要使用我們的熔斷設(shè)計了。
  • 重試還需要考慮被調(diào)用方是否有冪等的設(shè)計。如果沒有,那么重試是不安全的,可能會導(dǎo)致一個相同的操作被執(zhí)行多次。
  • 對于有事務(wù)相關(guān)的操作。我們可能會希望能重試成功,而不至于走業(yè)務(wù)補(bǔ)償那樣的復(fù)雜的回退流程。對此,我們可能需要一個比較長的時間來做重試,但是我們需要保存請求的上下文,這可能對程序的運(yùn)行有比較大的開銷,因此,有一些設(shè)計會先把這樣的上下文暫存在本機(jī)或是數(shù)據(jù)庫中,然后騰出資源來做別的事,過一會再回來把之前的請求從存儲中撈出來重試。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容