声明:本文我并非表达这样的观点,即
“激进发包,就可以做出很好的协议”,我只是为想这么做的人提供一个如何这么做的方法。我说这样的算法是“快”的,因为它确实是快的,我并没有说它是“好”的,对于它的代价,我也提到了,但并不多说。
最快的 TCP 拥塞控制算法就是去掉拥塞控制算法。
给出最牛 X 的拥塞控制算法前,先交代背景。
先看 TCP 拥塞状态机的必要性。
为确保拥塞控制作用下界,防止拥塞控制算法违宗旨,从而加重拥塞或破坏公平,TCP 不得不收回一些拥塞控制权限,以悬崖勒马。比方说:
* 发生丢包时接管算法,防止进一步拥塞。
* 对激进传输的后果实施不可违抗的惩罚。
so?想维持硬编码的大 cwnd 是不可能的。巨大 cwnd 意味着激进传输,丢包后依然激进重传,这不是拥塞控制,这是制造拥塞。TCP
拥塞状态机阻止了拥塞控制算法的如此行为,它在你无法控制的逻辑里强制将 cwnd 拉低,这是高尚的。
之前说过 BBR 改变了这一切。
为使 BBR 靠采集 Delivery Rate 判断拥塞,不得不给 BBR 更多控制权,对丢包进行另一种解释而避开拥塞状态机的动作 。
但至少对 Linux TCP 而言,拥塞状态机和拥塞控制算法是分离的,为支持 BBR,不得不重构拥塞状态机实现,引入全权接管算法的 cong_control
回调函数,简而言之,一切都在该回调中完成。
对于 4.9 以上版本内核,终于可以写一个“固定 cwnd”的算法了,这将是最快的算法:
#include <linux/module.h> #include <net/tcp.h> 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 <linux/tcp.h>
%} 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
算法的因素,“所有参与方都付出大代价后,价值取向就反转了。”,流氓的淫威建立在大多数人总是退让基础上,不可能人人都是流氓,流氓只有一两个时,流氓才有力量。
不用担心,若不计带宽成本,少量人使用是毫无问题的。如今网络带宽承载力很高,即使最后一公里边缘带宽也不是凭一两条流能淹没的,何况运营商只是软限制超卖,就更别提骨干网了。因此:
* 使用const算法不会制造拥塞,让你无愧。
* 使用const算法可以带来收益,让你无悔。
* 使用const算吧会增加重传率,让你知情。
可劲造。
很久以前我就想硬编码 cwnd,比如我有一条很牛的专线,又不想受到偶尔误码丢包对带宽的影响,但总是 hold 不住硬编码的 cwnd,因为除了在 cc
中以外,TCP 拥塞状态机也会设置 cwnd 想取消这些修改必须修改内核或采用 hack 手段。BBR
发布以后,做这件事成了可能。最近也有经理问到,所以就写了本文。不过还可以做得更好点,为每个连接硬编码一个 cwnd:增加一个 socket
option,将这个硬编码的 cwnd 保存在每个 tcp_sock 对象里。
浙江温州皮鞋湿,下雨进水不会胖。