Linux 6.19 TCP 的两个极限拉扯

Linux 内核 net/ipv4/tcp_input.c 在 6.19 更新了,但 linux/net/ipv4/tcp_output.c 没怎么更新,tcp_input.c 比 tcp_output.c 文件要大很多,可关联 RFC793 的 Robustness Principle,谓之 Postel 定律。

但本期主要关注 Eric Dumazet 关于 TCP 的 2 个极限拉扯:

具体细节自行 review 派池,源码不分析,我要展开说的是从中印证的两个惯常的观点:

  • 协议本身的极限拉扯无法平衡,双方不可兼得;
  • 协议实现的极限拉扯终将获得极致优化的效果;

先看第一个关于 SACK Compression 的派池,它源自以下拉扯:

  • 双向耦合 TCP 为了减少 ACK,采用可捎带 ACK,在有机会被捎带之前等待 X ms(40ms ?) 时间,即 Delayed ACK;
  • 由于 Delayed ACK 降低了 sender 敏感性,纷纷魔改 X 或 MIN_RTO,破坏了收敛平衡,间接引入不公平;
    为不降低 sender (快速)重传敏感性,乱序报文到达会立即触发 ACK/SACK,相安无事好多年;
  • 随着流量增加,立即 ACK/SACK 问题暴露,详见 tcp: add SACK compression,SACK 亦增加 delay = min ( 5 % of SRTT, Y ms) 的延迟;
  • 手痒就调 5% 和 Y,但这是后话。异构大尺度覆盖场景,线性缩放简单易实现,但线性动态范围不够,双斜率是必然;
  • 在极小 SRTT 看来,SACK Compression delay 过于小,于是增加 sysctl_tcp_comp_sack_slack_ns 延迟 Z = 100us 再 SACK;
  • 但在更小 SRTT 看来,Z = 100us 又显得过大,于是 Z = 10us,详见 tcp: reduce tcp_comp_sack_slack_ns default value to 10 usec
  • 要 WAN 和 LAN 异构自适应,双斜率更进一步,终于要调 5 % 了,5 % 被参数化,详见 tcp: add net.ipv4.tcp_comp_sack_rtt_percent
  • tcp_comp_sack_rtt_percent 是不是和 sysctl_tcp_comp_sack_slack_ns 重叠了,但怎么都不行,极限拉扯还会继续;
  • ...

可见以上这种拉扯是没完没了的,因此它是恶性的,无效的。

本质问题是 TCP ACK 捎带机制导致的反向静默 delay 造成的 RTT 歧义,这个 delay 不是太大,就是太小,传统 max(min(,),) 双斜率都压不住,三斜率生万物,极限调参,最后就拉扯成光滑曲线了,也不再简单易实现,最后还是个不可得兼。

这问题本质上是 TCP 协议设计导致,如果现在试着设计一个新的可靠双向通信协议,为避免这种不可得兼的极限拉扯,只需要把捎带 ACK 分离出来即可,ACK 和 Data 不要耦合在一起,来回各一路,所有问题就解决了。

至于实现细节反而不重要,我此前提到过一个最简单的 "双向 Data & ACK 不依赖" 的实现:

  • 携带 ACK 标志的报文标明 ACK 类型,立即 or 捎带,若捎带则给出滞留时间;
  • 携带 Data 的报文标明该 Data 是否需要被立即 ACK;

只需最多额外 24bit 空间,极限拉扯就停止了,这 24bit 中,预留 2bit 标识类型,若特定值,则额外 22bit 是一个以 us 为单位的时间戳,足以覆盖到 1s > MAX_RTT。

在TCP 实现中,Linux TCP 采用 first_(s)ack/last_(s)ack 估猜 seq rtt 和 ca rtt 两类,然后择优适应 RTO,也算是废了老劲的启发,这是无能为力时所能做的极限了,然后你也可以参考 QUIC 协议中 ACK Frame 是如何改进的。

上周关于 DDP 和窗口的胶着拉扯的讨论中,我有回复:

这种事要分开看,不能耦合在一起,不然就就是打地鼠。实践中可能没有那种糟糕的最坏场景,阿里那个 OPS 也是实践的结果,随着硬件基础设施提升,异常处理会越来越低频,把重传,窗口,buffer 这些分出来单独迭代,反而比设计一个大而全的协议更简单。比如接收端全局视图,看谁没到就一个 NAK,不再做别的,如果网络本身就保序和无损,这机制便可随时摘掉,如果网络尽力而为,这机制也可任意加强迭代。

接下来看第二个派池,tcp: add net.ipv4.tcp_rcvbuf_low_rtt,这也是个极限拉扯,但它无关 TCP 协议本身,只关系实现:

  • receriver 利用自以为是的精准测量调整 rcvbuf,但与其做不准确的测量而高估结果,不如最简化,tcp: fix sk_rcvbuf overshoot 是一次理性的回归,古德;
  • 后来发现由于反馈存在 1 个 RTT 滞后,在 1 个 RTT 内需考虑平滑增长余量,即上述派池使 rcvbuf 增长偏慢,于是 tcp: fix too slow tcp_rcvbuf_grow() action 引入一个斜率增长余量修正,grow 更快,古德;
  • 再后来发现上述修正后的 rcvbuf 在 RTT 特别小的场景增长过快,过大的 rcvbuf 触发了内核其它子系统的不良连锁反应,降低了全局效率,于是 tcp: add net.ipv4.tcp_rcvbuf_low_rtt 来修正,古德;

从最后这个派池的描述中可以看到极限拉扯:

This is a follow up of commit aa251c8 ("tcp: fix too slow tcp_rcvbuf_grow() action") which brought again the issue that I tried to fix in commit 65c5287 ("tcp: fix sk_rcvbuf overshoot")

特别小 RTT 的流本身 BDP 就不会过大,但它们各自遵循不同的甚至正交的波动逻辑,以效率论,小 RTT 和小 rcvbuf 的比例会有一个平衡点,最终达到了共识,就会闭环,这就是良性优化。

以上两个 Linux 6.19 的 TCP 派池实属两类,我表达的观点中的推论非常直白,如果是协议标准设计本身没有覆盖到的问题,试图从实现上去 "优化" 并解决它,只能算是贴膏药打补丁,这种方式注定是打地鼠游戏,摁下葫芦起了瓢,始终无法彻底解决问题,我们从 "可靠保序的 TCP","尽力而为的以太网" 以及 socket API 层面的 "TCP 源端口选择的 bind & connect 实现" 的迭代中看到过无数这般结局,总是试图为其增加些 feature 以解决一些 issue,但总会产生新的 issue,一个接一个,最后超过协议内核本身,比如 RFC793,IEEE802.3。

然而,如果仅针对实现如此般拉扯迭代,便是极致优化。我们可以从 UDP/TCP reuseport 的实现,tcp_v4_rcv 实现以及IP 路由(特别是 IPv6 路由实现的 yet another 极限拉扯[from 大卫米勒])的实现等迭代过程看出这种风格的持续优化过程。

协议设计确定了大框架,像一个刚性木头架子,要么横着太窄,要么竖着太宽,无法改变,而协议的实现则取决于你用橡皮筋包裹还是用麻绳包裹,伸缩不确定,效率就不确定。

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

相关推荐
新兴AI民工2 小时前
【Linux内核九】进程管理模块:list_head钩子构造双向列表和一些宏定义
linux·运维·list·linux内核
小周学学学2 小时前
ESXI故障处理-重启后数据存储丢失
linux·运维·服务器
哎哟喂呢哈2 小时前
ddns 免费 ipv6
linux
Flash.kkl2 小时前
Linux——线程的同步和互斥
linux·开发语言·c++
sunfove2 小时前
Python 面向对象编程:从过程式思维到对象模型
linux·开发语言·python
王九思2 小时前
Ansible 自动化运维介绍
运维·自动化·ansible
三不原则3 小时前
实战:基于 GitOps 实现 AI 应用的自动化部署与发布
运维·人工智能·自动化
云和数据.ChenGuang3 小时前
达梦数据库安装服务故障四
linux·服务器·数据库·达梦数据库·达梦数据
PPPPPaPeR.3 小时前
使用vim实现进度条(初级)
linux·编辑器·vim