如何通过重试来改进你的后端API call
无论你是从Node.js还是浏览器调用一个API Call,连接失败总是会发生。有些request的失败是调用相关的错误,比如客户端发送了一个错误的数据。另外一些则是连接的问题,比如连接到服务器的问题,或者是这之间的某一个节点出现了问题。虽然API和web服务检测可以看到这些问题,但是一个更好的方案也许可以处理这个问题。
解决这个问题,你可以在你的HTPP调用中加入一个重试的机制。这可以让你的API调用成功。有些库,比如got,就支持失败的重试,而另外一些库,比如axios,则需要一个独立的插件。但是假如你的库不支持这个,那么可以参考这篇文章。我们将基于返回的status来决定如何重试一个请求。
重试的基础
决定何时需要重试一个request,我们需要知道正在找寻什么。有很多HTTP status code可以用来检查。这样你的重试机制就根据不同的错误来进行,比如一个网络错误就是很好的重试情况,而别的,比如404错误,则不是一个很好的重试情况。我们的例子中,将会使用408,500,502,503,504,522和524.你也可以检查429,这个错误表示你可能调用了太多request。
下一个问题就是重试的频率。我们从一个小的延迟开始,然后每次都加上额外的延时。我们称之为“退避”。每次重试之间的时间都会不断增加。最终,我们还需要确定在多少次重试之后放弃。
下面是有一个伪码的逻辑
- If total attempts > attempts, continue
- if status code type matches, continue
- if (now – delay) > last attempt, try request
- else, return to the start
我们还会可以检测error code,并且限制在某些特定的方法中使用重试。比如,对POST不进行重试是一个很好地想法,这样可以保证不会重复创建entity。
递归请求的结构
要进行重试,我们就需要在请求fail的时候再次调用请求。这其实就是一个递归调用,所为递归调用就是在函数中调用自身。

比如,假如我们需要无限循环调用一个请求,代码可能像下面这样:
function myRequest(url, options = {}) { return requests(url, options, response => { if (response.ok) { return response } else { return myRequest(url, options) } }) }
从这个代码中可以看出,在else的情况下,会再次返回myRequest函数。因为现在的HTTP请求都是基于promis来实现的,所以我们能返回这个result。这对调用的用户的来说,其实是透明的,看起来就像是一个普通的调用。例如:
myRequest("https://example.com").then(console.log(response))
下面让我们来看看javascript中的重试是如何实现的。
将retry加入到Fetch中
我们首先从浏览器中的Fetch API来开始。这个fetch的实现和上面的递归实现类似,下面我们来使用status code的检测来实现同样的例子:
function fetchRetry(url, options) { // Return a fetch request return fetch(url, options).then(res => { // check if successful. If so, return the response transformed to json if (res.ok) return res.json() // else, return a call to fetchRetry return fetchRetry(url, options) }) }
这个实现是当fail的时候会无限循环调用。下面我们来加一个max number的检测:
function fetchRetry(url, options = {}, retries = 3) { return fetch(url, options) .then(res => { if (res.ok) return res.json() if (retries > 0) { return fetchRetry(url, options, retries - 1) } else { throw new Error(res) } }) .catch(console.error) }
这个函数我们加了一个retries的限制,默认值是3,也就是在retry的时候会先看看已经试过了多少次,假如超过了3次就不再重试了。这样就不用担心一直重试了,假如三次还没有成功,则会throw一个Error。
客户端可以使用下面代码来调用这个函数:
fetchRetry("https://status-codes.glitch.me/status/400") .then(console.log) .catch(console.error)
假如你查看network的traffice,你会发现有四次调用,开始的第一次和后面的三次重试。下面我们再加一个根据status code来决定要不要重试:
function fetchRetry(url, options = {}, retries = 3) { const retryCodes = [408, 500, 502, 503, 504, 522, 524] return fetch(url, options) .then(res => { if (res.ok) return res.json() if (retries > 0 && retryCodes.includes(res.status)) { return fetchRetry(url, options, retries - 1) } else { throw new Error(res) } }) .catch(console.error) }
这个代码中,首先加入了一个retryCodes的数组,这个数组中定义了我们需要retry的status code。你可以把这个设置放到一个config文件中,这样就可以让这段代码更加通用。在调用之前会先检测status code是否在这个array里面,要是在我们才会进行重试。
还有最后一个功能需要增加,就是我们之前提到的“退避”策略:
function fetchRetry(url, options = {}, retries = 3, backoff = 300) { /* 1 */ const retryCodes = [408, 500, 502, 503, 504, 522, 524] return fetch(url, options) .then(res => { if (res.ok) return res.json() if (retries > 0 && retryCodes.includes(res.status)) { setTimeout(() => { /* 2 */ return fetchRetry(url, options, retries - 1, backoff * 2) /* 3 */ }, backoff) /* 2 */ } else { throw new Error(res) } }) .catch(console.error) }
这里我们使用了setTimeout来延迟调用重试,每次调用这个timeout都会变成上一次的两倍。
现在,假如我们再调用fetchRetry,第一个call会立即执行,然后第一次重试会等待300ms,然后600ms,最后是900ms的间隔。
进一步的配置和更好的选择
上面的方案对一次性的调用或者小的app很不错,但是当应用变大之后,我们还有更好的选择。创建一个可配置的类可以为每一个API的集成提供更好的控制。你也可以把这个逻辑应用到断路器或者其他补救方法。
另外一个选项就是用一个工具来观察和处理API调用的问题。Bearer就是其中一个不错的选择,他会处理所有的这些东西。
更多:在Node的本地http模块中加入retry
上面的fetch实现对浏览器是可以的,但是Node.js怎么处理呢?你可以使用一个和fetch类似的库就像node-fetch。我们来看看如何把同样的内容应用到Node.js的本地http模块。
我们使用http.get来简化这个问题。重试的逻辑还是一样的。
在我们开始之前,我们需要把http.get从基于event改变到基于promise,这样我们就可以使用和fetch同样的方法来实现。假如你不太熟悉promise,他是一个目前流行的async的实现。每次你使用.then或者async/await,你都是真正使用的promise。不过假如你想理解本文,那么你只要知道promise可以resolve或者reject,他们分别表示成功或者失败。下面是没有retry逻辑的代码:
let https = require("https") https.get(url, res => { let data = "" let { statusCode } = res if (statusCode < 200 || statusCode > 299) { throw new Error(res) } else { res.on("data", d => { data += d }) res.end("end", () => { console.log(data) }) } })
总的来说,这个请求中有一个请求,假如statusCode不是在一个成功范围内,将会throw一个错误。否则他就会创建一个response并且把log到console中。为了简化,我们去除一些错误的处理:
function retryGet(url) { return new Promise((resolve, reject) => { https.get(url, res => { let data = "" const { statusCode } = res if (statusCode < 200 || statusCode > 299) { reject(Error(res)) } else { res.on("data", d => { data += d }) res.on("end", () => { resolve(data) }) } }) }) }
这里关键的地方是:
- 返回一个新的Promise
- 在成功的情况下resolve
- 有错误的时候,reject
我们可以通过调用下面代码来测试:
retryGet("https://status-codes.glitch.me/status/500").then(console.log).catch(console.error)
任何在不在200范围内的status code都会被catch。
下面,我们把所有之前的逻辑也加到这个函数中来:
function retryGet(url, retries = 3, backoff = 300) { /* 1 */ const retryCodes = [408, 500, 502, 503, 504, 522, 524] /* 2 */ return new Promise((resolve, reject) => { https.get(url, res => { let data = "" const { statusCode } = res if (statusCode < 200 || statusCode > 299) { if (retries > 0 && retryCodes.includes(statusCode)) { /* 3 */ setTimeout(() => { return retryGet(url, retries - 1, backoff * 2) }, backoff) } else { reject(Error(res)) } } else { res.on("data", d => { data += d }) res.on("end", () => { resolve(data) }) } }) }) }
这个和之前的fetch的例子类似,首先,有一个参数1),然后定义retryCodes(2),最后做了重试的逻辑并且返回retryGet。这个可以保证当用户调用retryGet()的时候,能够得到他们想要的promise。
总结
本文介绍了如何对API的调用的重试,希望大家能够喜欢。
参考文章:https://hackernoon.com/how-to-improve-your-backend-by-adding-retries-to-your-api-calls-83r3udx
Recent Comments