導入
外部API叩く系のコード、テストで通ってもスケーリングしたら死ぬことがあります
単純に一回に大量に叩いたり、そうでなくてもエラーから復旧したりした時に全員が同期して叩くみたいなことがあると、それはもはやDDoS攻撃だからです
それを解決するのが指数バックオフです
ミスったらいい感じに待ってやり直すというアルゴリズムです
その待つ時間まで一緒だと同期攻撃が繰り返されるだけなので乱数でずらします それがジッターです
GCPのCloudStorageのドキュメントとかでも自分で実装しろと書いてあります 悲しい
本質
このアルゴリズムの本質は待ち時間です
expBackoff.ts
const MIN_OFFSET_MSEC = 2000
await sleep( (2 ** retries) * MIN_OFFSET_MSEC * ( 1 + Math.random() ) )
今回は1回目は2〜4秒、2回目は4〜8秒、3回目は8〜16秒....の中で一様分布にしています
冪乗具合やrandom具合はお好みでアレンジしてください
コード
関数と最大リトライ数を引数に、指数バックオフを実行する関数です
expBackoff.ts
const MIN_OFFSET_MSEC = 2000 // 最初は2秒からスタート、それ以降は倍倍で増える
export const expBackOff = async (func: Function, MAX_RETRIES: number) => {
let retries = 0 // やり直した回数
let retry = true // やり直すかフラグ
let res
while(retry && retries < MAX_RETRIES){
await sleep( (2 ** retries) * MIN_OFFSET_MSEC * ( 1 + Math.random() ) ) // 本質
try {
res = await func() // undefinedは返さないで
retry = false
}
catch (e) {
console.log("retryCount: ", retries + 1)
console.log("expbackoff ERROR: ", e)
}
retries += 1
}
if(res == undefined) {
throw new Error("Expbackoff failed. retryCount: " + retries)
}
return res
}
async function sleep(msec: number) {
return new Promise(function(resolve) {
setTimeout(function() {resolve()}, msec);
})
}
使い方
index.ts
const fetch = require('node-fetch');
const fetchFunc = async () => { // 指数バックオフさせたい関数を作る
return await fetch(URL, options).then((response: any) => { // ちゃんとパースしてエラーに昇華させる
if(isYokunai(response)){ // レスポンスがよくなかった時
throw new Error(response)
} else {
return response
}
})
// return await someGoodApi() // ちゃんとエラーハンドリングされるならこれだけでもよい
}
const response = await expBackOff(fetchFunc, 5).then((res: any) => { // 5回やり直す
console.log("RESULT: ", res)
return res
}).catch((e) => {
console.log("ERROR: ", e)
return new Error(e) // 好きにハンドリングしてください
})
まとめ
良いスケーリングライフを!