编程小记
首发于编程小记
指数补偿(Exponential backoff)在网络请求中的应用

指数补偿(Exponential backoff)在网络请求中的应用

1 背景

在查阅Celery Task文档的时候发现可以为任务设置retry_backoff,以在任务失败时通过指数补偿算法进行重试。那么指数补偿究竟是什么样的呢?

2 指数补偿

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

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

c次碰撞后(比如请求失败),会选择0和2^{c}-1之间的随机值作为时隙的数量。

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

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

3 指数补偿的期望值

考虑到补偿时间的均匀分布,补偿时间的数学期望是所有可能性的平均值。也就是说,在c次冲突之后,补偿时隙数量在 [0,1,...,N] 中,其中 N=2^{c}-1 ,则补偿时间的数学期望(以时隙为单位)是

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}

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

  • 第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是1。而retries则对应上文的c(也就是碰撞次数)。若full_jitterTrue, 则和上文提到的指数补偿算法思路一致,不过对设置了补偿时间上限;若 full_jitterFalse,则不是随机选取,而是取最大的补偿时间,也就可能导致多个任务同时再次执行。更多见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分多钟,之后便不再重试。

编辑于 2018-06-05

文章被以下专栏收录