惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 布谷鸟过滤器 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading 布谷鸟过滤器 Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式
TCP 可靠传输实现原理 - 3.拥塞控制
2018-12-30 · via 蛮荆

2018-12-30 计算机网络

概述

TCP 实现可靠传输层的核心有三点:

  1. 确认与重传 (已经可以满足 “可靠性”,但是可能存在性能问题)
  2. 滑动窗口 (也就是流量控制,为了提高吞吐量,充分利用链路带宽,避免发送方发的太慢)
  3. 拥塞控制 (防止网络链路过载造成丢包,避免发送方发的太快)

滑动窗口和拥塞控制相互制约,使发送方可以从网络链路的全局角度来自动调整发送速率,从这个角度来看,TCP 对于整个网络的意义已经超过 “传输层”。

本文主要讲解三个核心中的第三点: 拥塞控制

滑动窗口主要关注发送方到接收方的流量控制

拥塞控制更多地关注整个网络 (链路) 层面的流量控制

拥塞控制的视角更为全面,会对整个网络链路中的所有主机、路由器,以及降低网络传输性能的有关因素进行综合考量。

前置知识点

在讲解 TCP 的拥塞控制之前,先来复习几个基本知识点。

1. 丢包

丢包一般分两种情况:

  • 网络质量导致的零星丢包
  • 网络拥塞导致的大量丢包

丢包会引起重传,重传会加剧网络负载,网络负载严重后,丢包会更严重,进入 “恶性循环”。

如果网络中所有发送方和接收方都进入 “恶性循环”,就会形成 “网络丢包” 风暴,直至拖垮整个网络。

所以 TCP 需要站在整个网络层面的角度,对传输的整个过程链路进行规划和把握,也就是本文的主题 拥塞控制


拥塞控制

TCP 的拥塞控制的核心思想是根据网络状况动态调整拥塞窗口(cwnd)的大小,通过反馈机制(如丢包和延迟)检测网络拥塞,并相应地调整数据发送速率,主要由 4 个方面组成:

  1. 慢启动
  2. 拥塞避免
  3. 快速重传 (拥塞发生后)
  4. 快速恢复

拥塞控制状态机

目前有非常多的 TCP 的拥塞控制协议,例如:

  • 基于丢包 的拥塞控制:采取慢启动方式,逐渐增大拥塞窗口,出现丢包时减小拥塞窗口,如 Reno、Cubic、NewReno
  • 基于时延 的拥塞控制:通过延迟和带宽估算来进行拥塞控制,延迟增大时减小拥塞窗口,延时减小时增大拥塞窗口,如 Vegas、FastTCP
  • 基于链路容量 的拥塞控制:实时测量网络带宽和时延,根据网络传输中的 报文总量 和带宽时延的乘积 进行拥塞控制,如 BBR (Google 开发的一种算法,通过估算瓶颈带宽和往返时延来控制拥塞)
  • 基于学习的 拥塞控制:没有特定的拥塞信号,通过分析不同网络条件下的传输行为,借助评价函数,基于训练数据,使用机器学习的方法生成最优的拥塞控制规则,如 Remy

本文不对上述协议展开讲解,感兴趣的读者可以查阅相关资料。

1. 慢启动

发送方和接收方三次握手后连接建立后,是不是应该直接开始快速发送数据包呢?TCP 认为,虽然单个连接上快速发送大量数据包是 OK 的 (毕竟有滑动窗口地址负责流量控制),但是如果网络中的所有连接都这么做的话,避免会导致流量高峰,进而形成网络拥塞。所以,刚开始的时候应该慢点发送数据,然后根据网络状态不断进行调整 (网络负载较小时就多发送,网络负载较小时就少发送)。

Typically, ssthresh starts at 65535 bytes.

  • 初始状态下,cwnd 设置为一个较小的值(例如 2 个 MSS)
  • 每收到一个 Ack,cwnd 加 1 (cwnd += 1)
  • 所以当前的 cwnd, 每经过一个 RTT, cwnd 翻倍 (cwnd *= 2)
  • 直到 cwnd >= ssthresh (预定义的慢启动阈值) 或者检测到丢包,cwnd 增长结束 (进入拥塞避免阶段)

因为是指数级增长,所以 “慢启动” 其实并不慢。

图片来源: https://coolshell.cn/articles/11609.html

Linux 3.0 之后,采用了 Google 的建议,将 cwnd 初始化为 10 个 MSS。

https://github.com/torvalds/linux/blob/02f8c6aee8df3cdc935e9bdd4f2d020306035dbe/include/net/tcp.h#L200

# Linux 3.0 默认 cwnd 大小

#define TCP_INIT_CWND		10

2. 拥塞避免

不同拥塞控制算法的核心差异主要在 拥塞避免 阶段。

拥塞避免 并非指完全能够避免网络拥塞,而是指在拥塞避免阶段,将拥塞窗口控制为按线性规律增长,使网络尽可能避免出现拥塞。

Typically, ssthresh starts at 65535 bytes.

慢启动持续一段时间后,拥塞窗口变为一个较大的值,此时距离 网络拥塞点 也比较接近了,所以需要降缓速度慢下来。

图片来源: Wireshark 网络分析就是这么简单

cwnd >= ssthresh 时,进入拥塞避免阶段,此时 cwnd 的增长方式变化为:

  • 每收到一个 Ack,cwnd = cwnd + 1/cwnd (使用 cwnd 自己的倒数作为增长值,cwnd 越大,增长值越小,反之 cwnd 越小,增长值越大)
  • 每经过一个 RTT, cwnd = cwnd + 1

从算法的变化过程可以看出,进入拥塞避免后,cwnd 由指数型增长变化为线性增长。

利用 Wireshark 估算网络拥塞点

网络拥塞的特征是连续丢包,丢包会引起重传,Wireshark 能够标识出重传的数据包,因此可以先从 Wireshark 中找到一连串重传包中的第 1 个,再根据该重传包的 Seq 值找到其原始包,最后计算该原始包发送时刻的在途字节数。由于网络拥塞就是在该原始数据包发出去的时刻发生的,所以这个在途字节数就大致代表了 网络拥塞点 的大小。

可以根据这个结果,多次采样进行参考,然后选定一个合适的值作为该连接的拥塞点。例如根据采样结果取一个偏小的 (类似接口响应的 95 分位) 数值。

3. 拥塞发生

传统 (基于丢包) 的 TCP 流量控制认为,一旦发生丢包,就是网络链路发生了拥塞,所以发送方会急剧地减小发送窗口,甚至进入短暂的等待状态(超时重传)。 所以,1% 的丢包率并不只是降低 1% 的传输性能,而是可能降低 50% 甚至更多 (取决于具体的 TCP 实现)。

图片来源: Wireshark 网络分析就是这么简单

拥塞发生之后,又有几种常用的算法来解决:

1. TCP Tahoe 算法

该算法和超时重传机制一样,工作过程如下:

  • 修改 sshthresh = cwnd / 2
  • 修改 cwnd = 1
  • 进入慢启动过程

图片来源: Wireshark 网络分析就是这么简单

因为拥塞窗口大小直接变为 1,然后重新开始慢启动,所以这是一种极为保守的算法。

2. TCP Reno 算法

该算法工作过程如下:

  • 修改 cwnd = cwnd / 2
  • 修改 sshthresh = cwnd
  • 进入快速恢复过程 (下文中会提到)

图片来源: Wireshark 网络分析就是这么简单

3. TCP Cubic 算法

Cubic 是一种改进的拥塞控制算法 (Linux 2.6.18 之后默认使用),是基于丢包机制的拥塞控制算法中表现最好的。

Cubic 和 Reno 算法一样,发生拥塞后也是采用加法线性增加,但是每次不是加 1, 而是根据一个特定的公式来决定窗口增加的大小,从而到达更快地收敛 cwnd。

如图所示,相比于 Reno 算法发生拥塞后的 “大起大落”,Cubic 算法要相对 “顺滑” 一些。

由此可见,没有网络拥塞时,发送窗口越大,性能越好,此时应该尽量增大接收窗口 (例如 LAN 环境中)。如果经常发生拥塞,那限制发送窗口反而能提高性能,因为丢包 + 重传是绝对的性能杀手。

4. 快速恢复

在重传数据包之后,发送方不会立即进入慢启动阶段,而是通过调整 cwnd 来快速恢复到丢包前的传输速率。

拥塞发生之后,如果发生了快速重传 (收到 3 个及以上 Dup Ack), 说明此时网络拥塞并没有很严重,所以只要保证接下来降低发送速率就可以了,完全不需要像超时重传一样那么保守,此时就可以进入到快速恢复阶段了。

进入 快速恢复 阶段之前,cwnd 和 sshthresh 已经被修改过了,例如 TCP Reno 算法修改如下:

cwnd = cwnd / 2
sshthresh = cwnd

快速恢复 (Fast Recovery) 算法如下:

  • cwnd = sshthresh + 3 * MSS(收到 3 个 Ack)
  • 重传 Dup Ack 指定的数据包
  • 再次收到 Dup Ack,修改 cwnd = cwnd + 1
  • 再次 Ack,修改 cwnd = sshthresh, 进入拥塞避免阶段

不足 (局限性)

相比保守的 RTO 超时重传,快速恢复调整 cwnd 和 sshthresh 更加符合真实的网络链路场景,但是该算法的不足在于: 严重依赖引起快速重传的 Dup Ack, 引发快速重传至少需要 3 个 Dup Ack 包,但是 3 个 Dup Ack 并不意味着只丢了 3 个数据包 (很可能是多个数据包),但是快速重传只会重传一个,其他丢失的数据包依然只能等待 RTO 超时重传,于是进入一个恶性循环:

  • 超时一个数据包,sshthresh 就锐减一半
  • 超时多个数据包,sshthresh 就会减少到 1, 并且不会触发快速恢复算法了

慢启动和快速恢复到底指什么?

慢启动和快速恢复针对的目标是同一个: cwnd 初始值大小,慢启动设置 cwnd 初始值为 1, 快速恢复设置 cwnd 初始值为 ssthresh。


小结

下面这张图,将慢启动、拥塞控制、拥塞发生、快速重传和超时重传各个不同的状态值,很好地展示了出来。


扩展阅读