实现一个松弛的传输层应对随机

复杂来自确定性,复杂影响稳定性,对抗不确定性的方式就反而是松弛。网络是一个准随机的统计复用环境,为适应它,需要松弛。

最近看了准随机网络随机 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 更加严重的拥塞,而悖论就是抖动多数恰由拥塞控制本身所造成,这便是分层信息不沟通的结果。

为解决传输层协议对端到端拥塞控制缺乏共识的问题,唯一的方式就是松弛化任何影响时延的确定性保障,让业务自身去处理,如此避免拥塞处理不同步造成的抖动和震荡,而确定性多来自重传以及多数人并不懂的带宽,损害的是网络传输唯一的指标,时延抖动。

裤衩子锁着边儿,而且绣着花儿。

高编码率的音视频流需要更大带宽承载,但它带给人的体验提升的收益是递减的,可是成本却很高,你要为此花更多钱,否则你就要承受抖动卡顿,直到编码器用更低的码率适配更低的带宽,然而时延抖动却是即刻的体验。

这就跟并不是东西越贵越好一样,贵更多是一种溢价,而不是使用价值,给人带来的情绪价值,带宽也一样,但好的即刻稳定的时延体验,一定需要支付些什么来交换,那便是松弛的协议实现。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值