10

数据中心网络 BBR 不如 CUBIC ?

 3 years ago
source link: https://kernel.taobao.org/2019/11/bbr-vs-cubic-in-datacenter-network/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Nov 13, 2019 • 李靖轩

数据中心网络 BBR 不如 CUBIC ?

TCP 拥塞控制算法在网络中占据重要地位,在 BBR 算法出来之前,大部分现代操作系统的拥塞控制算法经过好几代的更新,最后大多都是采用 Cubic;而在 BBR 出现之后,由于它在长肥网络中优异的带宽的利用率,加上 Google 在 Youtube 的推广,大有替换 cubic 等传统 TCP 拥塞算法的趋势。在 Aliyun Linux2 上我们也把默认的拥塞控制算法从 cubic 改成了 bbr。

然而 RDS 的 Redis 遇到一个问题,他们将他们的 ECS 从 Aliyun Linux 升级到 Aliyun Linux2 上之后,发现性能反而变差了,而且差了近一倍。他们给出的测试场景很简单:

在两个 VM 中,分别跑 redis-server 和 memtier_benchmark,具体的命令如下

VM#1: redis-server --protected-mode no
VM#2: memtier_benchmark -s 192.168.124.100 -p 6379 -d 3 -n 10000000 -c 100 -t 4 --ratio=100:0

然后,观察benchmark 输出结果里的 ops/sec。结果如下: Aliyun Linux 1 (kernel 4.4.95-3) : 14W+ Aliyun Linux 2 (kernel 4.19.48-14): 8W+

复现环境:

于是我们在阿里云官网买了两台 ECS,server端 24个 core, client 端4个 core,自己搭建了一个测试环境,发现可以复现该问题。 server 端跑 redis-server,实际运行跑起来,发现 server 端其实只用到了一个core。 client 端跑 4个线程,100个连接。实际测试发现 client端跑4个线程,16个连接即可达到 OPS 的极限,瓶颈应该在 server端。 内网 IP 在同一个网段中,

server: 47.104.214.74
cmd: redis-server --protected-mode no

client:  118.190.53.2
cmd: memtier_benchmark -s 172.31.210.8 -p 6379 -d 3 -n 10000000 -c 100 -t 4 --ratio=100:0

进一步对比,发现问题出在拥塞控制算法上面, Aliyun Linux 1 使用的是 cubic 算法,而 Aliyun Linux 2 使用的是 BBR.

调整 memtier_benchmark 的连接数和线程数,测试 BBR 和 CUBIC 的情况:

undefinedundefinedundefined

从上面的测试可以看出:

  1. 在连接数不多的情况下,该测试 CUBIC 和 BBR 差别不大,但当连接数到8个之后,BBR 明显不如 CUIBC;
  2. 单个线程和多个线程情况基本上差不多;
  3. 实际发现原因是 cubic 在跑相同的 QPS 的情况下,达到 CPU 瓶颈的时连接数明显比 BBR 要高,相同的连接数的情况下,BBR 的性能显然不如 CUBIC 高。

进一步测试单线程情况下两者的 CPU 利用率。 undefined

对比上面的图可以看出,单个线程的情况下,相同的连接数:

  1. BBR 的 sys 占比明显比 CUBIC 要高,说明 BBR 需要处理的内核逻辑比 CUBIC 明显要多。而 sys 越高,同样的 CPU 利用率的情况下,应用真正能干的活就越少;
  2. 随着连接数的增加,SYS 占比越来越高;这个是符合预期的,连接数越多,内核需要处理的逻辑越多,cache miss也会越高;
  3. 在该场景下,cubic 的 user/sys 大概是 bbr 的 1.55~1.86 倍之间。

说明,BBR 在这种场景下相比 CUBIC 会占用更多的 CPU。 难道 BBR 被夸大了?


抓包发现,这个场景下,memtier_benchmark/redis 的流量模型就是一个 request depth > 1 的 request-response,request的大小大概是 44 字节,response 大概是5 字节。 既然是个网络问题,我们还是用标准网络工具来验证。我们用 netperf 测试一下,看看是否有相同的情况:

测试命令为:

taskset -c 3 netperf -t TCP_RR -l 50 -H 172.31.210.8 -- -r $req_size,$rsp_size

测试结果如下:

TCP_RR (单连接时延测试)

这里测试的 request size 和 response 保持一致,相当于相同 size 的 ping-pong。测试结果如下图: netperf TCP_RR CUBIC vs. BBR

TCP_STREAM (单连接吞吐测试)

这里的 send size 是 netperf 的 TCP_STREAM 模式下的 -m 参数,也就是指定 netperf 调用的 sendto() 里面buf 的 len,len越小,一次系统调用下去给内核的数据越少。

测试命令为:

taskset -c 3 netperf -t TCP_STREAM -l 500 -H 172.31.210.8 -- -m $send_size

netperf TCP_RR CUBIC vs. BBR

对比 netperf 的测试结果,可以明显看出:

  1. 无论是 TCP_RR 还是 TCP_STREAM,结果与前面的 redis 的 benchmark 类似。相同吞吐的情况下,BBR 的 sys 态 CPU 利用率高于 CUBIC,在某些场景先,差别非常明显(send_size==1024, CUBIC vs. BBR: 51.69 vs. 98.20)。

既然 BBR 相比 CUBIC 有这么大的区别,那我们就应该要搞清楚为啥会差这么大,这中间是不是有什么可以优化的地方? 既然 netperf 可以轻易复现,想要找出来原因应该也不是一件困难的事情。我们先 perf 抓一把 netperf 在 cubic 和 bbr 的对比情况,如下: perf cubic vs. bbr

一对比才发现 CUBIC 跟 BBR 的 CPU 占用率的区别根本就没有在 BBR 的逻辑上啊! 但是,perf 应该不会骗人,先重点先看看 BBR 中几个标红而 CUBIC 上不明显的函数:

ipt_do_table();
_raw_spin_unlock_irqrestore();
smp_apic_timer_interrupt();

我们再 perf script 找了下这三个函数调用栈都是谁,并对比 cubic 下的情况。发现一些问题:

  1. ipt_do_table() 这种函数在 cubic 和 bbr 下执行的次数都很多;不同点在于, bbr 的 netperf 的调用栈中,多了很多如下的调用栈,而 cubic 下却没有。
     netperf 13335 620971.431131:     250000 cpu-clock:
             ffffffff847d2c2d _raw_spin_unlock_irqrestore+0xd ([kernel.kallsyms])
             ffffffff840f3a4e __hrtimer_run_queues+0xde ([kernel.kallsyms])
             ffffffff840f3c3c hrtimer_run_softirq+0x7c ([kernel.kallsyms])
             ffffffff84a000d1 __softirqentry_text_start+0xd1 ([kernel.kallsyms])
             ffffffff84800d3a do_softirq_own_stack+0x2a ([kernel.kallsyms])
             ffffffff84084688 do_softirq+0x58 ([kernel.kallsyms])
             ffffffff840846f7 __local_bh_enable_ip+0x57 ([kernel.kallsyms])
             ffffffff8471fbbb ipt_do_table+0x33b ([kernel.kallsyms])
             ffffffff846bde8d nf_hook_slow+0x3d ([kernel.kallsyms])
             ffffffff846d21bd __ip_local_out+0xcd ([kernel.kallsyms])
             ffffffff846d2237 ip_local_out+0x17 ([kernel.kallsyms])
             ffffffff846ec103 __tcp_transmit_skb+0x583 ([kernel.kallsyms])
             ffffffff846ec7f3 tcp_write_xmit+0x243 ([kernel.kallsyms])
             ffffffff846ed4f1 __tcp_push_pending_frames+0x31 ([kernel.kallsyms])
             ffffffff846df0ae tcp_sendmsg_locked+0x9be ([kernel.kallsyms])
             ffffffff846df467 tcp_sendmsg+0x27 ([kernel.kallsyms])
             ffffffff8464dab6 sock_sendmsg+0x36 ([kernel.kallsyms])
             ffffffff8464f1bc __sys_sendto+0xdc ([kernel.kallsyms])
             ffffffff8464f264 __x64_sys_sendto+0x24 ([kernel.kallsyms])
             ffffffff8400201b do_syscall_64+0x5b ([kernel.kallsyms])
             ffffffff84800088 entry_SYSCALL_64_after_hwframe+0x44 ([kernel.kallsyms])
                 7f84df63ae6d __libc_send+0x1d (/usr/lib64/libc-2.17.so)
    
  2. _raw_spin_unlock_irqrestore() 的调用者基本上都跟 hrtimer 有关;
  3. 而 cubic 中,就没有出现过 hrtimer;

以上几点都指向了 hrtimer。于是去找 BBR 的代码,发现原来人家代码一开始的注释中就写了个大写的 NOTE 😅:

  *
  * NOTE: BBR might be used with the fq qdisc ("man tc-fq") with pacing enabled,
  * otherwise TCP stack falls back to an internal pacing using one high
  * resolution timer per TCP socket and may use more resources.
  */

再盯着这个 pacing 看,发现: BBR 由于依赖于 pacing,所以 bbr 在 bbr_init() 里面,就把 sk->sk_pacing_status 初始化成了 SK_PACING_NEEDED;而一旦 sk->sk_pacing_status == SK_PACING_NEEDED,tcp 会尝试通过一个 hrtimer 来实现该功能,从而引入了大量的 hrtimer 的逻辑,占用了 CPU。

__tcp_transmit_skb() (发包的关键路径) ,有这么个判断:

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                              int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
	...// 省略若干行

         if (skb->len != tcp_header_size) {
                 tcp_event_data_sent(tp, sk);
                 tp->data_segs_out += tcp_skb_pcount(skb);
                 tp->bytes_sent += skb->len - tcp_header_size;
                 tcp_internal_pacing(sk, skb);          // tcp 层的 pacing
         }
	  
    ... // 省略若干行
}

可以看出,当当前的 skb 带有数据(不是一个纯 ack 包)时,就会调用 tcp_internal_pacing():

 /* BBR congestion control needs pacing.
  * Same remark for SO_MAX_PACING_RATE.
  * sch_fq packet scheduler is efficiently handling pacing,
  * but is not always installed/used.
  * Return true if TCP stack should pace packets itself.
  */
 static inline bool tcp_needs_internal_pacing(const struct sock *sk)
 {
         return smp_load_acquire(&sk->sk_pacing_status) == SK_PACING_NEEDED;
 }

static void tcp_internal_pacing(struct sock *sk, const struct sk_buff *skb)
{
        u64 len_ns;
        u32 rate;

        if (!tcp_needs_internal_pacing(sk))
                return;
        rate = sk->sk_pacing_rate;
        if (!rate || rate == ~0U)
                return;

        len_ns = (u64)skb->len * NSEC_PER_SEC;
        do_div(len_ns, rate);
        hrtimer_start(&tcp_sk(sk)->pacing_timer,
                      ktime_add_ns(ktime_get(), len_ns),
                      HRTIMER_MODE_ABS_PINNED_SOFT);
        sock_hold(sk);
}

其中 tcp_needs_internal_pacing() 就是判断 sk->sk_pacing_status 是否等于 SK_PACING_NEEDED。 如果相等,则就要走下面的 hrtimer_start() 的逻辑,起这个 pacing 的 hrtimer。这样就能解释,为什么 bbr 的perf 中,抓到那么多的 hrtimer 相关的函数,而 cubic 里面压根就没有了。 因为 cubic 里面这个 sk->sk_pacing_status == SK_PACING_NONE,而 bbr 中 sk->sk_pacing_status == SK_PACING_NEEDED。

TCP pacing

再接着看,BBR 依赖于 pacing,原来原始版本的 BBR 就是直接依赖于 tc-fq 的,之后,Eric D 老哥认为 BBR 这个拥塞控制算法不能跟流量调度算法绑定在一起,所以他搞了个 patch,在 TCP 内部自己实现了一个 pacing。 见:218af599 tcp: internal implementation for pacing 而正是这个 patch 引起了我们这问题。

既然知道了 bbr 多出来的那么多的 CPU 是由于 sk->sk_pacing_status 引发的。而这个 sk_pacing_status 还可以复用 tc-fq 中的 pacing (SK_PACING_FQ),那我们打开 tc-fq 的 pacing 应该就可以避免掉这个 hrtimer 的时钟中断的开销了。毕竟人家 BBR 的注释 NOTE 中也是这么说的嘛。

测试一把:

# tc qdisc add dev eth0 root fq

打开 tc-fq 后,重新跑一把 netperf 的 TCP_STREAM 测试,结果如下:

undefined

send_size == 512bytes 的时候, BBR 仍然比 CUBIC 要高一些 (69% vs 61%),但总算差别没那么明显了。

在抓一把 perf 对比:

undefined

嗯… 前面的现在看起来总算差不多了。

我们再来对比下业务反馈 redis 性能差的问题: undefined2thread_memtier_bench4thread_memtier_bench

BBR+FQ 还是比 CUBIC 稍微差一点点,但差别很小了。

所以,总结下来,这个问题可以这样描述: 在内网低时延、高吞吐的环境下:

  1. 默认不使用 tc-fq,BBR 由于需要 pacing,而在种情况下 pacing 依赖于高精度timer,导致需要消耗大量额外 CPU,在高 PPS 的场景下,性能会变差;
  2. 流量调度换成 tc-fq 之后,BBR 不再使用额外的高精度时钟,CPU 消耗与 cubic 差不太多,性能也与 cubic 相当 (差5%以内)

最后关于 BBR 建议:

  1. 若仅仅在内网使用,内网环境带宽高,时延低,低丢包率的情况下,建议继续使用 cubic;
  2. 若对外提供服务,建议使用 BBR,并使相应的网卡使用 tc-fq 调度,否则可能占用额外的 CPU 资源,影响性能;
  3. 在 Aliyun Linux2 上,不同的连接拥塞算法是可以不一样的,并且 tcp 拥塞控制算法可以分 net_namespace 来控制;所以,如果有一台机器上有多个容器,每个容器又分属不同的 net_namespace,而有些容器只对外提供服务,有些容器只对内提供服务,可以对这些容器分别设置不同的拥塞控制算法,并将跑 BBR 的容器的网卡配置成 tc-fq 调度算法;

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK