如何通过重试来改进你的后端API call

无论你是从Node.js还是浏览器调用一个API Call,连接失败总是会发生。有些request的失败是有效的,也许endpoint有问题,或者客户端发送了一个错误的数据。另外一些则是连接的问题,比如连接到服务器的问题,或者是这之间的某一个节点出现了问题。虽然API和web服务检测可以看到这些问题,但是一个更好的方案也许可以处理这个问题。

解决这个问题,你可以在你的HTPP调用中假如一个重试的机制。这可以让你的API调用成功。有些库,比如got,就支持失败的重试,而另外一些库,比如axios,则需要一个独立的插件。但是假如你的库不支持这个,那么可以参考这篇文章。我们将基于返回的status来决定如何重试一个请求。

重试的基础

决定何时需要重试一个request,我们需要知道正在找寻什么。有很多HTTP status code可以用来检查。这样你的重试机制就根据不同的错误来进行,比如一个网络错误就是很好的重试情况,而别的,比如404错误,则不是一个很好的重试情况。我们的例子中,将会使用408,500,502,503,504,522和524.你也可以检查429,这个错误表示你可能调用了太多request。

下一个问题就是重试的频率。我们从一个小的延迟开始,然后每次都加上额外的延时。我们称之为“退避”。每次重试之间的时间都会不断增加。最终,我们还需要确定在多少次重试之后放弃。

 下面是有一个伪码的逻辑

  1. If total attempts > attempts, continue
  2. if status code type matches, continue
  3. if (now – delay) > last attempt, try request
  4. 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)
        })
      }
    })
  })
}

这里关键的地方是:

  1. 返回一个新的Promise
  2. 在成功的情况下resolve
  3. 有错误的时候,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

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *