继续填满长肥管道,接上回:
修正了TCP流控rwnd算法后,吞吐就与RTT和rcvbuff解耦了,它的值就是横截面:
buffer的目的是平滑到达和处理之间的gap而不再作为“要被填满的批量搜集站”,好处至少两点:
- 解决了rcvbuffer的bufferbloat,就平滑了主机处理延时抖动。
- 数据读取速率直接取决于CPU而不是rcvbuffer的配置。
但关于这个buffer,又有另一个问题。这个问题可能就是损失那2Gbps(理论25Gbps,实际23Gbps)的祸首。
之前说过,Linux TCP非全双工,socket读写不能(多CPU)并行,ACK和触发发送不能并行,除了这些,软中断Data收和进程socket收也不能并行。
换句话说软中断和socket无法同时将数据放入receive queue和从中取走数据。
无论TCP软中断处理还是socket收,都直接lock或own整个socket,造成软中断和socket进程的串行处理。具体参考Linux内核协议栈的backlog实现。
lock或own整个socket显然粒度太粗,需要lock的只是TCP连接的元数据。
Linux UDP之前也有这个问题,但UDP本身无状态,也就元数据需要保护,针对Linux UDP的优化一气呵成,可参见2019年的一篇文章:
给出一个UDP双队列优化后的图示:
Linux TCP也需要类似的双队列buffer,两个队列原子转移,深入细致地管理这个双队列buffer,目的是软中断往buffer放数据的同时,socket可在另一个CPU上从buffer取数据。
这才真正实现TCP协议栈到进程并行流式交接,buffer作为平滑适配器而不是批量搜集站,这是真正的排队系统,socket就像网络中继转发节点,将数据从TCP转交给进程。
如昨天文中所说,进程不一定是最后一站,可能还要落盘,IO库,文件系统,磁盘buffer也依然要如是转交。
话回正途,Linux TCP拆解这个socket lock范围很难,我尝试将receiveve queue独立出来,但socket读也要独立才行,最终失败。
退而求其次,在socket读频率和每次读取的数量之间找一个平衡点。SO_LOWAT正好做这事,但iperf工具不支持该sockopt,可以用LD_PRELOAD来做,也可以用stap:
#!/usr/local/bin/stap -g
%{
#include <net/tcp.h>
%}
function alter_lowat(skk:long)
%{
struct sock *sk = (struct sock *)STAP_ARG_skk;
if (inet_sk(sk)->inet_num == 5001 && sk->sk_rcvlowat == 1) {
sk->sk_rcvlowat = 65500*2;
STAP_PRINTF("%d\n", sk->sk_rcvlowat);
}
%}
probe kernel.function("tcp_data_ready")
{
alter_lowat($sk);
}
意思是buffer里至少攒够了65500*2字节数据后才wakeup进程。保持相同的速率,CPU利用率下降20%。
此外就是调节系统调度器了,我们追求的目标是软中断将n字节放入receive queue和socket从receive queue中读n字节这两件事交替进行,但这几乎做不到:
- GRO/LRO必须开启,ACK过多会抑制发送。
- GRO/LRO无法确定数据包长度。
- Linux软中断无法精确控制pacing。
- wakeup收包进程无法保证它立即运行。
不过25Gbps尚未touch到CPU瓶颈,若100Gbps打进主机,恐怕上述问题就必须考虑了,由于Linux内核本身是一个复杂的控制面,跑100Gbps注定会吃力,任何wakeup模式因调度延时不确定而不能信任,busy poll模式就更加适合。不过这些都是后话。
终于还是避不开TCP单机性能的损失,核心问题不是调度系统调优,如果真去那般做,可能路子本身就是错的,内核本是一个控制面,内核协议栈跑数据面尚且还好因为尚未touch到CPU瓶颈,但随着带宽持续演进,早晚会touch到CPU瓶颈。作一短文,备忘。
浙江温州皮鞋湿,下雨进水不会胖。