TCP RTT测量妙计

TCP RTT 测不准饱受诟病:

  • 接收端可能开启 Delayed ACK ,Delay 延时不确定。
  • 接收端可能开启 LRO/GRO ,Merge 延时不确定。
  • 重传时无法区分原始数据包和重传数据包。

第三点还成了 QUIC 的反面教材。

果真如此?别人怎么教你,你就怎么记笔记,但凡跟笔记对不上的就是错的?

今晚,我来演示一种精确测量 RTT 的方法,你的笔记上肯定没有,但可以加上去。

这种方法的有趣之处恰哈因为它是在 Recovery 状态下测量。按照 TCP 规范,接收端在收到非连续报文时必须立刻 ACK,这避开了 Delayed ACK 的不确定性。

做法很简单:

  • 开启 timestamps 选项,该 opt 里的 tsval 可以 echo 回来,用该字段作为识别标识。
  • 为重传 skb 打上 anch = opts->tsval + 1 作为 tsval,发送之,记录当前 ts_us 到 skb。
  • 为其它传输 skb 及 重传 skb 避开 anch 作为 tsval,取 anch + 1 作为其 tsval。
  • 观测 ACK 的 tsecr,扫描 rtx queue,若 ACK.tsecr == anch,now_us - skb.ts_us 则为精确 RTT。

下面是一个简单的 POC 脚本:

#!/usr/local/bin/stap -g
%{
#include <linux/tcp.h>
#include <net/tcp.h>

__u32 anch = 0;
__u32 skip = 0;

%}

function set_ts(tpp:long, opt:long)
%{
	struct tcp_sock *tp = (struct tcp_sock *)STAP_ARG_tpp;
	struct sock *sk = (struct sock *)tp;
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_out_options *opts = (struct tcp_out_options *)STAP_ARG_opt;

	if (tp != NULL &&
	    ntohs(inet_sk(sk)->inet_dport) == 5001 &&
	    icsk->icsk_ca_state == TCP_CA_Recovery) {
		if (opts->tsval == skip) { // 避开 anch
			opts->tsval ++; // 不要避开太多,当心对端 PAWS 检测
		} else if (skip == 0){ // 设置 anch 作为识别符
			anch = opts->tsval; 
			opts->tsval = anch;
			skip = anch;
			STAP_PRINTF("set ts: %d\n", anch);
		}
	}
	if (tp != NULL && icsk->icsk_ca_state == TCP_CA_Open) {
		skip = 0;
		anch = 0;
	}
%}

function get_ts(skk:long)
%{
	struct sock *sk = (struct sock *)STAP_ARG_skk;
	struct tcp_sock *tp = (struct tcp_sock *)sk;
	struct inet_connection_sock *icsk = inet_csk(sk);

	if (ntohs(inet_sk(sk)->inet_dport) == 5001 &&
	    icsk->icsk_ca_state == TCP_CA_Recovery) {
		__u32 ecr = tp->rx_opt.rcv_tsecr + tp->tsoffset;
		if (skip == ecr) { // 找到确认打上 anch 的重传包的 ACK。
			u32 delta = tcp_time_stamp(tp) - ecr + tp->tsoffset;
			delta = delta * (USEC_PER_SEC / TCP_TS_HZ);
			STAP_PRINTF("echo ts: %d  anch: %d  rtt_us: %d srtt_us: %d\n", ecr, anch, delta, tp->srtt_us>>3);
			skip = 0;
			anch = 0;
		}
	}
%}

probe kernel.function("tcp_options_write")
{
	set_ts($tp, $opts);
}

probe kernel.function("tcp_ack_update_rtt")
{
	get_ts($sk);
}

有趣的是,timestamps 在测量 RTT 过程中没有参与任何与时间相关的运算,仅作为识别符。实际中, TCP timestamps 精度太低,不能识别 us,我曾一直以为 TCP 完全依赖它计算 RTT,但并没有:

// 实在是不堪大用 
static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{
        return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
}

上述脚本并未涉及扫描 rtx queue 的逻辑,因为需要修改 Linux TCP 源码,这不是晚上一两个小时能搞定的。

TCP 传输优化中,可以这么利用这个精确的 RTT:

  • 若精确的 RTT 与 srtt_us 相同未显著增大,大概率非拥塞丢包,可适当激进,重传完成后可 undo。
  • 在整个 Recovery 状态,均可使用这种方法进行精确 RTT 测量,并跟踪其与 srtt_us 的差值。
  • 配合前面的半程 RTT 抖动检测,即便是 Data 半程的抖动,也能区分是抖动还是拥塞。

玩法还是挺多的…

但如果不支持 timestamps 怎么办?

目标不是用 timestamps,目标是需要一个字段能 echo 回来用来将 Data 和 ACK 对应起来就行。

显然,seq 也可 echo 回来,把它 echo 回来的就是 ACK。为捕捉标识以精确计算 RTT 从而判断是否拥塞,最终决策是否 undo,需花式重传:

  • 若 una~una + mss 作为一个 skb 传输并被 mark lost,则重传 una~una + fk(fk 取 123 或 222等花值均可)。
  • 记录当前 ts_us 到该重传 skb。
  • 保留 una + fk ~ una + mss 为空洞,以期接收端及时回复 ACK。
  • 观测 ACK,扫描 rtx queue,若 ACK == una + fk == skb.end_seq,now_us - skb.ts_us 则为精确 RTT。

花式重传可以区分原始包和重传包:

  • 若 ACK 覆盖 una + fk,则该 ACK 非重传包触发,大概率非拥塞,无效重传,但情况复杂,忽略。
  • 若 ACK 等于 una + fk,则该 ACK 为重传包触发,按本文上述方法计算 RTT,与 srtt_us 比较。

后面的玩法就和 timestamps 开启时一样的。

花式传输还可以更花,后面单独介绍,本文仅和 RTT 测量相关。

至此,我们已经有了三个信息:

  • Open 状态下常规 srtt_us。
  • 任意状态两个半程 ts 的抖动。
  • Recovery 状态精确的 RTT。

其中后两个都是通过额外的想法挖掘的。我还是老观念,充分利用信息,把现有信息运用到极致后再抱怨信息不够。

这个思路很常用,给定一个数学函数,你能看到它的图像,但这就够了吗?对它求导,对它的导数再求导,再再求导…你就可以看到它的单调性,凸凹,极值等别的性质了。

天气闷热,高温黄色预警,总也不下雨,白天用派森操作意克塞尔表格折腾了一天,晚上换点事情做。随笔记录。

浙江温州皮鞋湿,下雨进水不会胖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值