简介本文是大厂名师Dog250在调试一些网络问题时的实战。希望读者通过阅读本文,了解高手们是如何“不择手段,利用一切手头的便利,以最快的速度攻击问题的关键点”的,从而实现快速调试和解决问题。我们在工作中总会遇到一些棘手的问题,需要尽快解决。解决此类问题往往有一套常规思路可循,但实际实施往往耗时且依赖于外部环境。更难的是,为了按照步骤高效地完成工作,需要学习大量的前置知识,比如相关工具的使用。我倾向于用最少的工作量来做POC。不会用crash/ebpf就不能调试内核?不会编程就不能优化系统吗?一点也不。下面我就让大家看看在县城摆摊修雨伞的二胡师傅和瑞士宫廷制表师的区别。在本文中,我将给出三个实际示例。都是低爆的过时玩意儿。###例1:TCP死连接排查netstat显示一个TCP连接的Send-Q积累了很多数据,但是对端对应的Recv-Q为0,tcpdump显示连接继续没有任何相互作用。这个时候怎么办?通过ss-it确认tcp_info信息后,结论是连接的RWND/CWND、RTT、RTO、MSS等数据正常,网卡没有相关的错误统计,实际上只是卡住了。这是一种异常现象。由于Send-Q中有数据,它必须*try***无论如何都要发送它。几乎可以肯定,有两个原因:-应用程序锁定套接字并在进入系统调用时阻塞它。-内核有问题。如何确认?大多数人倾向于使用crash工具来分析内核数据结构,但这是一个庞大的静态分析工程。我倾向于驾驶飞机和修理引擎,我不擅长分析死亡,但我擅长复苏。我的方法是尝试复苏TCP。TCP的传输一直由ACK时钟驱动。事实上,直到BBR开启了基于pacing的新TCP时代,ACK时钟才被抛弃。虽然从根本上讲不再需要ACK,但是BBR仍然用它来计算pacingrate,如果没有ACK到达,那么pacingrate会逐渐降为0,TCP会冻结。***所以TCP的恢复操作主要是构造一个ACK来命中它!***如果你对TCP足够了解,那么你一定会对我的方法赞不绝口。要在TCP连接中Send-Q积累的数据的发送端构造这个ACK,需要从本地网卡注入。为了避免路由子系统的火星消息验证,需要设置另一个网络命名空间来做这个。接下来构造这个ACK:为了最快的定位问题,我经常不遵循任何编码规范,所以地址和端口我就写死了,就算要改,我也会编辑代码再次。然后我们来注入:注意我们不知道代码中的seq和ack字段。如何准确地将这个ACK??注入这个死掉的TCP连接?准确注射需要两个步骤。使用bpftrace脚本配合上述python代码获取TCP的snd_una、rcv_nxt等字段:注意我hook的是tcp_rcv_established。我在执行第一步注入的时候,并没有进入这个trace。几乎可以肯定是应用锁住了连接,然后将ACK排队到backlog中进行Deferred处理,这种情况需要应用开发者接手。如果trace进入成功,那么我们就已经获取到了TCP连接的info信息,接下来我们可以利用打印出来的snd_una,rcv_nxt信息在python代码中填充seq和ack:ACK结构配合bpftrace脚本,这样我们就可以一路跟踪数据的发送逻辑,进而定位死发送的原因。我已经给出了核心思想。本文不是个案分析,就没有继续下去的必要了。顺便说一句,我不喜欢用bpftrace,太麻烦,限制太多,还是systemtap好用,尤其是-g选项。bpftrace不需要编译,执行速度快也不是不可或缺的优势。大家用bpftrace比较多,因为它很潮。###实例2:实现tun网卡的readv最近虽然golang实现的tunUDP隧道的总吞吐量接近物理网卡限制25Gbps,但是对于单流吞吐量,一直无法要突破2-3Gbps,所以我想找出瓶颈在哪里。其实在允许IP分片的情况下,我把tun的MTU设置为8000,单流吞吐量可以达到8Gbps。然而,在有数据包丢失的长途线路上,IP碎片(碎片丢失会导致TCP时钟停顿)可能会降低TCP性能并破坏BBR所依赖的步速保真度。根据之前的测量结果,在直连环境下,通过tunUDP隧道的ping延迟是物理网卡ping延迟的10倍。那么tun和UDPsocket处理的系统消耗会消耗初始吞吐量的10倍左右,25Gbps下降到2~3Gbps是合理的。所以我需要减少tun的读/写开销。批量读写是个合理的思路,比如io_uring、readv/writev等,但是tun不支持这些,怎么办?io_uring直接丢弃,太复杂了。如果我要实现一个完整的读写数据包的readv/writev,需要在内核态和用户态都实现数据包边界的拆包和分组,需要处理各种协议才能整体获得memory数据包的长度并将其截断,我得时刻注意内存的边界,想办法把不连续的内存组合成看似连续的内存,以便后续的加解密goroutine处理它们.这个看起来很复杂,需要修改整个程序(当然,这对标准的程序员来说根本不是问题,但对我来说,就很可怕了),至少需要一整天的时间。可以作为业余的东西,我每天很晚才回家,所以我没有时间担心这个。以下是我如何在一小时内完成所有工作。我改变了readv的语义:-每个iovec只存储一个skb的数据,下一个skb放在下一个iovec中。-返回复制成功的skbs个数,而不是复制数据的总字节数。下面是我对tun_do_read的改造:几行代码而已。是不是很简单。下面是对应的golang代码:下面是golang中的Readv:...###例3:实现松散的TCP语义,最后一个例子,我简单说一下。我想为直播服务提供一个松散的TCP传输协议,如何?什么是松散TCP?很容易理解:-当网络状态良好或有轻微丢包时,执行完整的TCP逻辑。-拥塞严重时,不重传,直接发送后续数据。能不能到,就看缘分了。-接收方可以发送一个NAK来指示发送方是否应该重传。-...这对于直播来说是有意义的,体现在三个方面:-直播抗干扰体验比清晰度体验更核心。当发生严重拥塞时,用户可以接受模糊但不能卡顿,所以可以丢帧,但不要卡顿。-在严重拥塞期间对实时流量进行松散的非重传处理可以降低带宽成本。-大家不要拼命重传,说不定网络拥堵会过去,良性的全局同步是可以期待的。既可以优化体验,又可以降低成本,何乐而不为呢?那么如何实施呢?开会立项,确定期限,然后大改TCP协议的实现代码?Linux内核中TCP的大段代码,简直让人看疯了。谁能改变它?那么可期的就是开会、延期、加班,快乐在哪里?所以我使用Netfilter:-发送方使用Netfilter在IP层拦截传出的TCP报文段,并在拥塞严重时伪造ACK回复。-接收端在IP层使用Netfilter拦截传入的TCP报文段,在严重拥塞时用0填充丢包和乱序造成的序列空洞。不依赖于TCP本身的实现吗?而且实施起来很快,唱着唱着就可以写了。先推0.1版本上去,业务喜欢,再慢慢改TCP代码。本文转载自微信公众号“Linux代码阅读领域”,可通过以下二维码关注。转载本文请联系Linux代码阅读领域公众号。
