在传输优化领域工作的最后一年写过一类 inflight\text{inflight}inflight 守恒的拥塞控制算法,它完全基于 带宽时延\dfrac{带宽}{时延}时延带宽 做调整,确保低时延,这类算法有一系列实现,我在前年到前段时间陆陆续续展示过几个,有的复杂,有的简单,但本质上它们都是一系列负反馈。
本文从 Kelly-NUM\text{Kelly-NUM}Kelly-NUM 优化的视角完整论述一个 inflight\text{inflight}inflight 守恒算法,以及针对该算法原始实现的一个优化,看看 AIMD\text{AIMD}AIMD 如何将 buffer\text{buffer}buffer 收敛在一个恒值,顺便展示,做一个完整的拥塞控制算法的过程,它远不只是把参数调激进那么简单。
buffer\text{buffer}buffer 收敛的本质是操作点要找对,该操作点是最优的,即 Kleinrock's optimal point\text{Kleinrock's optimal point}Kleinrock's optimal point,于 197919791979 年被提出,被视做膝点,也就是 BBR\text{BBR}BBR 可遇而不可达的点,而此后 Loss-based\text{Loss-based}Loss-based 的 AIMD\text{AIMD}AIMD 工作点被称作崖点。
后来 Jeffrey M. Jaffe\text{Jeffrey M. Jaffe}Jeffrey M. Jaffe 证明,在分布式网络里,各节点只看本地信息无法收敛到这个最优操作点,所以文献里一般不会说实现了 Kleinrock's optimal point\text{Kleinrock's optimal point}Kleinrock's optimal point,我也没有实现,只是逼近,但我的方式显然更漂亮些。
现在基于 NUM\text{NUM}NUM 框架论述它。
原始 Kelly-NUM\text{Kelly-NUM}Kelly-NUM 仅以带宽对数效用度量收益,那是 199019901990 年代,而如今的 online\text{online}online 时代更在意的时延。
换一种度量,即 I=带宽时延I=\dfrac{带宽}{时延}I=时延带宽,从好多年前开始,我基于这个式子做过好几个算法,现在用 NUM\text{NUM}NUM 论证并重构它。
由于 III 无法保证严格凹,需要用一个严格凹的函数等价它的涵义,于是采用:
U=带宽收益−时延惩罚U=带宽收益−时延惩罚U=带宽收益−时延惩罚
在保留凸优化性质的同时引入时延感知:
Ui(x)=logxi−α⋅Di(x)U_i(x) = \log x_i-\alpha \cdot D_i(x)Ui(x)=logxi−α⋅Di(x)
其中 iii 为流 iii 发送速率,logxi\log x_ilogxi 为带宽的效用,对应比例公平的基准,Di(x)D_i(x)Di(x) 为流 iii 的排队时延,α>0\alpha>0α>0 表示时延惩罚权重,调节系统吞吐与时延间的权衡。
设网络有 LLL 条链路,链路 lll 容量 clc_lcl,路由矩阵 Rl,i=1R_{l,i}=1Rl,i=1 表示流 iii 经过链路 lll,链路 lll 总负载 Xl=∑iRl,ixiX_l=\sum_i R_{l,i}x_iXl=∑iRl,ixi,采用 M/M/1\text{M/M/1}M/M/1 模型建模,虽然我知道是不符合现实的,但它简单,时延表示为:
Dl(Xl)=1cl−Xl,Xl<clD_l(X_l) = \dfrac{1}{c_l - X_l},\quad X_l < c_lDl(Xl)=cl−Xl1,Xl<cl
所有流共享排队时延,端到端时延为路径上各链路时延之和。
NUM\text{NUM}NUM 问题可表示为:
maxx≥0∑i=1nlogxi−α∑l=1LDl(Xl) s.t.∑iRl,ixi≤cl,∀l\begin{aligned} \max_{\boldsymbol{x}\ge \boldsymbol{0}} \quad & \sum_{i=1}^n \log x_i - \alpha \sum_{l=1}^L D_l(X_l) \ \text{s.t.} \quad & \sum_i R_{l,i}x_i \le c_l,\quad \forall l \end{aligned}x≥0maxi=1∑nlogxi−αl=1∑LDl(Xl) s.t.i∑Rl,ixi≤cl,∀l
∑logxi\sum\log x_i∑logxi 严格凹,Dl(Xl)D_l(X_l)Dl(Xl) 二阶导 Dl′′(Xl)=2(cl−Xl)3>0D_l''(X_l) = \dfrac{2}{(c_l-X_l)^3}>0Dl′′(Xl)=(cl−Xl)32>0,故严格凸,因此目标函数严格凹,约束集 x∣Rx≤c,x≥0x\mid Rx\le c,x\ge 0x∣Rx≤c,x≥0 为线性不等式定义的凸集,凸可行域上最大化严格凹函数,最优解唯一。
引入链路拉格朗日乘子 pl≥0p_l\ge0pl≥0 表示带宽成本,构造函数:
L(x,p)=∑ilogxi−α∑lDl(Xl)+∑lpl(cl−∑iRl,ixi)\mathcal{L}(x,p) = \sum_i \log x_i - \alpha\sum_l D_l(X_l) + \sum_l p_l\left(c_l - \sum_i R_{l,i}x_i\right)L(x,p)=∑ilogxi−α∑lDl(Xl)+∑lpl(cl−∑iRl,ixi)
对 xix_ixi 求偏导并令其为 000,得到一阶最优条件:
1xi∗=∑l:i∈l(pl∗+α⋅Dl′(Xl∗))\dfrac{1}{x_i^*} = \sum_{l: i\in l} \left( p_l^* + \alpha \cdot D_l'(X_l^*) \right)xi∗1=∑l:i∈l(pl∗+α⋅Dl′(Xl∗))
其中 Dl′(Xl)=1(cl−Xl)2D_l'(X_l) = \dfrac{1}{(c_l-X_l)^2}Dl′(Xl)=(cl−Xl)21 为排队时延的边际增量,流 iii 的路径总边际成本由带宽成本和时延边际惩罚组成。
看互补松弛条件:
pl∗⋅(cl−∑iRl,ixi∗)=0,pl∗≥0p_l^* \cdot \left(c_l - \sum_i R_{l,i}x_i^*\right) = 0,\quad p_l^*\ge0pl∗⋅(cl−∑iRl,ixi∗)=0,pl∗≥0
链路未饱和时,∑Rl,i(x)i∗<cl\sum R_{l,i}(\boldsymbol{x})i^* < c_l∑Rl,i(x)i∗<cl,则 pl∗=0p_l^*=0pl∗=0,仅有时延边际代价,而链路饱和时 ∑Rl,i(x)i∗=cl\sum R{l,i}(\boldsymbol{x})_i^* = c_l∑Rl,i(x)i∗=cl,则 pl∗>0p_l^*>0pl∗>0,两个代价共同生效。
最优条件可表述为边际收益等于路径总边际成本。
在单瓶颈场景,所有流共享同一路径成本,故 1xi∗\dfrac{1}{x_i^*}xi∗1 相等,多瓶颈场景则满足广义比例公平,对任意可行扰动 Δx\Delta xΔx,有:
∑iΔxixi∗≤α∑lDl′(Xl∗)ΔXl\sum_i \dfrac{\Delta x_i}{x_i^*} \le \alpha \sum_l D_l'(X_l^*)\Delta X_l∑ixi∗Δxi≤α∑lDl′(Xl∗)ΔXl
公平权重由带宽成本与时延代价共同决定。
现在映射到工程算法。
理想情况下,每条链路维护本地成本 pl+αDl′(Xl)p_l + \alpha D_l'(X_l)pl+αDl′(Xl),并将其标记在数据包中,端侧累加成 Pi=∑l∈i(pl+αDl′)P_i = \sum_{l\in i} (p_l + \alpha D_l')Pi=∑l∈i(pl+αDl′),按 1xi≷Pi\dfrac{1}{x_i}\gtrless P_ixi1≷Pi 执行调整,这严格匹配 KKT\text{KKT}KKT 条件,但哑网络并不支持反馈,无法在互联网部署,但却可以在数据中心自研,就又卷出一篇论文。
仅考虑端到端场景,端侧仅能通过测量可观测值作为等价成本:
- 丢包事件,对应链路饱和,pl>0p_l>0pl>0,等价带宽成本;
- 排队时延 Dque=RTT−τpropD_{\text{que}} = \text{RTT} - \tau_{\text{prop}}Dque=RTT−τprop,对应时延边际代价 Dl′D_l'Dl′;
其中 τprop\tau_{\text{prop}}τprop 为传播时延基线。
将其映射为类似 TCP\text{TCP}TCP 的窗口增减机制:
- 无丢包且排队时延低于阈值,窗口线性增加;
- 检测到丢包或排队时延超阈值,窗口按比例削减,引入递增惩罚函数 ϕ(Dque)\phi(D_{\text{que}})ϕ(Dque),使惩罚随时延增大而增强,匹配 D′D'D′ 随排队时延上升;
可部署的工程算法如下。
设拥塞窗口 www,速率 x=wRTTx=\dfrac{w}{\text{RTT}}x=RTTw。
加性增触发条件为无丢包,且 Dque<DthD_{\text{que}} < D_{\text{th}}Dque<Dth,更新规则:
w←w+αw \leftarrow w + \alphaw←w+α
乘性减阶段触发条件为检测到丢包,或 Dque≥DthD_{\text{que}} \ge D_{\text{th}}Dque≥Dth,更新规则:
w←w⋅11+β⋅DqueDthw \leftarrow w \cdot \dfrac{1}{1 + \beta \cdot \dfrac{D_{\text{que}}}{D_{\text{th}}}}w←w⋅1+β⋅DthDque1
其中 α,β\alpha,\betaα,β 为 AIMD\text{AIMD}AIMD 参数,DthD_{\text{th}}Dth 为拥塞时延阈值,DqueDth\dfrac{D_{\text{que}}}{D_{\text{th}}}DthDque 为归一化时延,作为递增惩罚函数,时延越高,分母越大,窗口削减越多,以对应 D′D'D′ 随排队时延快速上升:
{Dque=1c−X,Dl′(Xl)=1(cl−Xl)2⟹D′=Dque 2\left\{\begin{aligned}D_{\text{que}} &= \frac{1}{c-X}, \\D_l'(X_l) &= \frac{1}{(c_l-X_l)^2}\end{aligned} \right.\quad\Longrightarrow\quad D' = D_{\text{que}}^{\,2}⎩ ⎨ ⎧DqueDl′(Xl)=c−X1,=(cl−Xl)21⟹D′=Dque2
伪代码如下:
txt
# ==============================================
# 算法:Delay-Aware AIMD (DA-AIMD)
# 基于路线 A NUM 模型的端到端拥塞控制
# ==============================================
# 配置参数
# a: 加性增窗步长
# beta: 基础衰减系数
# D_th: 排队时延拥塞阈值
# w_min, w_max: 拥塞窗口上下限
# 状态变量
# w: 当前拥塞窗口
# RTT_smoothed: 平滑后的往返时延
# min_RTT: 观测到的最小 RTT
# loss_detected: 当前周期是否检测到丢包
# 初始化
w ← w_init
RTT_smoothed ← 初始测量RTT
min_RTT ← RTT_smoothed
loss_detected ← False
while True:
发送窗口内数据包,接收ACK
测量当前样本RTT_sample
min_RTT ← min(min_RTT, RTT_sample)
RTT_smoothed ← 平滑滤波(RTT_smoothed, RTT_sample)
D_que ← RTT_smoothed - min_RTT # 计算排队时延
检测当前周期是否出现丢包 → loss_detected
if loss_detected == False and D_que < D_th:
w ← w + a
else:
penalty_ratio ← 1 + beta * (D_que / D_th)
w ← w / penalty_ratio
loss_detected ← False # 重置丢包标记
w ← clamp(w, w_min, w_max)
算法完成,但仿真效果不好,究其原因,源自 M/M/1\text{M/M/1}M/M/1 模型中带宽录用率和时延是一对矛盾:
Dque=1C−XD_{\text{que}} = \dfrac{1}{C - X}Dque=C−X1
其中 CCC 是链路容量,XXX 是入速率。
要让 DqueD_{\text{que}}Dque 收敛到一个小值 DtargetD_{\text{target}}Dtarget,C−XC-XC−X 就收敛到固定大值,总速率 X=C−1DtargetX = C-\dfrac{1}{D_{\text{target}}}X=C−Dtarget1 必然小,若想 XXX 逼近 CCC,则 C−X→0C-X\to 0C−X→0,则 Dque→∞D_{\text{que}}\to\inftyDque→∞,时延会随负载指数增加,低时延稳定和满带宽利用率,在排队系统中天然矛盾。
盯着 I=带宽时延I=\dfrac{带宽}{时延}I=时延带宽,为表达它,不纠结形式,只有修改 UUU:
Ui(xi)=logxi−α⋅max(0, Dque(X)−Dtarget)2U_i(x_i) = \log x_i - \alpha \cdot \max\left(0,\ D_{\text{que}}(X) - D_{\text{target}}\right)^2Ui(xi)=logxi−α⋅max(0, Dque(X)−Dtarget)2
其中 X=∑jxjX = \sum_j x_jX=∑jxj 是链路总速率,仍基于 M/M/1\text{M/M/1}M/M/1 建模,Dque(X)D_{\text{que}}(X)Dque(X) 是总速率的单调凸函数。
对 xix_ixi 求导,得到边际效用:
dUidxi=1xi⏟带宽边际收益−2α⋅max(0, Dque−Dtarget)⋅Dque′(X)⏟时延边际代价\dfrac{\text{d}U_i}{\text{d}x_i} = \underbrace{\dfrac{1}{x_i}}{\text{带宽边际收益}} - \underbrace{2\alpha \cdot \max\left(0,\ D{\text{que}} - D_{\text{target}}\right) \cdot D'{\text{que}}(X)}{\text{时延边际代价}}dxidUi=带宽边际收益 xi1−时延边际代价 2α⋅max(0, Dque−Dtarget)⋅Dque′(X)
其中 Dque′(X)D'_{\text{que}}(X)Dque′(X) 是排队时延对总速率的导数,代表速率增加一点,时延上涨的幅度。
最优均衡条件:
dUidxi=0\dfrac{\text{d}U_i}{\text{d}x_i}=0dxidUi=0
欲达此处,最大化效用采用梯度上升准则,即速率变化率与边际效用成正比:
dxidt=k⋅dUidxi\dfrac{\text{d}x_i}{\text{d}t} = k \cdot \dfrac{\text{d}U_i}{\text{d}x_i}dtdxi=k⋅dxidUi
k>0k>0k>0 是步长系数。
代入边际效用表达式,就对应算法的两个分支,当 Dque≤DtargetD_{\text{que}} \le D_{\text{target}}Dque≤Dtarget,max(0,⋅)=0\max(0,\cdot)=0max(0,⋅)=0,惩罚项消失,边际效用 1xi>0\dfrac{1}{x_i}>0xi1>0,速率可持续减缓上升:
dxidt=kxi\dfrac{\text{d}x_i}{\text{d}t} = \dfrac{k}{x_i}dtdxi=xik
当 Dque>DtargetD_{\text{que}} > D_{\text{target}}Dque>Dtarget,惩罚项生效,边际成本随超调量快速上升,当超调足够大,边际成本超过边际收益,边际净效用变负,速率开始下降:
dxidt=k(1xi−2α⋅(Dque−Dtarget)⋅Dque′(X))\dfrac{\text{d}x_i}{\text{d}t} = k\left(\dfrac{1}{x_i} - 2\alpha \cdot (D_{\text{que}}-D_{\text{target}}) \cdot D'_{\text{que}}(X) \right)dtdxi=k(xi1−2α⋅(Dque−Dtarget)⋅Dque′(X))
每个 RTT\text{RTT}RTT 更新一次窗口 www,结合 xi=wiRTTx_i=\dfrac{w_i}{\text{RTT}}xi=RTTwi,将连续微分近似为单步差分,就得到算法的规则。
归一化时延误差来近似调整强度:
e=Dque−DtargetDtargete=\dfrac{D_{\text{que}}-D_{\text{target}}}{D_{\text{target}}}e=DtargetDque−Dtarget
DqueD_{\text{que}}Dque 越低,eee 越负,1−e1-e1−e 越大,增窗越大,对应低时延的激进带宽探测,但接近目标时 e→0e\to0e→0,增窗步长收敛到基准值 α\alphaα,对应算法步骤的增窗:
w←w+a⋅(1−e)⋅RTTRTTbasew \leftarrow w + a \cdot (1-e) \cdot \dfrac{\text{RTT}}{\text{RTT}_{\text{base}}}w←w+a⋅(1−e)⋅RTTbaseRTT
若 e>0e>0e>0,边际代价随超调量 Dque−DtargetD_{\text{que}}-D_{\text{target}}Dque−Dtarget 递增,减窗幅度与超调量成正比,连续负梯度对应离散乘性衰减:
w←w⋅(1−β⋅min(e,1))w \leftarrow w \cdot (1-\beta \cdot \min(e, 1))w←w⋅(1−β⋅min(e,1))
超调量越大,边际惩罚上升越快,减窗越狠。
伪代码如下:
txt
# ==============================================
# 算法:FP-DTA-AIMD
# 基于严格凹 NUM 框架,兼顾时延定值与比例公平
# ==============================================
# 配置参数
# a: 基准增窗步长
# beta: 超调衰减增益
# D_target: 目标排队时延
# RTT_base: 基准 RTT,用于归一化
# w_slow: 新流加速阈值
# w_min, w_max: 窗口上下限
w ← w_init
RTT_smoothed ← 初始测量值
min_RTT ← RTT_smoothed
loss_detected ← False
while True:
min_RTT ← min(min_RTT, RTT_sample)
RTT_smoothed ← 平滑滤波(RTT_smoothed, RTT_sample)
D_que ← RTT_smoothed - min_RTT
e ← (D_que - D_target) / D_target
检测丢包 → loss_detected
if loss_detected: # TODO 1
w ← w * 0.5
loss_detected ← False
else if e <= 0:
increment ← a * (1 - e) * (RTT_smoothed / RTT_base)
if w < w_slow and D_que < 0.5 * D_target:
increment ← increment * 2 # 新流加速 TODO
w ← w + increment
else:
penalty ← beta * min(e, 1.0)
w ← w * (1 - penalty)
w ← clamp(w, w_min, w_max)
裤衩子锁着边儿,而且绣着花儿。
仿真效果图,以下三组分别使用了不同参数,可见一个真矛盾,收敛速度,时延,带宽利用率之间又形成一个不可能三角,可取舍择优,但不可兼得:

挺好的,但相比标准 Loss-based AIMD\text{Loss-based AIMD}Loss-based AIMD 也确实更复杂。
在并不 care\text{care}care 低时延的 offline\text{offline}offline 时代,下载快才重要,选择崖点做操作点就是最优的,只需要 111 个 bit\text{bit}bit 信息就完美收敛,没有比这更极简的了,而当年如果 Delya-based Vegas\text{Delya-based Vegas}Delya-based Vegas 再稍微复杂一点,加点梯度强度而不是分区调整, 便就是本文如此了。
值得一提的是,本文的算法步骤一定要连续,不能分区,否则会破坏 NUM\text{NUM}NUM 推导过程的连续性假设,偏离了 NUM\text{NUM}NUM 框架严格的对偶梯度迭代结构。非连续的分区破坏了所有流按统一边际准则调整速率的公平性基础,导致公平性问题。
时间来到 201020102010 年代,移动设备促进的低存储 onlineonlineonline 时代让低时延变得重要,而不再是大设备大存储的下载,而经理们还在学术区分拥塞控制的类型,实际上不同的算法只是操作点的转移,哪个更加重要而已,经理拿崖点开刀,优化低时延,终究徒劳。
浙江温州皮鞋湿,下雨进水不会胖。