声明:本文我并非表达这样的观点,即 “激进发包,就可以做出很好的协议”,我只是为想这么做的人提供一个如何这么做的方法。我说这样的算法是“快”的,因为它确实是快的,我并没有说它是“好”的,对于它的代价,我也提到了,但并不多说。
最快的 TCP 拥塞控制算法就是去掉拥塞控制算法。
给出最牛 X 的拥塞控制算法前,先交代背景。
先看 TCP 拥塞状态机的必要性。
为确保拥塞控制作用下界,防止拥塞控制算法违宗旨,从而加重拥塞或破坏公平,TCP 不得不收回一些拥塞控制权限,以悬崖勒马。比方说:
so?想维持硬编码的大 cwnd 是不可能的。巨大 cwnd 意味着激进传输,丢包后依然激进重传,这不是拥塞控制,这是制造拥塞。TCP 拥塞状态机阻止了拥塞控制算法的如此行为,它在你无法控制的逻辑里强制将 cwnd 拉低,这是高尚的。
之前说过 BBR 改变了这一切。
为使 BBR 靠采集 Delivery Rate 判断拥塞,不得不给 BBR 更多控制权,对丢包进行另一种解释而避开拥塞状态机的动作 。
但至少对 Linux TCP 而言,拥塞状态机和拥塞控制算法是分离的,为支持 BBR,不得不重构拥塞状态机实现,引入全权接管算法的 cong_control 回调函数,简而言之,一切都在该回调中完成。
对于 4.9 以上版本内核,终于可以写一个“固定 cwnd”的算法了,这将是最快的算法:
#include
#include
static u32 const_cwnd = 1000;
module_param(const_cwnd, uint, 0664);
static void const_main(struct sock *sk, const struct rate_sample *rs)
{
tcp_sk(sk)->snd_cwnd = const_cwnd;
}
static void const_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
tcp_sk(sk)->snd_cwnd = const_cwnd;
}
static void const_init(struct sock *sk)
{
tcp_sk(sk)->snd_cwnd = const_cwnd;
}
static u32 const_ssthresh(struct sock *sk)
{
return const_cwnd;
}
static void const_set_state(struct sock *sk, u8 new_state)
{
if (new_state == TCP_CA_Loss) {
tcp_sk(sk)->snd_cwnd = const_cwnd;
}
}
static u32 const_undo(struct sock *sk)
{
return tcp_sk(sk)->snd_cwnd;
}
static struct tcp_congestion_ops tcp_const_cong_ops __read_mostly = {
.name = "const",
.undo_cwnd = const_undo,
.init = const_init,
.cong_control = const_main,
/*.cong_avoid = const_cong_avoid,*/
.ssthresh = const_ssthresh,
.set_state = const_set_state,
};
static int __init const_register(void)
{
return tcp_register_congestion_control(&tcp_const_cong_ops);
}
static void __exit const_unregister(void)
{
tcp_unregister_congestion_control(&tcp_const_cong_ops);
}
module_init(const_register);
module_exit(const_unregister);
MODULE_LICENSE("GPL");
在 4.9 内核之前,若要如此效果,非要 kprobe/systemtap 强行 hack,给一个 stap 脚本 setcwnd.stp:
#!/usr/local/bin/stap -g
// 使用方法:./setcwnd.stp 10000
%{
#include
%}
function _set_cwnd(skk:long, ptype:long, pconst_cwnd:long)
%{
struct sock *sk = (struct sock *)STAP_ARG_skk;
int const_cwnd = (int)STAP_ARG_pconst_cwnd;
struct tcp_sock *tp = tcp_sk(sk);
// 可设置更复杂过滤规则
if (htons(inet_sk(sk)->inet_dport) == 1234) {
tp->snd_cwnd = const_cwnd;
}
%}
probe kernel.function("tcp_write_xmit")
{
_set_cwnd($sk, 1, $1);
}
probe kernel.function("tcp_xmit_recovery")
{
_set_cwnd($sk, 0, $1);
}
否则,把上面代码的注释打开,换把 cong_control 回调注释掉,设置的 const_cwnd 是维持不住的。
效果不展示,自己试,25 Gbps 带宽,丢包率调到 25% ,BBR/CUBIC 已经憋死,const 依然可保持 24 Gbps 无损带宽。Makefile 如下,直接make:
obj-m += tcp_const.o
all:
make -C /lib/modules/`uname -r`/build M=`pwd` modules
使能命令:
sysctl -w net.ipv4.tcp_congestion_control=const
调整 cwnd 命令:
echo 50000 >/sys/module/tcp_const/parameters/const_cwnd
终极问题,可实用吗?
拥塞控制本即社会博弈,const 算法本质上是 “取消了拥塞控制”,预期任何使用者均不会设置一个保守 cwnd。
但除重传代价极大外,有另一个阻止部署 const 算法的因素,“所有参与方都付出大代价后,价值取向就反转了。”,流氓的淫威建立在大多数人总是退让基础上,不可能人人都是流氓,流氓只有一两个时,流氓才有力量。
不用担心,若不计带宽成本,少量人使用是毫无问题的。如今网络带宽承载力很高,即使最后一公里边缘带宽也不是凭一两条流能淹没的,何况运营商只是软限制超卖,就更别提骨干网了。因此:
可劲造。
很久以前我就想硬编码 cwnd,比如我有一条很牛的专线,又不想受到偶尔误码丢包对带宽的影响,但总是 hold 不住硬编码的 cwnd,因为除了在 cc 中以外,TCP 拥塞状态机也会设置 cwnd 想取消这些修改必须修改内核或采用 hack 手段。BBR 发布以后,做这件事成了可能。最近也有经理问到,所以就写了本文。不过还可以做得更好点,为每个连接硬编码一个 cwnd:增加一个 socket option,将这个硬编码的 cwnd 保存在每个 tcp_sock 对象里。
浙江温州皮鞋湿,下雨进水不会胖。