Backoff 退避策略是什麼?Exponential Backoff 指數退避完整教學
Backoff 退避策略是一種處理請求失敗與重試的核心技術。它的精髓在於:請求失敗後不立即重試,而是等待一段時間再試,且每次失敗後等待時間遞增,有效防止雪崩效應(Thundering Herd Problem),是所有生產環境必備的網路韌性策略。
什麼是 Backoff 退避策略?
想像一個情境:你打電話給客服,電話一直佔線。如果你每秒就重撥一次,不僅自己很累,還讓客服電話更難被接通。聰明的做法是:等 1 秒撥、再等 2 秒撥、再等 4 秒撥……讓線路有機會空出來。這就是 Backoff 的核心思想。
在軟體系統中,Backoff 退避策略指的是:當 API 請求或網路操作失敗後,不立即重試,而是等待一段時間後再重試,並且每次失敗後將等待時間逐漸增加。
為什麼不能立即重試?
立即重試(Immediate Retry)在大多數情況下是有害的:
- 伺服器已過載:請求失敗通常正是因為伺服器正在忙碌(CPU 過高、DB 連線耗盡、流量尖峰)。立即重試等於在已過載的伺服器上繼續加壓,讓它更難恢復。
- 雪崩效應(Thundering Herd):想像 1000 個客戶端同時遇到失敗,如果所有人都在第 1 秒重試,伺服器會在同一時間收到 1000 個重試請求,比原來的負載更高。
- 網路抖動(Network Jitter):短暫的網路波動通常在幾百毫秒內就會恢復,等一下再試通常就能成功,完全不需要立即重試。
Backoff 策略讓重試時間錯開,給伺服器喘息的空間,同時又保證不會永無止境地等待。
三種 Backoff 策略
1. Fixed Backoff(固定退避)
每次失敗後,等待固定的時間間隔再重試。
第 1 次失敗 → 等 2 秒 → 重試
第 2 次失敗 → 等 2 秒 → 重試
第 3 次失敗 → 等 2 秒 → 重試
優點:最簡單,行為可預測。 缺點:無法應對長時間的伺服器問題,重試頻率一直不變。
2. Linear Backoff(線性退避)
每次失敗後,等待時間線性增加:1 秒、2 秒、3 秒、4 秒……
第 1 次失敗 → 等 1 秒 → 重試
第 2 次失敗 → 等 2 秒 → 重試
第 3 次失敗 → 等 3 秒 → 重試
第 4 次失敗 → 等 4 秒 → 重試
優點:比固定退避更有彈性,給伺服器更多恢復時間。 缺點:增長速度相對緩慢,在大規模系統中分散效果不如指數退避。
3. Exponential Backoff(指數退避)
每次失敗後,等待時間以2 的冪次方增長:1 秒、2 秒、4 秒、8 秒、16 秒……
第 1 次失敗 → 等 1 秒 → 重試
第 2 次失敗 → 等 2 秒 → 重試
第 3 次失敗 → 等 4 秒 → 重試
第 4 次失敗 → 等 8 秒 → 重試
第 5 次失敗 → 等 16 秒 → 重試(通常設上限)
優點:給伺服器足夠的恢復時間,是分散重試壓力最有效的策略,也是雲端服務(AWS、GCP)推薦的標準做法。 缺點:等待時間可能增長過快,需要設定上限(Max Delay)。
指數退避是工業界最廣泛採用的 Backoff 策略。
計算公式詳解
基本公式
delay = min(base × 2^n, maxDelay)
base:基礎等待時間(通常設 1000 毫秒 = 1 秒)n:已失敗次數(從 0 開始)maxDelay:最大等待時間上限(通常設 30 秒)
| 第幾次失敗 (n) | 計算 | 結果 | 有上限(30秒)後 |
|---|---|---|---|
| 0 | 1000 × 2⁰ | 1 秒 | 1 秒 |
| 1 | 1000 × 2¹ | 2 秒 | 2 秒 |
| 2 | 1000 × 2² | 4 秒 | 4 秒 |
| 3 | 1000 × 2³ | 8 秒 | 8 秒 |
| 4 | 1000 × 2⁴ | 16 秒 | 16 秒 |
| 5 | 1000 × 2⁵ | 32 秒 | 30 秒(被上限截斷) |
| 6 | 1000 × 2⁶ | 64 秒 | 30 秒 |
Full Jitter(完全隨機化)
光靠指數退避還不夠。如果 1000 個客戶端同時失敗,它們計算出的退避時間是一樣的——它們仍然會在同一時間重試,雪崩效應依然存在。
解決方案是加入 Jitter(抖動):在退避時間中加入隨機性,讓不同客戶端的重試時間錯開。
Full Jitter(完全隨機化):在 0 到計算出的延遲之間隨機取一個值:
delay = random(0, min(base × 2^n, maxDelay))
function fullJitterDelay(n, base = 1000, maxDelay = 30000) {
const exponentialDelay = base * Math.pow(2, n);
const cappedDelay = Math.min(exponentialDelay, maxDelay);
return Math.random() * cappedDelay; // 在 0 到上限之間隨機取值
}
Equal Jitter(等分隨機化):保留一半的確定性,另一半加入隨機:
delay = delay/2 + random(0, delay/2)
function equalJitterDelay(n, base = 1000, maxDelay = 30000) {
const exponentialDelay = base * Math.pow(2, n);
const cappedDelay = Math.min(exponentialDelay, maxDelay);
const halfDelay = cappedDelay / 2;
return halfDelay + Math.random() * halfDelay; // 在 delay/2 到 delay 之間
}
為什麼 Jitter 很重要?
以一個具體案例說明:
- 情境:1000 個使用者同時在用你的服務,伺服器在 t=0 時發生短暫故障,所有請求在同一時刻失敗。
- 沒有 Jitter:1000 個客戶端計算出相同的退避時間(例如 2 秒),在 t=2s 時同時發出 1000 個重試請求,伺服器剛恢復又被打垮。
- 有 Full Jitter:1000 個客戶端在 0 到 2 秒之間隨機分散重試,伺服器每秒大約收到 500 個請求,能平穩地處理完畢。
結論:在高並發系統中,Jitter 是 Backoff 策略不可缺少的一部分。
JavaScript 原生實作
以下是一個使用原生 fetch(不依賴 axios)實作帶 Jitter 指數退避的完整函式:
// fetchWithBackoff:帶指數退避 + Full Jitter 的 fetch 封裝
// 使用方式和 fetch() 完全相同,只是加了自動重試邏輯
async function fetchWithBackoff(url, options = {}, retryOptions = {}) {
const {
maxRetries = 3, // 最大重試次數
baseDelay = 1000, // 基礎延遲(毫秒)
maxDelay = 30000, // 最大延遲上限(毫秒)
retryOn = [500, 502, 503, 504, 429], // 哪些 HTTP 狀態碼才重試
} = retryOptions;
// 計算帶 Full Jitter 的指數退避延遲
function calculateDelay(attempt) {
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const cappedDelay = Math.min(exponentialDelay, maxDelay);
return Math.random() * cappedDelay; // Full Jitter
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
let lastError = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// 成功回應(2xx)直接回傳
if (response.ok) {
return response;
}
// 判斷這個狀態碼是否需要重試
if (!retryOn.includes(response.status)) {
// 4xx 錯誤(除了 429)通常是客戶端問題,不重試
throw new Error(`請求失敗(${response.status}),不重試`);
}
// 需要重試的伺服器錯誤
lastError = new Error(`伺服器回應錯誤:${response.status}`);
// 如果是最後一次嘗試,直接拋出錯誤
if (attempt === maxRetries) {
throw lastError;
}
// 特殊處理 429 Rate Limiting:優先使用 Retry-After 標頭
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000 // Retry-After 是秒,轉為毫秒
: calculateDelay(attempt);
console.warn(
`第 ${attempt + 1} 次請求失敗(${response.status}),` +
`${Math.round(delay / 1000)} 秒後重試...`
);
await sleep(delay);
} catch (error) {
// 網路錯誤(斷線、DNS 失敗等)
lastError = error;
if (attempt === maxRetries) {
throw new Error(`已達最大重試次數(${maxRetries}),最後錯誤:${error.message}`);
}
// 如果是我們主動拋出的「不重試」錯誤,直接往上拋
if (error.message.includes('不重試')) {
throw error;
}
const delay = calculateDelay(attempt);
console.warn(
`第 ${attempt + 1} 次請求發生網路錯誤,` +
`${Math.round(delay / 1000)} 秒後重試...`,
error.message
);
await sleep(delay);
}
}
throw lastError;
}
// 使用範例
async function fetchUserData(userId) {
try {
const response = await fetchWithBackoff(
`/api/users/${userId}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
{
maxRetries: 3,
baseDelay: 1000,
maxDelay: 15000,
retryOn: [500, 502, 503, 504, 429],
}
);
const data = await response.json();
console.log('使用者資料:', data);
return data;
} catch (error) {
console.error('取得使用者資料失敗:', error.message);
throw error;
}
}
// 呼叫
fetchUserData(123);
使用 Axios Retry 套件
如果你的專案已經在使用 Axios,可以透過 axios-retry 套件快速加上重試與退避功能,不需要自己實作:
首先,安裝 axios-retry:
npm install axios-retry
然後在你的程式碼中使用它:
import axios from 'axios';
import axiosRetry from 'axios-retry';
// 配置 Axios 實例
const instance = axios.create({
baseURL: 'https://api.example.com',
});
// 設定 axios-retry 的配置
axiosRetry(instance, {
retries: 3, // 重試次數
retryDelay: axiosRetry.exponentialDelay, // 退避策略,使用指數退避
retryCondition: (error) => {
// 在此處添加自定義的重試條件,例如只在特定錯誤碼時重試
return axiosRetry.isNetworkError(error) || (error.response && error.response.status === 500);
},
});
// 使用 Axios 實例進行請求
instance.get('/data')
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error.message);
});
axios-retry 進階設定選項
axios-retry 提供更多設定選項,讓你精確控制重試行為:
axiosRetry(instance, {
// 最多重試 4 次
retries: 4,
// 使用指數退避(內建選項)
// axiosRetry.exponentialDelay 會產生 100ms, 200ms, 400ms... 的延遲
retryDelay: (retryCount) => {
// 自定義延遲:加入 Jitter
const delay = axiosRetry.exponentialDelay(retryCount);
const jitter = Math.random() * 500; // 加入最多 500ms 的隨機抖動
return delay + jitter;
},
// 重試條件:哪些情況下要重試
retryCondition: (error) => {
// 網路錯誤(斷線)
if (axiosRetry.isNetworkError(error)) return true;
// 冪等請求(GET、HEAD)的超時也重試
if (axiosRetry.isIdempotentRequestError(error)) return true;
// 5xx 伺服器錯誤重試
if (error.response && error.response.status >= 500) return true;
// 429 Rate Limiting 重試
if (error.response && error.response.status === 429) return true;
return false;
},
// 在每次重試前執行的回呼(可用於記錄 log)
onRetry: (retryCount, error, requestConfig) => {
console.warn(`重試第 ${retryCount} 次:`, requestConfig.url, error.message);
},
});
實際應用場景
場景 1:API 請求重試
最典型的應用——前端呼叫第三方 API 或自家後端 API 發生暫時性錯誤:
// 取得商品詳情,最多重試 3 次
async function getProductDetails(productId) {
return fetchWithBackoff(
`https://api.shop.com/products/${productId}`,
{ headers: { Authorization: `Bearer ${token}` } },
{ maxRetries: 3, baseDelay: 500 }
);
}
場景 2:資料庫連線重試
在 Node.js 後端啟動時,資料庫可能尚未就緒:
async function connectWithBackoff(dbClient, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
await dbClient.connect();
console.log('資料庫連線成功');
return;
} catch (error) {
const delay = Math.min(1000 * Math.pow(2, i), 30000);
console.warn(`資料庫連線失敗,${delay / 1000} 秒後重試...`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('無法連線到資料庫');
}
場景 3:第三方服務整合
金流、簡訊、Email 服務商的 API 偶爾會有短暫不穩定:
// 送出付款請求,加入重試保護
async function processPayment(paymentData) {
return fetchWithBackoff(
'https://payment-gateway.com/api/charge',
{
method: 'POST',
body: JSON.stringify(paymentData),
headers: { 'Content-Type': 'application/json' },
},
{
maxRetries: 2, // 金流敏感,重試次數保守一些
baseDelay: 2000,
retryOn: [503, 504], // 只在明確的伺服器故障時重試,不包含 500(避免重複扣款)
}
);
}
場景 4:處理 429 Rate Limiting
許多 API(OpenAI、GitHub、Stripe)有請求速率限制,遇到 429 時應該按照 Retry-After 標頭等待:
// fetchWithBackoff 已內建處理 Retry-After 標頭(見上方原生實作)
// 當伺服器回傳:
// HTTP/1.1 429 Too Many Requests
// Retry-After: 60
// 函式會自動等待 60 秒後重試,而不是使用指數退避
async function callOpenAI(prompt) {
return fetchWithBackoff(
'https://api.openai.com/v1/chat/completions',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4', messages: [{ role: 'user', content: prompt }] }),
},
{ maxRetries: 3, retryOn: [429, 500, 503] }
);
}
常見問題(FAQ)
Q1:最大延遲(Max Delay)應該設多少?
沒有絕對的標準,但以下是常見的考量:
- 使用者面向的請求(互動型):最大延遲設 5-15 秒。超過 15 秒使用者通常會直接放棄或刷新頁面,繼續重試意義不大。
- 背景任務(批次處理、排程):最大延遲可以設 30-60 秒,甚至更長。因為不影響使用者體驗,等待時間長一點反而能讓伺服器更好地恢復。
- 關鍵服務(金流、認證):最大延遲設 10-30 秒,同時搭配較少的重試次數(2-3 次)。
Q2:哪些 HTTP 狀態碼應該重試?哪些不該?
| HTTP 狀態碼 | 是否重試 | 原因 |
|---|---|---|
| 5xx(500, 502, 503, 504) | 是 | 伺服器端的暫時性錯誤,重試有意義 |
| 429 Too Many Requests | 是 | Rate Limiting,等待後重試。應優先讀取 Retry-After 標頭 |
| 408 Request Timeout | 視情況 | 網路超時,重試可能有效 |
| 4xx(400, 401, 403, 404) | 否 | 客戶端錯誤,重試不會改變結果(請求本身有問題) |
| 401 Unauthorized | 否(先重新認證) | 應先刷新 Token 再重試,而不是直接重試 |
原則:5xx 和 429 重試;4xx 不重試(先修正請求本身)。
Q3:最多應該重試幾次?
取決於場景:
- 一般 API 請求:3 次是最常見的選擇,搭配指數退避總共等待約 7-15 秒。
- 關鍵業務操作(訂單、付款):2-3 次,且要注意冪等性(idempotency)——確保重試不會造成重複操作(例如重複扣款)。
- 非關鍵背景同步:5-8 次,但每次等待要設更長的間隔。
- 網路連線重試(啟動時連 DB):5-10 次,搭配指數退避。
超過 5 次重試在大多數情況下意義不大——如果服務真的掛掉超過幾分鐘,應該觸發告警並回報使用者,而不是無限重試。
了解退避策略後,搭配前端輪詢技術可以實作出更可靠的資料同步機制。請參考:前端 Polling 輪詢教學