任何通過網路與其它應用通訊地的程式,都應該有足夠的靈活性,來應對短暫的臨時性故障。因為這些故障很多時候是可以自恢復的。 例如,為了避免服務過載,很多應用會採取某些限流措施,在併發請求達到一定數量時,暫時性的拒絕新的請求加入。這種情況下,嘗試使用該應用的程式,一開始可能會被拒絕連接,但下一刻就好了。 ...
任何通過網路與其它應用通訊地的程式,都應該有足夠的靈活性,來應對短暫的臨時性故障。因為這些故障很多時候是可以自恢復的。
例如,為了避免服務過載,很多應用會採取某些限流措施,在併發請求達到一定數量時,暫時性的拒絕新的請求加入。這種情況下,嘗試使用該應用的程式,一開始可能會被拒絕連接,但下一刻就好了。
因此,在設計系統時,應該考慮到此種故障。並且在條件允許時,加入重試機制,自動再次發起相應的請求。在某些情況下,可能會顯著的改善應用程式的用戶體驗。
能否發起重試,最重要的前提之一是,對同一資源的發起多次相同的請求,能否得到相同的結果。即資源介面是否具有冪等性。
標準 REST API 中,GET/HEAD/OPTIONS 通常是不會更改伺服器上的資源的,因此大多是可重試的。
換個說法就是,如果能夠確定,下次請求有可能成功,那就可以嘗試重試。否則的話,就不必浪費時間、精力以及系統資源了。
例如,在請求 HTTP 服務時,收到 503 或 408 這樣的狀態碼,則重試可能會有效;但是如果收到了 401 或 403 之類的狀態碼,則簡單的重試肯定不起作用。
確定了什麼情況下發起重試後,還有另外一個問題值得考慮。即在什麼時間、以什麼樣的頻率發起重試。儘管可以,但通常並不會是請求失敗後,立即發起重試,而是需要根據具體的場景,選擇合適的重試
時機。
假如說我們請求失敗的原因,是服務端請求過載。則立即發起重試,除了給服務端添亂外,不會有其它結果。嚴重情況下,可能會加劇伺服器的負擔,直到耗盡伺服器資源。
為了避免上述問題,常見的做法是在重試之前增加一些延遲。但是如何增加這些延遲,又有多種策略,比較多使用的有兩種:
- 固定間隔。即每次請求失敗後,都等待固定的時間後,再次發起下次重試請求;
- 指數遞增。即多次請求之間的延時,成倍增加;
如果失敗是由服務端過載引起的,則後一種策略可能會更好。假如它的初始請求在 0ms 發出,則第二次請求在失敗 200ms 後發出,第三次請求在失敗 400ms 後發出,第四次在 800ms... 以此類推。這種分散的請求和重試機制,可能有助於減緩客戶端及伺服器的負載,提高我們最終獲取到成功結果的機會。
但是假如所有的請求都從同一時刻發起,並按照同樣的機制延時重試,則兩種策略是一樣的,並不會有所改進。這種情況下,可能需要加入一些隨機因素。
扯的有點多,接下來搞些實際的,上一點兒代碼。簡單起見,這裡演示一下在 JavaScript 中的實現,使用諸位都瞭解的 axios。Axios 提供了攔截器機制,用來處理通用的重試邏輯正合適。
const SLEEP_TIME = 200;
const RETRY_TIMES = 3;
const RETRY_STATUS = [408, 503, 504];
function getSleepTime(i = 0) {
const wTime = Math.min(SLEEP_TIME * 2 ** i);
return Math.random() * wTime;
}
async function fakeSleep(ms) {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
async function onError(error) {
const { status } = error.response;
let { retryCount = 0 } = error.response.config;
if (RETRY_STATUS.includes(status) && retryCount < RETRY_TIMES) {
retryCount += 1;
sleep_ms = getSleepTime(retryCount);
await fakeSleep(sleep_ms);
error.config.retryCount = retryCount;
return await axios(error.config);
}
}
axios.interceptors.response.use(onError);
代碼不多,應該不需要額外的解釋。其它編程語言,實現思路大致也差不多。相信您肯定可以寫出更好的。
歡迎批評指正。
作者:袁首京原創文章,轉載時請保留此聲明,並給出原文連接。