• 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    有趣的是,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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上述脚本并未涉及扫描 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。

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

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

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

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

  • 相关阅读:
    PIL中的paste方法拼接透明背景照片
    mysql面试题(史上最全面试题,精心整理100家互联网企业,面试必过)
    为什么我从 Swift 转向 Flutter,你也应该这样做
    Servlet —— Tomcat, 初学 Servlet 程序
    基于CentOS7搭建Linux, Nginx, MySQL, PHP环境
    vue通过获取url中的信息登录页面
    Spring如何使用环境变量控制配置文件加载呢?
    Edexcel A-Level化学真题讲解(1)
    33基于MATLAB的对RGB图像实现中值滤波,均值滤波,维纳滤波。程序已通过调试,可直接运行。
    magma build system 分析 —— 第04记 make -n 的记录
  • 原文地址:https://blog.csdn.net/dog250/article/details/125414489