0

    由STGW下载慢问题引发的网络传输学习之旅

    2023.07.06 | admin | 143次围观

    导语:本文分享了笔者现网遇到的一个文件下载慢的问题。最开始尝试过很多办法,包括域名解析,网络链路分析,AB环境测试,网络抓包等,但依然找不到原因。然后利用网络命令和报文得到的蛛丝马迹,结合内核网络协议栈的实现代码,找到了一个内核隐藏很久但在最近版本解决了的BUG。如果你也想了解如何分析和解决诡异的网络问题,如果你也想温习一下课堂上曾经学习过的慢启动、拥塞避免、快速重传、AIMD等老掉牙的知识,如果你也渴望学习课本上完全没介绍过的TCP的一系列优化比如混合慢启动、尾包探测甚至BBR等,那么本文或许可以给你一些经验和启发。

    问题背景

    线上用户经过STGW(Secure Tencent Gateway,腾讯安全网关-七层转发代理)下载一个50M左右的文件,与直连用户自己的服务器相比,下载速度明显变慢,需要定位原因。在了解到用户的问题之后,相关的同事在线下做了如下尝试:

    1. 从广州和上海直接访问用户的回源VIP(Virtual IP,提供服务的公网IP地址)下载,都耗时4s+,正常;

    2. 只经过TGW(Tencent Gateway,腾讯网关-四层负载均衡系统),不经过STGW访问,从广州和上海访问上海的TGW,耗时都是4s+,正常;

    3. 经过STGW,从上海访问上海的STGW VIP,耗时4s+,正常;

    4. 经过STGW,从广州访问上海的STGW VIP,耗时12s+,异常。

    前面的三种情况都是符合预期,而第四种情况是不符合预期的,这个也是本文要讨论的问题。

    前期定位排查

    发现下载慢的问题后,我们分析了整体的链路情况,按照链路经过的节点顺序有了如下的排查思路:

    (1)从客户端侧来排查,DNS解析慢,客户端读取响应慢或者接受窗口小等;

    (2)从链路侧来排查,公网链路问题,中间交换机设备问题,丢包等;

    (3)从业务服务侧来排查,业务服务侧发送响应较慢,发送窗口较小等;

    (4)从自身转发服务来排查,TGW或STGW转发程序问题,STGW拥塞窗口缓存等;

    按照上面的这些思路,我们分别做了如下的排查:

    1.是否是由于异常客户端的DNS服务器解析慢导致的?

    用户下载小文件没有问题,并且直接访问VIP,配置hosts访问,发现问题依然复现,排除。

    2.是否是由于客户端读取响应慢或者接收窗口较小导致的?

    抓包分析客户端的数据包处理情况,发现客户端收包处理很快,并且接收窗口一直都是有很大空间。排除。

    3.是否是广州到上海的公网链路或者交换机等设备问题,导致访问变慢?

    从广州的客户端上ping上海的VIP,延时很低,并且测试不经过STGW,从该客户端直接访问TGW再到回源服务器没有默认网关会产生什么问题,下载正常,排除。

    4.是否是STGW到回源VIP这条链路上有问题?

    在STGW上直接访问用户的回源VIP,耗时4s+,是正常的。并且打开了STGW LD(LoadBalance Director,负载均衡节点)与后端server之间的响应缓存,抓包可以看到,后端数据4s左右全部发送到STGW LD上,是STGW LD往客户端回包比较慢,基本可以确认是Client->STGW这条链路上有问题。排除。

    5.是否是由于TGW或STGW转发程序有问题?

    由于异地访问必定会复现,同城访问就是正常的。而TGW只做四层转发,无法感知源IP的地域信息,并且抓包也确认TGW上并没有出现大量丢包或者重传的现象。STGW是一个应用层的反向代理转发,也不会对于不同地域的cip有不同的处理逻辑。排除。

    6.是否是由于TGW是fullnat影响了拥塞窗口缓存?

    因为之前由于fullnat出现过一些类似于本例中下载慢的问题,当时定位的原因是由于STGW LD上开启了拥塞窗口缓存,在fullnat的情况下,会影响拥塞窗口缓存的准确性,导致部分请求下载慢。但是这里将拥塞窗口缓存选项 sysctl -w net.ipv4.tcp_no_metrics_save=1 关闭之后测试,发现问题依然存在,并且线下用另外一个fullnat的vip测试,发现并没有复现用户的问题。排除。

    根据一些以往的经验和常规的定位手段都尝试了以后,发现仍然还是没有找到原因,那到底是什么导致的呢?

    问题分析

    首先,在复现的STGW LD上抓包,抓到Client与STGW LD的包如下图,从抓包的信息来看是STGW回包给客户端很慢,每次都只发很少的一部分到Client。

    这里有一个很奇怪的地方就是为什么第7号包发生了重传?不过暂时可以先将这个疑问放到一边,因为就算7号包发生了一个包的重传,这中间也并没有发生丢包,LD发送数据也并不应该这么慢。那既然LD发送数据这么慢,肯定要么是Client的接收窗口小,要么是LD的拥塞窗口比较小。

    对端的接收窗口,抓包就可以看到,实际上Client的接收窗口并不小,而且有很大的空间。那是否有办法可以看到LD的发送窗口呢?答案是肯定的:ss -it,这个指令可以看到每条连接的rtt,ssthresh,cwnd等信息。有了这些信息就好办了,再次复现,并写了个命令将cwnd等信息记录到文件:

    while true; do date +"%T.%6N" >> cwnd.log; ss -it >> cwnd.log; done

    复现得到的cwnd.log如上图,找到对应的连接,grep出来后对照来看。果然发现在前面几个包中,拥塞窗口就直接被置为7,并且ssthresh也等于7,并且可以看到后面窗口增加的很慢,直接进入了拥塞避免,这么小的发送窗口,增长又很缓慢,自然发送数据就会很慢了。

    那么到底是什么原因导致这里直接在前几个包就进入拥塞避免呢?从现有的信息来看,没办法直接确定原因,只能去啃代码了,但tcp拥塞控制相关的代码这么多,如何能快速定位呢?

    观察上面异常数据包的cwnd信息,可以看到一个很明显的特征,最开始ssthresh是没有显示出来的,经过了几个数据包之后,ssthresh与cwnd是相等的,所以尝试按照"snd_ssthresh ="和"snd_cwnd ="的关键字来搜索,按照snd_cwnd = snd_ssthresh的原则来找,排除掉一些不太可能的函数之后,最后找到了tcp_end_cwnd_reduction这个函数。

    再查找这个函数引用的地方,有两处:tcp_fastretrans_alert和tcp_process_tlp_ack这两个函数。

    tcp_fastretrans_alert看名字就知道是跟快速重传相关的函数,我们知道快速重传触发的条件是收到了三个重复的ack包。但根据前面的抓包及分析来看,并不满足快速重传的条件,所以疑点就落在了这个tcp_process_tlp_ack函数上面。那么到底什么是TLP呢?

    什么是TLP(Tail Loss Probe)

    在讲TLP之前,我们先来回顾下大学课本里学到的拥塞控制算法,祭出这张经典的拥塞控制图。

    TCP的拥塞控制主要分为四个阶段:慢启动,拥塞避免,快重传,快恢复。长久以来,我们听到的说法都是,最开始拥塞窗口从1开始慢启动,以指数级递增,收到三个重复的ack后,将ssthresh设置为当前cwnd的一半,并且置cwnd=ssthresh,开始执行拥塞避免,cwnd加法递增。

    这里我们来思考一个问题,发生丢包时,为什么要将ssthresh设置为cwnd的一半?

    想象一个场景,A与B之间发送数据,假设二者发包和收包频率是一致的,由于A与B之间存在空间距离,中间要经过很多个路由器,交换机等,A在持续发包,当B收到第一个包时,这时A与B之间的链路里的包的个数为N,此时由于B一直在接收包,因此A还可以继续发,直到第一个包的ack回到A,这时A发送的包的个数就是当前A与B之间最大的拥塞窗口,即为2N,因为如果这时A多发送,肯定就丢包了。

    ssthresh代表的就是当前链路上可以发送的最大的拥塞窗口大小,理想情况下,ssthresh就是2N,但现实的环境很复杂没有默认网关会产生什么问题,不可能刚好cwnd经过慢启动就可以直接到达2N,发送丢包的时候,肯定是N

    实际上,各个拥塞控制算法都有自己的实现,初始cwnd的值也一直在优化,在linux 3.0版本以后,内核CUBIC的实现里,采用了Google在RFC6928的建议,将初始的cwnd的值设置为10。而在linux 3.0版本之前,采取的是RFC3390中的策略,根据不同的MSS,设置了不同的初始化cwnd。具体的策略为:

    If (MSS snd_wl1)是基本都会命中的,因为不管窗口有没有变化,ack_seq都会比snd_wl1 大的,ack_seq都是递增的,snd_wl1在tcp_update_wl中又会被更新成上一次的ack_seq。因此绝大多数的包的flag都会被打上FLAG_WIN_UPDATE标记。

    如果是这样的话,那is_tlp_dupack不就是都为false了吗?不管有没有收到dup ack包,TLP都会进入拥塞避免,这个就不符合TLP的设计初衷了,这里是否是内核实现的Bug?

    随后我查看了linux 4.14内核代码:

    发现从内核版本linux 4.0开始,BUG就已经被修复了,去掉了flag的一些不合理的判断条件,这才是真正的符合TLP的设计原理。

    到此,整个问题的所有疑点才都得到了解释。

    总结

    本文从一个下载慢的线上问题入手,首先介绍了一些常规的排查思路和手段,发现仍然不能定位到原因。然后分享了一个可以查询每条连接的拥塞窗口命令,结合内核代码分析了TCP拥塞控制ssthresh的设计理念及混合慢启动,ER和尾包探测(TLP)等优化算法,并介绍了两个常用的内核调试工具:ftrace和systemtap,最终定位到是内核的TLP实现BUG导致的下载慢的问题,从内核4.0版本之后已经修复了这个问题。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论