diva-notes
  • README
  • Ads
    • 定价策略
    • 广告层级
    • 归因模型
    • 买量
    • Chat GPT
    • Google
  • AI
    • 参考资料
    • Chat GPT
    • stable-diffusion-webui安装
  • Algorithm
    • 倍增
    • 并查集
    • 参考
    • 环的判断
    • 凸包
    • 蓄水池抽样
    • 最短路径
    • 最小生成树
    • KMP算法
    • Rabin-Karp算法
    • Tarjan桥算法
  • Architecture
    • Serverless
  • Career
  • CICD
    • 代码质量
    • CICD实践
  • Data Structure
    • 布谷鸟过滤器
    • 布隆过滤器
    • 浮点
    • 红黑树
    • 锁
    • LSM树
  • DB
    • My SQL
      • 隔离级别
      • 架构
      • 索引
      • 锁
      • 页结构
      • 主从同步
      • ACID
      • Log
      • MVCC
      • Questions
    • Postgres
      • 持久化
      • 对比MySQL
      • 隔离级别
      • 索引
      • Greenpulm
      • MVCC
    • 倒排索引
    • 列式存储
    • H Base
    • HDFS
    • MPP数据库选型
    • Questions
  • Distributed System
    • 分布式事务
    • 服务网格
    • BASE理论
    • CAP
    • Etcd
    • Raft协议
    • ZAB协议
  • Go
    • 1.语言基础
      • 1.CPU寄存器
      • 2-1.函数调用
      • 2-2.函数调用栈
      • 2.接口
      • 3.汇编
      • 4.调试
    • 2.编译
      • 1.编译
      • 2.词法与语法分析
      • 3.类型检查
      • 4.中间代码生成
      • 5.机器码生成
    • 3.数据结构
      • 1.数组array
      • 2.切片slice
      • 3.哈希表map
      • 4.字符串
    • 4.常用关键字
      • 1.循环
      • 2.defer
      • 3.panic和recover
      • 4.make和new
    • 5.并发编程
      • 1.上下文Context的实现
      • 2-1.runtime.sema信号量
      • 2-2.sync.Mutex的实现
      • 2-3.sync.WaitGroup
      • 2-4.sync.Once的实现
      • 2-5.sync.Map的实现
      • 2-6.sync.Cond
      • 2-7.sync.Pool的实现
      • 2-8.sync.Semaphore的实现
      • 2-9.sync.ErrGroup
      • 3.定时器Timer的实现
      • 4.Channel的实现
      • 5-1.调度-线程
      • 5-2.调度-MPG
      • 5-3.调度-程序及调度启动
      • 5-4.调度-调度策略
      • 5-5.调度-抢占
      • 6.netpoll实现
      • 7.atomic
    • 6.内存管理
      • 1-1.内存分配基础-TCmalloc
      • 1-2.内存分配
      • 2.垃圾回收
      • 3.栈内存管理
    • 参考
    • 各版本特性
    • 坑
    • Go程序性能优化
    • http.Client
    • net.http路由
    • profile采样的实现
    • Questions
    • time的设计
  • Kafka
    • 高可用
    • 架构
    • 消息队列选型
    • ISR
    • Questions
  • Network
    • ARP
    • DNS
    • DPVS
    • GET和POST
    • HTTP 2
    • HTTP 3
    • HTTPS
    • LVS的转发模式
    • NAT
    • Nginx
    • OSI七层模型
    • Protobuf
    • Questions
    • REST Ful
    • RPC
    • socket缓冲区
    • socket详解
    • TCP滑动窗口
    • TCP连接建立源码
    • TCP连接四元组
    • TCP三次握手
    • TCP数据结构
    • TCP四次挥手
    • TCP拥塞控制
    • TCP重传机制
    • UDP
  • OS
    • 磁盘IO
    • 调度
    • 进程VS线程
    • 零拷贝
    • 内存-虚拟内存
    • 内存分配
    • 用户态VS内核态
    • 中断
    • COW写时复制
    • IO多路复用
    • Questions
  • Redis
    • 安装
    • 参考
    • 高可用-持久化
    • 高可用-主从同步
    • 高可用-Cluster
    • 高可用-Sentinel
    • 缓存一致性
    • 事务
    • 数据结构-SDS
    • 数据结构-Skiplist
    • 数据结构-Ziplist
    • 数据结构
    • 数据类型-Hashtable
    • 数据类型-List
    • 数据类型-Set
    • 数据类型-Zset
    • 数据淘汰机制
    • 通信协议-RESP
    • Questions
    • Redis6.0多线程
    • Redis分布式锁
    • Redis分片
  • System Design
    • 本地缓存
    • 错误处理
    • 大文件处理
    • 点赞收藏关注
    • 短链接生成系统
    • 负载均衡
    • 高并发高可用
    • 规则引擎
    • 集卡活动
    • 秒杀系统
    • 评论系统
    • 熔断
    • 限流
    • 延迟队列
    • Docker
    • ES
    • K 8 S
    • Node.js
    • Questions
  • Work
    • Bash
    • Charles
    • Code Review
    • Ffmpeg
    • Git
    • intellij插件
    • I Term 2
    • Mac
    • mysql命令
    • Nginx
    • postgresql命令
    • Protoc
    • Ssh
    • Systemd
    • Tcp相关命令
    • Vim
Powered by GitBook
On this page
  • 超时重传机制
  • 快速重传机制
  • SACK 方法
  • Duplicate SACK / D-SACK
  • 超时时长的设置
  • TCP的RTT算法
  • 经典算法
  1. Network

TCP重传机制

PreviousTCP拥塞控制NextUDP

Last updated 2 years ago

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意,接收端给发送端的 ack 确认只会确认最后一个连续的包,比如,发送端发了 1,2,3,4,5 一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以 ack 的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。

超时重传机制

一种是不回 ack,死等3,当发送方发现收不到3的 ack 超时后,会重传3。一旦接收方收到3后,会 ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到 ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。

对此有两种选择:

  • 一种是仅重传 timeout 的包。也就是第3份数据。

  • 另一种是重传 timeout 后所有的数据,也就是第3,4,5这三份数据。

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等 timeout,timeout 可能会很长(在下篇会说TCP是怎么动态地计算出 timeout 的)

快速重传机制

于是,TCP引入了一种叫 Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就 ack 最后那个可能被丢了的包,如果发送方连续收到3次相同的 ack,就重传。Fast Retransmit 的好处是不用等超时了再重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就 ack 回2,结果2因为某些原因没收到,3到达了,仍然 ack 回2,后面的4和5都到了,也仍然 ack 回2,因为2还是没有收到,于是发送端收到了三个 ack=2 的确认,知道了2还没有到,于是就马上重传2。然后,接收端收到了2,此时因为3,4,5都收到了,于是 ack 回6。示意图如下:

Fast Retransmit 只解决了一个问题,就是 timeout 的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个 ack(2) 是谁传回来的?也许是#6,#10,#20传来的,这样发送端很有可能要重传从2到20的这堆数据(这就是某些 TCP 的实际的实现)。可见,这是一把双刃剑。

SACK 方法

这样,在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。在 Linux 下,可以通过 tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

注意1:接收方有权把已经报给发送端 SACK 里的数据给丢了。因此不能用 SACK 完全代替 ACK,ACK 仍然需要保留。

Duplicate SACK / D-SACK

有时候接收端收到数据了,但是返回的 ACK 丢了 / 超时到达。发送端没有收到 ACK,以为丢包了,就会重传。这种重传其实是没必要的。

因此有了 Duplicate SACK ,又称 D-SACK。从上面可以看到,SACK 是一个区间数组,如果这个数组的第一个区间被 ACK 覆盖,或者被第二个区间包含,就是 D-SACK 的做法,表示收到的数据之前传过了,是重复的数据,不需再传了。

超时时长的设置

TCP的RTT算法

从前面的 TCP 重传机制我们知道 Timeout 的设置对于重传非常重要。

  • 设长了,重发就慢,丢了老半天才重发,没有效率,性能差

  • 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的重发。

而且,这个超时时间在不同的网络的情况下,根本没有办法设置一个死的值,只能动态地设置

为了动态地设置,TCP 引入了 RTT: Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置 RTO (Retransmission TimeOut),以让我们的重传机制更高效。 听起来似乎很简单,好像就是在发送端发包时记下 t0,然后接收端再把这个 ack 回来时再记一个 t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。

总体思路是:超时时间 = 较大权重 × 最近几次 RTT + 较小权重 + 稍远几次的 RTT

经典算法

1)首先,先采样 RTT,记下最近好几次的 RTT 值。

2)然后做平滑计算 SRTT((Smoothed RTT)。公式为:

其中的 α 取值在0.8 到 0.9之间,这个算法英文叫 Exponential weighted moving average,中文叫加权移动平均

3)开始计算 RTO。公式如下:

其中:

  • UBOUND 是最大的 timeout 时间,上限值

  • LBOUND 是最小的 timeout 时间,下限值

  • β 值一般在1.3到2.0之间。

Karn / Partridge 算法

但是上面的这个算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和 ACK 回来的时间做 RTT 样本值,还是用重传的时间和 ACK 回来的时间做RTT样本值?

这个问题无论你选那头都是按下葫芦起了瓢。 如下图所示:

  • 情况(a)是 ACK 没回来,所以重传。如果你计算第一次发送和 ACK 的时间,那么,明显算大了。

  • 情况(b)是 ACK 回来慢了,但是导致了重传,但刚重传不一会儿,之前 ACK 就回来了。如果你是算重传的时间和 ACK 回来的时间的差,就会算短了。

但是,这样一来,又会引发一个大BUG——如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的RTO很小),于是,因为重转的不算,所以,RTO就不会被更新,这是一个灾难。 于是Karn算法用了一个取巧的方式——只要一发生重传,就对现有的 RTO 值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的RTT也不靠谱。

Jacobson / Karels 算法

计算平滑RTT

计算平滑RTT和真实的差距(加权移动平均)

神一样的公式

在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。这就是算法中的“调得一手好参数”,(nobody knows why, it just works…)

参考

另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看),这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,新增的 SACK 用于汇报已收到的数据段。参看下图:

注意2:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《》

中定义的经典算法是这样的:

SRTT=(α∗SRTT)+((1−α)∗RTT)SRTT = ( α * SRTT ) + ((1- α) * RTT)SRTT=(α∗SRTT)+((1−α)∗RTT)
RTO=min[UBOUND,max[LBOUND,(β∗SRTT)]]RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]RTO=min[UBOUND,max[LBOUND,(β∗SRTT)]]
img

所以1987年的时候,搞了一个叫,这个算法的最大特点是——忽略重传,不把重传的RTT做采样(你看,你不需要去解决不存在的问题)。

前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。所以,1988年,又有人推出来了一个新的算法,这个算法叫 Jacobson / Karels Algorithm(参看)。这个算法引入了最新的 RTT 的采样和平滑过的 SRTT 的差距做因子来计算。 公式如下:(其中的DevRTT是Deviation RTT的意思)

SRTT=SRTT+α(RTT–SRTT)SRTT = SRTT + α (RTT – SRTT)SRTT=SRTT+α(RTT–SRTT)
DevRTT=(1−β)∗DevRTT+β∗(∣RTT−SRTT∣)DevRTT = (1-β)*DevRTT + β*(|RTT-SRTT|)DevRTT=(1−β)∗DevRTT+β∗(∣RTT−SRTT∣)
RTO=µ∗SRTT+∂∗DevRTTRTO= µ * SRTT + ∂ *DevRTTRTO=µ∗SRTT+∂∗DevRTT

最后的这个算法在被用在今天的TCP协议中(Linux的源代码在:)。

RFC 2018
TCP SACK的性能权衡
RFC793
Karn / Partridge Algorithm
RFC6289
tcp_rtt_estimator
陈皓 - TCP 的那些事儿(上)
img
img