限流后,你能够经过指数退避来重试

1、背景

本文同步发表于 Prodesire 公众号,和 Prodesire 博客
最近作云服务 API 测试项目的过程当中,发现某些时候会大批量调用 API,从而致使限流的报错。在遇到这种报错时,传统的重试策略是每隔一段时间重试一次。但因为是固定的时间重试一次,重试时又会有大量的请求在同一时刻涌入,会不断地形成限流。html

这让我回想起两年前在查阅Celery Task 文档的时候发现能够为任务设置 retry_backoff 的经历,它让任务在失败时以 指数退避 的方式进行重试。那么指数退避到底是什么样的呢?python

2、指数退避

根据 wiki 上对 Exponential backoff 的说明,指数退避是一种经过反馈,成倍地下降某个过程的速率,以逐渐找到合适速率的算法。git

在以太网中,该算法一般用于冲突后的调度重传。根据时隙和重传尝试次数来决定延迟重传。github

c 次碰撞后(好比请求失败),会选择 0 和 $2^c-1$ 之间的随机值做为时隙的数量。算法

  • 对于第 1 次碰撞来讲,每一个发送者将会等待 0 或 1 个时隙进行发送。
  • 而在第 2 次碰撞后,发送者将会等待 0 到 3( 由 $2^2-1$ 计算获得)个时隙进行发送。
  • 而在第 3 次碰撞后,发送者将会等待 0 到 7( 由 $2^3-1$ 计算获得)个时隙进行发送。
  • 以此类推……

随着重传次数的增长,延迟的程度也会指数增加。编程

说的通俗点,每次重试的时间间隔都是上一次的两倍。segmentfault

3、指数退避的指望值

考虑到退避时间的均匀分布,退避时间的数学指望是全部可能性的平均值。也就是说,在 c 次冲突以后,退避时隙数量在 [0,1,...,N] 中,其中 $N=2^c-1$ ,则退避时间的数学指望(以时隙为单位)是dom

$$E(c)=\frac{1}{N+1}\sum_{i=0}^{N}{i}=\frac{1}{N+1}\frac{N(N+1)}{2}=\frac{N}{2}=\frac{2^c-1}{2}$$socket

那么对于前面讲到的例子来讲:ide

  • 第 1 次碰撞后,退避时间指望为 $E(1)=\frac{2^1-1}{2}=0.5$
  • 第 2 次碰撞后,退避时间指望为 $E(2)=\frac{2^2-1}{2}=1.5$
  • 第 3 次碰撞后,退避时间指望为 $E(3)=\frac{2^3-1}{2}=3.5$

4、指数退避的应用

4.1 Celery 中的指数退避算法

来看下 celery/utils/time.py 中获取指数退避时间的函数:

def get_exponential_backoff_interval(
    factor,
    retries,
    maximum,
    full_jitter=False
):
    """Calculate the exponential backoff wait time."""
    # Will be zero if factor equals 0
    countdown = factor * (2 ** retries)
    # Full jitter according to
    # https://www.awsarchitectureblog.com/2015/03/backoff.html
    if full_jitter:
        countdown = random.randrange(countdown + 1)
    # Adjust according to maximum wait time and account for negative values.
    return max(0, min(maximum, countdown))

这里 factor 是退避系数,做用于总体的退避时间。而 retries 则对应于上文的 c(也就是碰撞次数)。核心内容 countdown = factor * (2 ** retries) 和上文提到的指数退避算法思路一致。
在此基础上,能够将 full_jitter 设置为 True,含义是对退避时间作一个“抖动”,以具备必定的随机性。最后呢,则是限定给定值不能超过最大值 maximum,以免无限长的等待时间。不过一旦取最大的退避时间,也就可能致使多个任务同时再次执行。更多见 Task.retry_jitter

4.2 《UNIX 环境高级编程》中的链接示例

在 《UNIX 环境高级编程》(第 3 版)的 16.4 章节中,也有一个使用指数退避来创建链接的示例:

#include "apue.h"
#include <sys/socket.h>

#define MAXSLEEP 128

int connect_retry(int domain, int type, int protocol,
                  const struct sockaddr *addr, socklen_t alen)
{
    int numsec, fd;

    /*
    * 使用指数退避尝试链接
    */
    for (numsec = 1; numsec < MAXSLEEP; numsec <<= 1)
    {
        if (fd = socket(domain, type, protocol) < 0)
            return (-1);
        if (connect(fd, addr, alen) == 0)
        {
            /*
            * 链接接受
            */
            return (fd);
        }
        close(fd);

        /*
        * 延迟后重试
        */
        if (numsec <= MAXSLEEP / 2)
            sleep(numsec);
    }
    return (-1);
}

若是链接失败,进程会休眠一小段时间(numsec),而后进入下次循环再次尝试。每次循环休眠时间是上一次的 2 倍,直到最大延迟 1 分多钟,以后便再也不重试。

总结

回到开头的问题,在遇到限流错误的时候,经过指数退避算法进行重试,咱们能够最大程度地避免再次限流。相比于固定时间重试,指数退避加入了时间放大性和随机性,从而变得更加“智能”。至此,咱们不再用担忧限流让整个测试程序运行中断了~

相关文章
相关标签/搜索