用时间戳优化 TCP 实践
Linux TCP 对 timestamps 的利用很不充分,这很可惜。但凡信息就要利用,信息利用率和效率正相关,拥塞控制在实验室工作良好在真实环境拉胯就是因为实验室环境已知,按照信息论,不需额外信息就能 hold 住。
先看这篇:
TCP 时间戳妙用
这篇基本讲明了 TCP timestamps 如何甄别半程抖动。本文则要展示一下。
环境:
下面是打印两个半程抖动的代码:
#!/usr/local/bin/stap -g
%{
#include <linux/tcp.h>
#include <net/tcp.h>
%}
function print_delta(skk:long)
%{
struct sock *sk = (struct sock *)STAP_ARG_skk;
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
static unsigned long d1_var = 0, d2_var = 0;
static unsigned long d1_last = 0, d2_last = 0;
long delta1 = 0, delta2 = 0;
if (ntohs(inet_sk(sk)->inet_dport) == 5001) {
unsigned long d1 = tcp_time_stamp_raw() - tp->tsoffset - tp->rx_opt.rcv_tsval;
unsigned long d2 = tp->rx_opt.rcv_tsval - tp->rx_opt.rcv_tsecr;
if (d1_last == 0) d1_last = d1;
if (d2_last == 0) d2_last = d2;
// 假装计算方差,实际上简陋得很。
if (d1 > d1_last)
delta1 = d1 - d1_last;
else
delta1 = d1_last - d1;
if (d2 > d2_last)
delta2 = d2 - d2_last;
else
delta2 = d2_last - d2;
// 不丢失历史,移指平均。
d1_var = (d1_var*1000*4/10 + delta1*1000*6/10)/1000;
d2_var = (d2_var*1000*4/10 + delta2*1000*6/10)/1000;
d1_last = d1;
d2_last = d2;
STAP_PRINTF(&#34;d1:%llud2:%llu \n&#34;, d1_var, d2_var);
}
%}
probe kernel.function(&#34;tcp_ack_update_rtt&#34;).return
{
print_delta($sk);
}下面是一条 netem 规则:
tc qdisc add dev eth0 root netem delay 10ms 15ms 90%不管它配置在哪个半程,ping 的结果如下:
抖动很厉害,可如果打印两个半程的时间抖动就能明显区分。下图左边是 netem 配置在 ACK 半程的结果,右边是配置在 Data 半程的结果:
有此结论作为指导,容易猜测,如果抖动发生在 ACK 半程,拥塞控制按照全程算出来的可用带宽就可以补偿,反之则不能。
很容易验证,直连 25Gbps 带宽,下面是 netem 配置在 ACK 半程的打流结果:
如果将 cwnd 硬编码成 29200,结果如下(红色箭头指示开启 tp->snd_cwnd = 29200 的时刻):
如果 netem 配置在 Data 半程,同样设置 tp->snd_cwnd = 29200,结果如下:
且重传率非常高。
结论是 Data 稳定到达,ACK 抖动反馈,并不影响最大有效带宽,但盲目激进补偿会造成高重传率。
写死 cwnd 是因为我们知道抖动发生在哪个半程,实际场景中算法如何自己发现 ACK 半程抖动时这个很大的 cwnd 呢?
不断 probe 显然不够,因为算法本身以全程衡量 RTT ,抖动的 ACK 半程意味着算法想要的结果不会被反馈回来,以 BBR 为例,Deliver rate 的测量完全依赖全程时间而不是半程。
但 ACK 半程抖动时,发送端偏偏有这个能力打满带宽,问题是如何让它自己发现这种能力而不是以上帝视角帮它写死 cwnd 呢?
半程时间戳方差就是来提供帮助的,该信息明确甄别出抖动发生的具体半程,可用来指导拥塞控制算法作出决策,比如 Vegas 可从中受益。
不写死 cwnd ,可以设计一个与 d1_var 正相关,与 d2_var 负相关的函数,计算一个额外的 cwnd 补偿,加到拥塞控制算法的计算结果中。举个最简单的例子:
cwnd = cwnd + alpha*(d1_var - d2_var);
写入代码:
...
function print_delta(skk:long)
%{
...
d1_var = (d1_var*1000*4/10 + delta1*1000*6/10)/1000;
d2_var = (d2_var*1000*4/10 + delta2*1000*6/10)/1000;
d1_last = d1;
d2_last = d2;
// 100是拍脑袋结果,实际可调
tp->snd_cwnd = tp->snd_cwnd + 100 * (d1_var - d2_var);
STAP_PRINTF(&#34;d1:%llud2:%llu, %llu%llu\n&#34;, d1, d2, d1_var, d2_var);
}
%}
probe kernel.function(&#34;tcp_write_xmit&#34;).return
{
print_delta($sk);
}如下图,左边是 ACK 半程抖动的补偿结果,右边是 Data 半程抖动的补偿结果:
抖动发生在 ACK 半程,cwnd 依然可根据算法的补偿上升到一个很高的值,但对于 Data 半程的抖动,算法意识到可能真的发生了拥塞,不再激进盲目补偿,cwnd 不会增加,也不会带来额外重传。
该简陋算法已可以识别抖动的位置,进行决定是否要补偿了。
一个额外信息参与进来,能获得的收益要比优化启发式算法好很多,再优秀启发也终究是启发,终究是猜测,终究会误判,而误判的效应会叠加。
周中想到的一个 TCP 时间戳的妙用,趁周四晚上帮忙查了一个有点相关的问题后,赶紧做了一个实验,果然有效,趁到周末,总结一下。浙江温州皮鞋湿,下雨进水不会胖。
页:
[1]