delay_ack引发bbr速率掉底血案(上)

“时延低的传输速率一定高于时延高的网络”,“hash表的查询操作只需要O(1)的时间”,“bbr拥塞算法优于cubic算法”。“大部分场景”都正确并不适用所有场景,但是平时大家总是将这些“大部分正常”当作永远正确来处理。然而小概率并不是不发生,就程序而言,我们写的程序逻辑在大部分场景都是正常,但是可能某些场景下就会出现逻辑异常,也就是我们常说的程序bug。

今天我要说的delay_ack导致的bbr速率异常问题也是一种“小部分场景”异常的问题。在推动某头部直播客户上云的过程中,客户投诉卡顿率飙升,主要表现是出现速率突然掉底的问题,比如下图是随时间吞吐量的变化图,4分钟的时间内出现三次速率突然抖降的现象。

 

整个传输链路较多,经过逐级定位发现住传输卡在流,推流和解码部分使用两台虚拟机来进行功能隔离,也就是两台虚拟机之间的传输问题。

我司机房的两台虚拟机之间的RTT时间非常的短,实测在60~120us。而友商A机房的数据对比,普遍在200us~300us,是我们的两倍。另一家厂商B,时延在300us~2ms之前。据客户反馈,友商A也会发现这种码率掉底的问题,只是触发概率很低,而友商B几乎不发生。看着是“时延越低,越容易触发”的异常,于是使用tc命令分别添加了200us和1ms的时延进行测试,200us的时延场景下推流4分钟,偶尔发生一次掉底现象。而时延增加1ms后,基本不发生码率掉底问题。

看似通过使用tc命令增加时延就能解决这个问题,但解决问题需要“知其然知其所以然”的态度,技术研究不能总想着找到规避方案,更需要发现其深层本质原因。

bbr拥塞算法正常情况下,只有两种情况下会主动抑制发送的行为,一种是检测到有规律的丢包进入到long-term状态,另一种是长时间min_rtt不更新进入到probe_rtt状态。但显然我们这个场景并不符合这两种case,long-term需要丢包,虚拟机内部传输基本没有丢包发生。probe_rtt状态的退出条件是一轮时间&200ms的时间,而我们这个场景会持续好几秒的时长。于是只能使用kprobe探测bbr的运行状态,bbr_main函数是bbr的主函数,主要输入参数是通过struct rate_sample *rs指针进行传入,rate_sample结构体如下图所示,我们打印出几个关键信息,比如带宽bw,lt_use_bw,is_app_limited,interval_us等等,当然为了方便对照wireshark对应的位置,还打印了tp->snd_una变量。

struct rate_sample {
    u64  prior_mstamp; /* starting timestamp for interval */
    u32  prior_delivered;   /* tp->delivered at "prior_mstamp" */
    s32  delivered;     /* number of packets delivered over interval */
    long interval_us;   /* time for tp->delivered to incr "delivered" */
    long rtt_us;        /* RTT of last (S)ACKed packet (or -1) */
    int  losses;        /* number of packets marked lost upon ACK */
    u32  acked_sacked;  /* number of packets newly (S)ACKed upon ACK */
    u32  prior_in_flight;   /* in flight before this ACK */
    bool is_app_limited;    /* is sample from packet with bubble in pipe? */
    bool is_retrans;    /* is sample from retransmission? */
};

根据wireshark找到码率下降的时间点,此时的snd_una的值为3962813608,找到kprobe打印输出的日志对应的部分,发现了该时刻发生了带宽陡降的问题,bbr带宽被错误的更新了。而bbr带宽的更新的条件是当前的带宽采样值非app_limited,或者当前的采集值大于当前带宽,才会调用minmax_running_max去尝试更新带宽,而如果最大带宽已经很久没有更新则会被替换。

 

static void bbr_update_bw(struct sock *sk, const struct rate_sample *rs)
{   
 ...
    /* If this sample is application-limited, it is likely to have a very
     * low delivered count that represents application behavior rather than
     * the available network rate. Such a sample could drag down estimated
     * bw, causing needless slow-down. Thus, to continue to send at the
     * last measured network rate, we filter out app-limited samples unless
     * they describe the path bw at least as well as our bw model.
     *
     * So the goal during app-limited phase is to proceed with the best
     * network rate no matter how long. We automatically leave this
     * phase when app writes faster than the network can deliver :)
     */
    if (!rs->is_app_limited || bw >= bbr_max_bw(sk)) {
        /* Incorporate new sample into our max bw filter. */
        minmax_running_max(&bbr->bw, bbr_bw_rtts, bbr->rtt_cnt, bw);
    }
}

直觉问题就变是“为何这么小的带宽采用值是非app_limited”。那就要看什么时候才会设置这个rs->is_app_limited,在接收ack会在tcp_rate_skb_delivered函数中设置,也就是发送的时候如果是app_limited型,就设置rs->is_app_limited。

void tcp_rate_skb_delivered(struct sock *sk, struct sk_buff *skb, struct rate_sample *rs)
{      ...
if (!rs->prior_delivered ||
        after(scb->tx.delivered, rs->prior_delivered)) {
        rs->prior_delivered  = scb->tx.delivered;
        rs->prior_mstamp     = scb->tx.delivered_mstamp;
        rs->is_app_limited   = scb->tx.is_app_limited;
        rs->is_retrans       = scb->sacked & TCPCB_RETRANS;
       ...
}

发送数据包时,在tcp_rate_skb_sent中根据tp->app_limited来进行设置,tp->app_limited会在tcp_rate_check_app_limited函数中设置,并且只在应用层写数据中tcp_sendmsg_locked/tcp_sendpage_locked调用。具体细节不在详述,主要大意是应用层写数据的时候,发现当前已经无数据可以发送并且还有足够的拥塞窗口未使用时就会认为是app_limited,最常见场景就是所有数据都已经被确认,重新开始发送的第一轮所有的包都会被认为是app_limited类型。

void tcp_rate_skb_sent(struct sock *sk, struct sk_buff *skb)
{
    ...
    TCP_SKB_CB(skb)->tx.first_tx_mstamp = tp->first_tx_mstamp;
    TCP_SKB_CB(skb)->tx.delivered_mstamp    = tp->delivered_mstamp;
    TCP_SKB_CB(skb)->tx.delivered       = tp->delivered;
    TCP_SKB_CB(skb)->tx.is_app_limited  = tp->app_limited ? 1 : 0;
}
void tcp_rate_check_app_limited(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (/* We have less than one packet to send. */
        tp->write_seq - tp->snd_nxt < tp->mss_cache &&
        /* Nothing in sending host's qdisc queues or NIC tx queue. */
        sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1) &&
        /* We are not limited by CWND. */
        tcp_packets_in_flight(tp) < tp->snd_cwnd &&
        /* All lost packets have been retransmitted. */
        tp->lost_out <= tp->retrans_out)
        tp->app_limited =
            (tp->delivered + tcp_packets_in_flight(tp)) ? : 1;
}

而当设置完tp->app_limited后开始发送,有数据被确认,之后发送的数据数据包则不认为是app_limited。这也是为啥时延越小就越容易触发的原因,因为越小的时延,越容易出现发送后几个数据包的时候,已经有数据包被ack了,也就是已经开始算下一轮了。

void tcp_rate_gen(struct sock *sk, u32 delivered, u32 lost, bool is_sack_reneg, struct rate_sample *rs)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 snd_us, ack_us;

    /* Clear app limited if bubble is acked and gone. */
    if (tp->app_limited && after(tp->delivered, tp->app_limited))
        tp->app_limited = 0;
     ...
}

那是不是这个app_limited判定不合理呢?我个人觉得不是,app_limited主要衡量发送该某数据包的时刻,网络中inflight是否充足的一个概念。如果发送这个数据包时,inflight充足,那这个就是一个充足的带宽采样,至于是否发送delay_ack导致计算出来带宽过小,不在他的考虑范围。

interval_us是发送间隔send phase和接收间隔ack phase二者的最大值,正常的interval_us只有150us左右,而异常点因为delay_ack存在,造成了rcv_interval变为40ms左右,从而计算出一个很小的带宽值。bbr对这种保守的方式解释是为了防止出现接收速率高于发送速率的情形。但是并未考虑到这种delay造成的接收速率远小于发送速率的问题。为什么没有考虑呢?主要是刚好delay_ack的采样值更新带宽的概率很低,因为bw是minmax结构体,采样获得一个局部最大bw可以保持8轮的时间,并且在8轮内更新第二大bw和第三大bw,如果没有出现app_limited类型采样值。也就是发生的条件是需要前面8轮都是app_limited类型,刚好第一个app_limited类型的采样发生了delay_ack。前面一条的条件就很难满足,但是在直播场景下就非常容易满足。

那如何解决呢?不想变更bbr代码可以通过关闭delay_ack来实现,此前我一直以为关闭delay_ack需要setsockopt不断设置TCP_QUICKACK,实际路由中的metirc中可以开启RTAX_QUICACK来进入quicack模式。如果想更改代码,可以考虑忽略delay_ack的采样值,社区之后的代码中有在rs中传递进一个is_ack_delayed变量标示,可以搜索commit 41c3d35 。

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值