Taming the Elephants -- New TCP Slow Start


这篇文章给出了对于老古董技术Slow Start的诸多现有性能问题评测分析,
并最后提出一种叫Hybird Slow Start (Hystart)的技术来尝试解决分析的一些问题。
这篇文章是理解最新的slow start技术的关键文章。


现有Slow-Start的性能问题

长肥网络[1]中的TCP性能问题是现代TCP设计的一个重要领域。这不,连那么naive的slow start都在长肥网络中遇到的诸多性能问题。

a. 标准的Slow Start算法可能引起严重的网络丢包
在ssthreshold值设置的过高时,慢启动一段时间后,cwnd的指数增长会显得过快。
有可能在上一个RTT,cwnd刚好等于BDP;下一个RTT,cwnd就等于2BDP了。
这样就可能导致多出的一个BDP的数据包被丢弃。这类丢包属于burst packet losses

b. Slow start可能导致系统负载过高,尤其是在使用SACK机制时
关于SACK选项可能引起CPU utilization过高的问题已经有了普遍的认识[2]
这篇论文给出的一个数据倒是令我比较感兴趣:

The problems consistently occur in all three dominant operating systems,
Linux, Windows XP and FreeBSD during more than 40% of slow start runs
in large BDP networks.  

解决Slow Start问题两个明显的思路:

1. 继续优化SACK的处理机制,提高遍历重传队列的效率
    -- 这也是目前较热的一个研究和工程趋势
2. 解决slow start可能带来的burst losses,进而也能缓解SACK性能问题 
    -- 这是这篇论文所主要关心的思路

吐个小槽:这篇论文的数据来看,Window XP被黑的不轻啊,╮(╯▽╰)╭


Hybird-Slow-Start

本文提出一种叫做Hybird Slow Start的算法来解决上述问题,论文中有不少精妙的
理论分析,这里就不再赘述。

主要想解释一下packet train是个什么样的概念。
举个栗子,下图是我理解的一个packet train的样子,[Packet1]这些是表示一个具体
的数据包,’-‘用于表示两个数据包之间的发送时间间隔。
那么这样一个packet train能用来干嘛呢?计算带宽利用率!
具体来说,图中四个数据包的数据总量可以求和得到,然后总的时间间隔也可以得到
(timestamp4 - timestamp1)。

[Packet1] -- [Packet2] ------ [Packet3] ---- [Packet4]

那这篇论文就利用的类似的思路,通过ACK train信息判断一个Safe Exit Point
for Slow Start. 同时为了避免计算带宽,通过一个巧妙的转换(具体建议看论文),只要判断时间差
是否超过Forward one-way delay(Dmin)来决定是否退出慢启动。

总的来说,Hystart算法可以总结如下

Hystart算法通过以下两个技术来判断是否应该退出慢启动
1. 一个窗口内的数据包的总传输间隔是否超过Dmin
2. 数据包的RTT sample是否出现较大幅度的增长

论文给出的伪代码如下图所示


内核源码分析

从上面的hystart算法伪代码可以看出,hystart算法主要包括两个方面:
a. 在每个RTT区间开始时,重置hystart算法相关变量
b. 对于每个收到的ACK,根据ack train算法和RTT波动来判断是否应该退出slow start

在Linux 3.10内核中,hybird slow start算法被集成在了默认的CUBIC算法中。
在tcp_cubic.c文件中有两个函数分别对应以上两个步骤,
分别是bictcp_hystart_reset()和hystart_update()。
这两个函数几乎是严格按照上述伪代码来实现的,所以也就没有接着展开解释的必要。
下面主要从整体的处理逻辑方面来分析源码

// cubic算法的关键数据结构  
static struct tcp_congestion_ops cubictcp = {
    .init           = bictcp_init,              // 在TCP流建立之初调用
    .ssthresh       = bictcp_recalc_ssthresh,
    .cong_avoid     = bictcp_cong_avoid,
    .set_state      = bictcp_state,
    .undo_cwnd      = bictcp_undo_cwnd,
    .pkts_acked     = bictcp_acked,             // 每次收到ACK数据包后调用
    .owner          = THIS_MODULE,
    .name           = "cubic",
};

上面是cubic算法的关键结构体的初始化部分,bictcp_init()负责TCP流建立时的初始化动作,
bictcp_acked()负责在每次收到ACK包后,更新相关信息。所有hystart算法上面的两个
函数也分别是在cubic的这两个函数中。下面依次看cubic中这两个函数的实现

static void bictcp_init(struct sock *sk)
{
    bictcp_reset(inet_csk_ca(sk));

    if (hystart)                    // 如果开启(默认开启),则进行hystart相关变量的初始化
        bictcp_hystart_reset(sk);

    if (!hystart && initial_ssthresh)
        tcp_sk(sk)->snd_ssthresh = initial_ssthresh;
}

static void bictcp_acked(struct sock *sk, u32 cnt, s32 rtt_us)
{
    ...
    /* first time call or link delay decreases */
    if (ca->delay_min == 0 || ca->delay_min > delay)
        ca->delay_min = delay;

    /* hystart triggers when cwnd is larger than some threshold */
    if (hystart && tp->snd_cwnd <= tp->snd_ssthresh && tp->snd_cwnd >= hystart_low_window)
        hystart_update(sk, delay);
}

参考资料

Taming the Elephant: New TCP Slow Start
用OProfile彻底了解性能