这个状况跟前面描述的recv buffer太小不一样,8K是很小,但是因为rtt也很小,所以server总是能很快就ack收到了,接收窗口也一直不容易达到full状态,但是一旦接收窗口达到了full状态,居然需要惊人的6秒钟才能恢复,这等待的时间有点太长了。这里应该是应用读取数据太慢导致了耗时6秒才恢复,所以最终这个请求执行会非常非常慢(时间主要耗在了上传SQL而不是执行SQL)。
实际原因不知道,从读取TCP数据的逻辑来看这里没有明显的block,可能的原因:
- request的SQL太大,Server(3306端口上的服务)从TCP读取SQL需要放到一块分配好的内存,内存不够的时候需要扩容,扩容有可能触发fgc,从图形来看,第一次满就卡顿了,而且每次满都卡顿,不像是这个原因
- request请求一次发过来的是多个SQL,应用读取SQL后,将SQL分成多个,然后先执行第一个,第一个执行完后返回response,再读取第二个。图形中卡顿前没有response返回,所以也不是这个原因。
- ……其它未知原因
接收方不读取数据导致的接收窗口满同时有丢包发生
服务端返回数据到client端,TCP协议栈ack这些包,但是应用层没读走包,这个时候 SO_RCVBUF 堆积满,client的TCP协议栈发送 ZeroWindow 标志给服务端。也就是接收端的 buffer 堆满了(但是服务端这个时候看到的bytes in fly是0,因为都ack了),这时服务端不能继续发数据,要等 ZeroWindow 恢复。
那么接收端上层应用不读走包可能的原因:
- 应用代码卡顿、GC等等
应用代码逻辑上在做其它事情(比如Server将SQL分片到多个DB上,Server先读取第一个分片,如果第一个分片数据很大很大,处理也慢,那么第二个分片数据都返回到了TCP buffer,也没去读取其它分片的结果集,直到第一个分片读取完毕。如果SQL带排序,那么Server。
- 会轮询读取多个分片,造成这种卡顿的概率小了很多
上图这个流因为应用层不读取TCP数据,导致TCP接收Buffer满,进而接收窗口为0,server端不能再发送数据而卡住,但是ZeroWindow的探测包,client都有正常回复,所以1903秒之后接收方窗口不为0后(window update)传输恢复。
这个截图和前一个类似,是在Server上(3003端口)抓到的包,不同的是接收窗口为0后,server端多次探测(Server上抓包能看到),但是client端没有回复 ZeroWindow(也有可能是回复了,但是中间环节把ack包丢了,或者这个探测包client没收到),造成server端认为client死了、不可达之类,进而反复重传,重传超过15次之后,server端认为这个连接死了,粗暴单方面断开(没有reset和fin,因为没必要,server认为网络连通性出了问题)。
等到1800秒后,client的接收窗口恢复了,发个window update给server,这个时候server认为这个连接已经断开了,只能回复reset。
网络不通,重传超过一定的时间(tcp_retries2)然后断开这个连接是正常的,这里的问题是:
为什么这种场景下丢包了,而且是针对某个stream一直丢包?
可能是因为这种场景下触发了中间环节的流量管控,故意丢包了(比如proxy、slb、交换机都有可能做这种选择性的丢包)
这里server认为连接断开,没有发reset和fin,因为没必要,server认为网络连通性出了问题。client还不知道server上这个连接清理掉了,等client回复了一个window update,server早就认为这个连接早断了,突然收到一个update,莫名其妙,只能reset。
接收窗口和SO_RCVBUF的关系初始接收窗口一般是 mss乘以初始cwnd(为了和慢启动逻辑兼容,不想一下子冲击到网络),如果没有设置SO_RCVBUF,那么会根据 net.ipv4.tcp_rmem 动态变化,如果设置了SO_RCVBUF,那么接收窗口要向下面描述的值靠拢。
初始cwnd可以大致通过查看到:
ss -itmpn dst "10.81.212.8" State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 0 0 10.xx.xx.xxx:22 10.yy.yy.yyy:12345 users:(("sshd",pid=1442,fd=3)) skmem:(r0,rb369280,t0,tb87040,f4096,w0,o0,bl0,d92) Here we can see this socket has Receive Buffer 369280 bytes, and Transmit Buffer 87040 bytes. Keep in mind the kernel will double any socket buffer allocation for overhead. So a process asks for 256 KiB buffer with setsockopt(SO_RCVBUF) then it will get 512 KiB buffer space. This is described on man 7 tcp.
初始窗口计算的代码逻辑,重点在18行:
/* TCP initial congestion window as per rfc6928 */ #define TCP_INIT_CWND 10 /* 3. Try to fixup all. It is made immediately after connection enters * established state. */ void tcp_init_buffer_space(struct sock *sk) { int tcp_app_win = sock_net(sk)->ipv4.sysctl_tcp_app_win; struct tcp_sock *tp = tcp_sk(sk); int maxwin; if (!(sk->sk_userlocks & SOCK_SNDBUF_LOCK)) tcp_sndbuf_expand(sk); //初始最大接收窗口计算过程 tp->rcvq_space.space = min_t(u32, tp->rcv_wnd, TCP_INIT_CWND * tp->advmss); tcp_mstamp_refresh(tp); tp->rcvq_space.time = tp->tcp_mstamp; tp->rcvq_space.seq = tp->copied_seq; maxwin = tcp_full_space(sk); if (tp->window_clamp >= maxwin) { tp->window_clamp = maxwin; if (tcp_app_win && maxwin > 4 * tp->advmss) tp->window_clamp = max(maxwin - (maxwin >> tcp_app_win), 4 * tp->advmss); } /* Force reservation of one segment. */ if (tcp_app_win && tp->window_clamp > 2 * tp->advmss && tp->window_clamp tp->advmss > maxwin) tp->window_clamp = max(2 * tp->advmss, maxwin - tp->advmss); tp->rcv_ssthresh = min(tp->rcv_ssthresh, tp->window_clamp); tp->snd_cwnd_stamp = tcp_jiffies32; }
传输过程中,最大接收窗口会动态调整,当指定了SO_RCVBUF后,实际buffer是两倍SO_RCVBUF,但是要分出一部分(2^net.ipv4.tcp_adv_win_scale)来作为乱序报文缓存。
1.net.ipv4.tcp_adv_win_scale = 2 //2.6内核,3.1中这个值默认是1。
如果SO_RCVBUF是8K,总共就是16K,然后分出2^2分之一,也就是4分之一,还剩12K当做接收窗口;如果设置的32K,那么接收窗口是48K。
static inline int tcp_win_from_space(const struct sock *sk, int space) {//space 传入的时候就已经是 2*SO_RCVBUF了 int tcp_adv_win_scale = sock_net(sk)->ipv4.sysctl_tcp_adv_win_scale; return tcp_adv_win_scale <= 0 ? (space>>(-tcp_adv_win_scale)) : space - (space>>tcp_adv_win_scale); //sysctl参数tcp_adv_win_scale }
接收窗口有最大接收窗口和当前可用接收窗口。
一般来说一次中断基本都会将 buffer 中的包都取走。
绿线是最大接收窗口动态调整的过程,最开始是146010,握手完毕后略微调整到147210(可利用body增加了12),随着数据的传输开始跳涨。