复杂来自确定性,复杂影响稳定性,对抗不确定性的方式就反而是松弛。网络是一个准随机的统计复用环境,为适应它,需要松弛。
最近看了准随机网络随机 spraying 策略,那么如何实现一个这样的协议呢?正好上周又涉及到多路径冗余多发选收的一个算法,我觉得这在传输意义上与数据解耦后,便都是一回事,进行了一些思考,作文以记之。
要设计一个传输算法,传统地看,其核心是可靠性,保序和排重,但区别在于多大程度上实现这些保证,这事能让应用层业务自己做吗?如果能,你甚至可以直接放过,什么都不做。
如果业务是低时延优先的,它一定知道用丢包不重传来交换,如果业务是需要可靠性的,它就一定知道不能确保低时延,一个合理的折中点是,传输层协议不要自作多情。
试图在传输层解决一切拥塞控制,流量控制的问题,这本身就是 TCP 留给后人的技术债,你们想还就接着还。
但凡涉及可靠,冗余,去重,保序的话题,缓存,重传,定时器几乎就是第一设计,然后就成了远不如 TCP 的 YaTCP,由于编程的人容不得丢包和不确定性,他们倾注洪荒之力确保不丢包,不得已再用重传挽救,说到底还是围着 buffer 和启发式判定绕圈圈,一绕就是 30 多年。
复杂性来自确定性,因为要克服随机做功的,而确定性本身只是单一指标的修饰,这是端到端传输自由度决定的,确定的可靠性就是不确定的时延抖动,确定性的时延就是不稳定的带宽。
不如把确定性交给统计,反而在更方面都很均衡。但编程的人永远会盯着 99 分位以外,恰因如此,他们在 99 分位外引入不可控的单自由度的复杂性,鱼与熊掌不可兼得,终成长尾时延,自食其果。
统计律表明,你越放松,结果越均衡,若要确定性,要注入信息,若没信息,那就随机。端到端传输由于互联网的沙漏模型的本质,决定了信息的上限仅限于端到端,而对网络一无所知,这话我说了 15 年多,但凡你启发,你挣扎,结果就会和最优解有偏差。不信你用一个启发式算法猜硬币,看看是高于还是低于 50%,再算算 ROI。
具体到冗余多发,我很理解重传的意义,因为即使 N = 100 的冗余度,也还是有概率会让 N 份副本全部丢失,因此必须要重传兜底,可重传真的只是兜底吗?看看 TCP 设计了多少机制来减少误判吧,一个机制引入问题,再引入其它机制缓解而不是解决问题,如此反复而不得有终。
都 N 倍冗余了,相当于重传了 N 遍,编程的人执着要求确认,可编程的人却接受重传 M 次不成功便成仁的设定。至于何时重传,定时器似乎又是第一正义,但真的需要定时器吗?
把约束都解除就豁然开朗了,下面是一个算法,发送端不缓存任何报文,无重传,无可靠保证,仅大概率保证传输可靠性以及保序,可用于 UDP-Based 传输和隧道协议:
- 准可靠性保证由发送端冗余度负责;
- 保序由算法乱序队列和业务自身负责;
- 队头阻塞由小概率性事件不发生规避;
伪代码如下:
// 静态可配置常量
const T: integer // WinSet 内乱序 seq 积压数量阈值
const D2: integer // AllMin - Gseq 兜底差值阈值
const N: integer // 总路径条数
const HostReorderMode: bool // true:隧道仅存 seq,乱序直接放行报文;
// false:隧道缓存报文,乱序暂存不转发
// 运行时全局状态
var Gseq: integer = 0 // 已连续有序输出的最大序列号
var WinSet: Set<integer> = empty set // 存储所有大于 Gseq 的 seq,用于冗余去重
var PathMax: array[0..N-1] of integer = [0] * N
var AllMin: integer = 0 // 所有路径 PathMax 最小值(最慢路径进度)
var SurplusCnt: integer = 0 // WinSet 中 seq 总数量,两种模式均生效
// 向后端继续传递
func ForwardPkt():
pass
// 清理 WinSet 中所有 ≤ Gseq 的过期序列号
func CleanExpiredWinSet():
temp = empty list
for seq in WinSet:
if seq <= Gseq:
temp.append(seq)
for seq in temp:
WinSet.remove(seq)
SurplusCnt = SurplusCnt - 1
// 公共逻辑:批量判定丢包、跳过缺失序号并连续输出窗口内有序 seq
func SkipLostAndFlush():
while SurplusCnt >= T OR (AllMin - Gseq) > D2:
// 判定 Gseq + 1 所有副本丢失,主动跳过,丢就丢吧
Gseq = Gseq + 1
// 持续输出紧跟的连续缓存 seq
while (Gseq + 1) ∈ WinSet:
WinSet.remove(Gseq + 1)
ForwardPkt()
Gseq = Gseq + 1
SurplusCnt = SurplusCnt - 1
// 跳号后清理过期条目
CleanExpiredWinSet()
// 报文接收,转发主入口
// s: 当前报文序列号,k: 报文所属路径编号
func RecvPkt(s: integer, k: integer):
// 更新单路径最大 seq 与全局最慢路径进度 AllMin
if s > PathMax[k]:
PathMax[k] = s
AllMin = min(PathMax)
// 分支1:序列号早于已连续输出上限,冗余旧副本直接丢弃
if s < Gseq:
return
// 分支2:刚好是下一个期望的连续报文
if s == Gseq + 1:
ForwardPkt()
Gseq = s
// 输出窗口内连续可放行的 seq
while (Gseq + 1) ∈ WinSet:
WinSet.remove(Gseq + 1)
ForwardPkt()
Gseq = Gseq + 1
SurplusCnt = SurplusCnt - 1
CleanExpiredWinSet()
SkipLostAndFlush()
return
// 分支3:乱序报文 s > Gseq + 1
if s > Gseq + 1:
// 已存在则为冗余副本,丢弃
if s ∈ WinSet:
return
// HostReorderMode 为 true,仅放入 s seq,否则才放入整报文
WinSet.add(s)
SurplusCnt = SurplusCnt + 1
if HostReorderMode == true:
ForwardPkt()
SkipLostAndFlush()
return
我们需要根据现网的实际丢包率,乱序率,丢包影响体验的阈值分析来进一步调节参数,使算法迭代进化。但从算法本身卡门,几乎没有任何约束,自然结果就是与发送端行为天然解耦,稍后会看到它是可以处理随机 spraying 的。
HostReorderMode 是一个让算法更加松散的配置,如果业务自己都不关注保序和可靠了,我一个传输层何必多此一举呢。
看发送行为:
// 发送端静态配置
const M: integer // 单报文冗余副本数量
const N: integer // 隧道总路径条数
var SendSeq: integer = 0 // 发送端全局单调序列号,初始0
// 原始业务报文入隧道入口
func EnqueueOrgPkt(orgPkt):
// 1. 分配全局单调递增序列号
curSeq = SendSeq
SendSeq = SendSeq + 1
// 2. 复制M份冗余副本
copyList = empty list
for i from 0 to M-1:
tunnelPkt = WrapTunnelHeader(orgPkt, curSeq)
copyList.append(tunnelPkt)
// 3. 将M个副本分发到N条路径(轮询分发策略)
pathIdx = 0
for pkt in copyList:
SendToPath(pathIdx, pkt)
pathIdx = (pathIdx + 1) % N
// 封装隧道头部,填充序列号
func WrapTunnelHeader(rawPkt, seq):
tunnelPkt = new TunnelPacket()
tunnelPkt.payload = rawPkt
tunnelPkt.seq = seq
return tunnelPkt
// 将隧道报文下发至指定路径发送队列
func SendToPath(pathId, tunnelPkt):
// 底层硬件/驱动发送逻辑,此处占位
pass
既然接收端无约束,发送端便可扩展,它竟然可任意实现随机 spraying:
for pkt in allList:
randPath = Random(0, N-1)
SendToPath(randPath, pkt)
接收端将 T,D2 设高一些即可兼容随机 spraying。
这基本就是一个松弛传输协议的大概了,信念就是不在一个统计环境做任何确定性保证的事情,相信概率的解就是最优解,不保证,不死磕。
一个松弛的传输层,无论站在什么立场都是好的。
以 TCP 为代表的传统传输层,并非真正的端到端协议。一个真正的拥塞控制机制要做的是识别真正的端到端,一方面要确保对网络不过度注入,另一方面对业务要减少数据生成量,而 TCP 以及编程的人显然忽视了后者。
TCP 缺乏对应用层的反馈,那么确定性的代价就是它自身的抖动。明明网络已经拥塞,已经 AIMD 退避了,可数据只是挤压在 socket buffer,拥塞换了个位置罢了,同理,BBR 的 ProbeRTT 会造成业务 buffer 更加严重的拥塞,而悖论就是抖动多数恰由拥塞控制本身所造成,这便是分层信息不沟通的结果。
为解决传输层协议对端到端拥塞控制缺乏共识的问题,唯一的方式就是松弛化任何影响时延的确定性保障,让业务自身去处理,如此避免拥塞处理不同步造成的抖动和震荡,而确定性多来自重传以及多数人并不懂的带宽,损害的是网络传输唯一的指标,时延抖动。
裤衩子锁着边儿,而且绣着花儿。
高编码率的音视频流需要更大带宽承载,但它带给人的体验提升的收益是递减的,可是成本却很高,你要为此花更多钱,否则你就要承受抖动卡顿,直到编码器用更低的码率适配更低的带宽,然而时延抖动却是即刻的体验。
这就跟并不是东西越贵越好一样,贵更多是一种溢价,而不是使用价值,给人带来的情绪价值,带宽也一样,但好的即刻稳定的时延体验,一定需要支付些什么来交换,那便是松弛的协议实现。
浙江温州皮鞋湿,下雨进水不会胖。
1151

被折叠的 条评论
为什么被折叠?



