“小坝” 策略:始发站 buffer 控制与优化
端到端,这两个端是两个应用程序中的位置,第一个端指数据被产生处,第二个端指数据被消费处。更一般的,把数据发生的应用程序所在的主机视为数据始发站也是合理的。
网络中遍布 buffer,buffer 却是一把双刃剑的存在,它能吸收统计突发降低丢包率,但塞得太满也会带来高延时,如何配置 buffer 的大小以及如何控制 buffer 的用量一直都是网络传输领域的核心之一(另一个核心关乎拓扑)。
bufferbloat 就是一种 buffer 使用不当带来的高延时症状,但这种使用不当是无心错,因为 buffer 总有一种被填满的倾向,buffer 被填满似乎是一种博弈均衡的结果,唯一的措施几乎就是不要把 buffer 配置太大(当然,很多人不会同意)。
但在始发站,针对始发站的 buffer,却可以严格控制 buffer,避免 bufferbloat。
始发站的网络传输逻辑从 socket write 开始,在网卡 xmit 后结束,期间经过两个 buffer,一个在 socket 和 TCP(UDP 也一样) 之间,另一个在协议栈和网卡之间(比如 Qdisc),或许网卡本身还有个 buffer,但它不通用,故忽略。
确保这两个 buffer 保持低用量,即可确保始发站不会 bufferbloat。
以 Linux 协议栈为例,socket 写入 TCP 发送队列前有机制去检查当前发送队列的长度,当它大于一个阈值,意味着不能再继续写入,直到队列长度低于该阈值,该机制可通过 net.ipv4.tcp_notsent_lowat 配置起作用。比如将它配置成一个比较低的值,具体多少关乎第二个 buffer。该机制检测的代码如下:
static inline bool tcp_stream_memory_free(const struct sock *sk)
{const struct tcp_sock *tp = tcp_sk(sk);u32 notsent_bytes = READ_ONCE(tp->write_seq) - tp->snd_nxt;return notsent_bytes < tcp_notsent_lowat(tp);
}
TCP 为避免本地队列(比如 Qdisc)过长,采用了 small queue 控制机制,缺省情况下,该机制允许本地队列仅排入 1ms 的报文,该值就是 pacing_rate / 1000。当然,也可以配置一个更小的值:
/* TCP Small Queues :* Control number of packets in qdisc/devices to two packets / or ~1 ms.* (These limits are doubled for retransmits)* This allows for :* - better RTT estimation and ACK scheduling* - faster recovery* - high rates* Alas, some drivers / subsystems require a fair amount* of queued bytes to ensure line rate.* One example is wifi aggregation (802.11 AMPDU)*/
static bool tcp_small_queue_check(struct sock *sk, const struct sk_buff *skb,unsigned int factor)
{unsigned int limit;limit = max(2 * skb->truesize, sk->sk_pacing_rate >> sk->sk_pacing_shift);limit = min_t(u32, limit,sock_net(sk)->ipv4.sysctl_tcp_limit_output_bytes);limit <<= factor;if (refcount_read(&sk->sk_wmem_alloc) > limit) {/* Always send skb if rtx queue is empty.* No need to wait for TX completion to call us back,* after softirq/tasklet schedule.* This helps when TX completions are delayed too much.*/if (tcp_rtx_queue_empty(sk))return false;set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);/* It is possible TX completion already happened* before we set TSQ_THROTTLED, so we must* test again the condition.*/smp_mb__after_atomic();if (refcount_read(&sk->sk_wmem_alloc) > limit)return true;}return false;
}
注意函数注释最后一句,“呜呼,有些网络设备却需要更大量的排队数据以确保良好的吞吐”,这种 “大队列需求” 似乎与 small queue 相悖,于是 TSQ 提供了相应接口可以修改缺省配置:
/* We need a bit of data queued to build aggregates properly, so* instruct the TCP stack to allow more than a single ms of data* to be queued in the stack. The value is a bit-shift of 1* second, so 7 is ~8ms of queued data. Only affects local TCP* sockets.*/
sk_pacing_shift_update(skb->sk, 7);
说的就是 Wi-Fi 的情况,因为它涉及到帧聚合,就需要有足够多的报文来配合底层做聚合,这又是一个通过多层耦合来优化性能的例子,更详细的可参考 Adapting TCP Small Queues for IEEE 802.11 Networks。
现在,剩下的问题是如何根据 tsq 反推出 tcp_notsent_lowat 的配置以获得最佳吞吐延时比。先看 lowat 的取值:
static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{struct net *net = sock_net((struct sock *)tp);return tp->notsent_lowat ?: net->ipv4.sysctl_tcp_notsent_lowat;
}
显然取一个固定配置站在性能优化的视角是不合理的(但足够通用),它应该被实时计算出来,下面的函数会更好吗:
static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{struct sock *sk = (struct sock *)tp; struct net *net = sock_net(sk);unsigned int limit;int inc_dec = ...; // 考虑到传输层和 IP 层计算(如拥塞控制算法的开销)损耗的时间,这个值...limit = max(2 * skb->truesize, sk->sk_pacing_rate >> sk->sk_pacing_shift);limit = min_t(u32, limit,sock_net(sk)->ipv4.sysctl_tcp_notsent_lowat);limit += inc_dec;return limit;
}
这意味着在始发站构建了两道闸门构成的两座小坝。获得最佳吞吐延时比的途径就是维持这两座小坝处于低水位,快空了补,越界就截止,或许可以设置 low,high 水位来更平滑控制,但我知道,这或许就是 mptcp meta_sk 和 subflow 队列控制算法的答案。
现在到了形而上的时间,引自昨天发的朋友圈。
昨天写了一篇关于 mptcp 的随笔,格调依然如故,我不是说关于 CPU,内存,锁相关的主机优化技术没用,也从没有觉得 DPDK,XDP,eBPF 等通用技术没用(我自己在这些方面也是老手),我只是更关注网络性能本身,和 CPU 相比,即使数据中心网络传输也要慢至少一个数量级,真正的网络技术和主机根本就不在一个频道工作,这是两个领域,更何况即使是 DCN,我也从没把它当做网络看,只是看做主机总线的延伸。举个例子,即使我坐最快的飞机去里斯本,飞行的时间也比我在客厅卧室卫生之间兜兜转转慢几个数量级,我会更关注我在路上干什么,而不是纠结是先拉屎还是先收拾行李。
所以我更 concern 的是分布式一致性,博弈均衡,拥塞控制算法,流量调度策略,流量工程,社会工程学这些东西,这也正是我写的东西虽论技术,却根本不像技术文档的原因,如果不是你的菜,你可能根本不知道我的文章在说什么,完全学不到具体怎么做的技术,我也不解释,但如果是你的菜,看一眼就会觉得全是宝藏…
浙江温州皮鞋湿,下雨进水不会胖。