简介:最近一直在做数据库相关的事情,遇到了很多TCP相关的问题,新场景新挑战,还有很多之前没有把握透彻的点,让我大开眼界,选了几个案例分享。最近一直在做数据库相关的事情,遇到了很多TCP相关的问题,新场景新挑战,还有很多之前没有把握透彻的点,让我大开眼界,特选了几个案例来分享。情况一:TCP中并不是所有的RST都是有效的背景知识:在TCP协议中,包含RST标志的数据包用于异常关闭连接。它在TCP的设计中是必不可少的。在发送RST段关闭连接时,不必等待缓冲区中的数据全部发送完,直接丢弃缓冲区中的数据。接收端收到RST报文段后,不需要发送ACK确认。现象:客户在连接数据库时经常遇到连接中断的情况。但经过反复排查,后台数据库实例排查,并没有出现执行异常、崩溃等问题。发现了一个可疑点。在TCP交互过程中,客户端发送了一个RST(后来发现是客户端本地一些安全相关的iptables规则引起的),但是神奇的是这个RST并没有影响到TCP数据交互。双方愉快地忽略了RST,继续愉快地交换数据。然而,10秒后,连接突然中断。请参考以下抓包:关键点分析。TCP数据交互似乎没有受到任何影响。数据传输和ACK都正常。本轮数据交互结束后,TCP连接正常空闲一段时间。10秒后,连接突然被RST断开。这里有两个有意思的问题:在TCP数据交互过程中,一方发送RST后,连接会不会终止?是立即终止连接,还是等待10s查看RFC官方解释:简单的说,RST包不一定有效,除了TCP握手阶段,其他情况下RST包的Seq号必须在窗口。thisinthewindow其实从字面上很难理解。经过对Linux内核代码的辅助分析,确定其含义其实是指TCP——滑动窗口,准确的说是滑动窗口中的接收窗口。我们直接查看Linux内核的源代码。内核收到一条TCP报文后,进入如下处理逻辑:是否终止连接?A:不一定会被终止。需要检查RST的Seq是否在receiver的接收窗口内。在上面的例子中,由于Seq号很小,所以它不是合法的RST,被Linux内核忽略。.Q:连接是立即终止,还是等待10s?A:连接会立即终止。在上面的例子中,它将在10s后终止。正是由于Linux内核严格执行RFC,忽略了RST报文,但是客户端和数据库之间传递的SLB(云负载均衡设备)处理了RST报文,导致TCP连接在10s后关闭(SLB)会话在10秒后被清理)。这个案例告诉我们,把底层知识掌握透彻其实是非常有用的,否则一旦遇到问题,(自我证明,指向根本原因)就不知道往哪个方向查了。案例二:Linux内核中有多少可用的TCP端口背景知识:我们通常有一个常识,Linux内核中可用的端口号只有65535个,也就是说一台机器在不考虑多个网络的情况下最多只能开放65535个牌。TCP端口。但是经常看到单机上有上百万个TCP连接。怎么做?这是因为TCP使用四重(客户端IP+客户端端口+服务器IP+服务器端口)作为唯一的TCP连接。确定。如果作为TCP的服务器端,无论连接多少客户端,本地端口只需要占用相同的端口号即可。而如果作为TCP的client端,当连接的对端是同一个IP+Port时,确实每个连接都需要占用一个本地端口,但是如果连接的对端不是同样的IP+Port,那么本地其实是可以复制的。使用的是端口,所以其实linux中可用的端口有很多(只要四元组不重复)。问题现象:作为一个分布式数据库,每个节点都需要和其他每个节点建立TCP连接进行数据交换,那么假设有100个数据库节点,每个节点就需要100个TCP连接。当然,由于是多进程模型,实际上每次并发需要100个TCP连接。如果有100个并发,那么需要1W个TCP连接。但其实1W个TCP连接数并不算多。从前面介绍的背景知识可以知道,这还远远没有达到Linux内核的瓶颈。但是我们经常会遇到端口不够用的情况,也就是“bind:Addressalreadyinuse”:其实看到这里,很多同学已经猜到问题的重点了,经典的TCPtime_wait问题,大约TCPtime_wait的背景介绍和应对方法不是本文的重点,就不赘述了,大家可以自行了解。乍一看,系统中有一个50W的time_wait连接,端口号只有65535,肯定是不可用的:但是这个猜测是错误的!因为已经启用了系统参数net.ipv4.tcp_tw_reuse,所以不会因为time_wait问题出现上述现象。理论上,当net.ipv4.cp_tw_reuse开启后,只要对端IP+Port不重复,就可以使用有很多端口,因为每个对端IP+Port有65535个可用端口:问题分析有多少端口可以??在Linux中使用吗?为什么在tcp_tw_reuse的情况下端口还是不够linux可以使用多少个端口从有效使用理论来说,端口号是一个16位整数,总共可以使用65535个端口,但是linux操作系统有一个系统参数来控制端口号的分配:net.ipv4.ip_local_port_range我们知道在编写网络应用程序时有两种使用端口的方法:?方法一:显式指定端口号——通过bind()系统调用,为bind显式指定一个端口号,如bind(8080),然后执行listen()或connect()等系统调用,会使用应用程序在bind()中指定的端口号。?方法二:系统自动赋值——bind()系统调用参数传0,即bind(0),然后执行listen()。或者不调用bind(),直接connect(),此时Linux内核随机分配一个端口号,Linux内核会在net.ipv4.ip_local_port_range系统指定的范围内随机分配一个未被占用的端口范围。比如下面这种情况相当于1-20000是系统保留的端口号(除非按照方法1明确指定了端口号),自动分配时只会从20000-65535中随机选择一个端口,andwillnotuseaportlessthan20000端口:为什么在tcp_tw_reuse=1的情况下端口还是不够用。细心的同学可能已经发现,所有的错误信息都是bind()系统调用失败,而没有一条是connect()失败。在我们的数据库分布式节点中,所有的connect()调用(即作为TCP客户端)都是成功的,但是很多作为TCP服务器的bind(0)+listen()操作都不成功,报错信息是端口是不足的。由于我们在源码中使用了bind(0)+listen()方法(而不是绑定某个固定端口),即操作系统随机选择监听端口号,所以问题的根源就在这里。connect()调用仍会从net.ipv4.ip_local_port_range池中提取端口,但bind(0)不会。为什么,因为两个看似行为相似的系统调用,其底层实现行为并不相同。源码之前,没有秘密:bind()系统调用选择随机端口时,使用inet_csk_bind_conflict判断是否可用,排除time_wait状态连接的端口:而connect()系统调用选择随机端口port,就是用__inet_check_established来判断可用性,它不仅可以复用有TIME_WAIT连接的端口,还可以对有TIME_WAIT连接的端口做如下判断比较,判断是否可以复用:一张图总结一下:所以答案很明显是bind(0)和connect()冲突,ip_local_port_range的pool被connect()遗留下来的50Wtime_wait占用,导致bind(0)失败。知道了原因,修复方案就比较简单了。把bind(0)改成bind指定端口,然后在应用层维护一个pool,每次从pool中随机分配。总结Q:在Linux中可以有效使用多少个端口?A:Linux一共有65535个端口可用,其中ip_local_port_range范围可以由系统随机分配,其他需要指定绑定。只要TCP连接四元组不完全相同,同一个端口就可以无限重复使用。Q:为什么tcp_tw_reuse=1时端口还是不够用?A:connect()系统调用和bind(0)系统调用在随机绑定端口时有不同的选择限制,bind(0)会忽略有time_wait连接的端口。这个案例告诉我们,如果你对某个知识点比如time_wait,比如linux有多少个端口可用,知之甚少,但只知一二,很容易陷入思考的陷阱而忽略真正的根案例。你必须彻底掌握它。案例三:诡异的幽灵连接背景知识:TCP三次握手,SYN、SYN-ACK、ACK是大家耳熟能详的常识,但是说到Socket代码层面,这三者又是如何对应的呢?方式握手过程?现在,你可以看看下面这张图就明白了(来源:小林编码):这个过程的关键点在于,在Linux中,一般情况下,内核代理握手3次,也就是说,当你client调用connect()后,内核负责发送SYN,接收SYN-ACK,发送ACK。然后connect()系统调用返回,客户端握手成功。服务器端的Linux内核在收到SYN后会负责回复SYN-ACK,然后等待ACK才允许accept()返回,这样就完成了服务器端的握手。因此Linux内核需要引入半连接队列(用来存放已经收到SYN但还没有收到ACK的连接)和全连接队列(用来存放已经完成3次握手的,但是应用层代码还没有完成accept())Connection)握手中存储的连接的两个概念。问题现象:我们的分布式数据库在初始化阶段,每两个节点之间都会建立TCP连接,为后续的数据传输做准备。但是当节点数比较多时,比如320个节点,很容易卡在初始化阶段。经过代码追查,卡住的原因是发起TCP握手的一方成功完成了connect()动作,认为TCP已经建立。成功,但是TCPpeer没有握手成功,还在等待peer建立TCP连接,所以整个集群还没有初始化。重点分析:看了前面的背景介绍,聪明的小伙伴一定很好奇。如果我们上层的accpet()调用不是那么及时(应用层压力大,上层代码在干别的事情),那就是连接队列满了。可能是满的,满了会怎么样。让我们关注一下当全连接队列已满时会发生什么。当全连接队列满时,connect()和accept()的行为是什么?实践是检验真理的最好方法。我们直接进入测试程序。client.c:server.c:通过执行上面的代码,我们观察到Linux3.10版本内核在fullconnectionqueue已满时的现象。神奇的事情发生了,服务端全连接队列满了,连接丢失了,但是客户端connect()系统调用已经返回成功,客户端以为TCP连接握手成功,但是服务端不知道,这个连接像幽灵一样存在片刻又消失:这个问题对应的抓包如下:就像问题描述的现象一样,在一个320个节点的集群中,总会有个别的节点,显然connect()返回成功了,但是对端失败了,因为3.10内核会在全连接队列满的时候先回复SYN-ACK,然后在发现满了丢弃连接之前移入全连接队列,这样TCP连接从客户端的角度来看是成功的,但服务器什么都不知道。全连接队列满时Linux4.9版本内核的行为在4.9内核中,全连接队列的处理有所不同。connect()系统调用不会成功并且会一直阻塞,这意味着可以避免幽灵连接。:抓包交换如下。可以看到服务器没有回复SYN-ACK,客户端一直重传SYN:其实我第一次遇到这个问题的时候,立马怀疑是fullconnectionqueue满了。然而悲剧的是,我看的源码是Linux3.10的,而我发现的一个本地日常测试ECS恰好是Linux4.9内核的,于是写了一个demo测试例子,问题并没有重现。排除所有其他原因,一周后(一个悲伤的故事)它又回来了。总结Q:当全连接队列满时,connect()和accept()的行为是怎样的?A:Linux3.10内核和新版内核的行为不一致。如果使用Linux3.10内核,会出现客户端误连接成功的问题,Linux4.9内核不会有问题。这个案例告诉我们,实践是检验真理的最好方法,但是在实践的时候也要擦亮眼睛,看清环境的差异。像Linux内核这样稳定的东西并不是一成不变的。唯一不变的就是变化,说不定你也可以来数据库内核玩转底层技术。原文链接本文为阿里云原创内容,未经许可不得转载。
