简介:最近在作数据库相关的事情,碰到了不少TCP相关的问题,新的场景新的挑战,有不少以前并无掌握透彻的点,大大开了一把眼界,选了几个案例分享一下。
做者 | 韩述
来源 | 阿里技术公众号linux
最近在作数据库相关的事情,碰到了不少TCP相关的问题,新的场景新的挑战,有不少以前并无掌握透彻的点,大大开了一把眼界,选了几个案例分享一下。数据库
在TCP协议中,包含RST标识位的包,用来异常的关闭链接。在TCP的设计中它是不可或缺的,发送RST段关闭链接时,没必要等缓冲区的数据都发送出去,直接丢弃缓冲区中的数据。而接收端收到RST段后,也没必要发送ACK来确认。后端
某客户链接数据库常常出现链接中断,可是通过反复排查,后端数据库实例排查没有执行异常或者Crash等问题,客户端Connection reset的堆栈以下图:安全
通过复现及双端抓包的初步定位,找到了一个可疑点,TCP交互的过程当中客户端发了一个RST(后经查明是客户端本地的一些安全相关iptables规则致使),可是神奇的是,这个RST并无影响TCP数据的交互,双方很愉快的无视了这个RST,很开心的继续数据交互,然而10s钟以后,链接忽然中断,参看以下抓包:网络
从抓包现象看,在客户端发了一个RST以后,双方的TCP数据交互彷佛没有受到任何影响,不管是数据传输仍是ACK都很正常,在本轮数据交互结束后,TCP链接又正常的空闲了一会,10s以后链接忽然被RST掉,这里就有两个有意思的问题了:session
查看一下RFC的官方解释:并发
简单来讲,就是RST包并非必定有效的,除了在TCP握手阶段,其余状况下,RST包的Seq号,都必须in the window,这个in the window其实很难从字面理解,通过对Linux内核代码的辅助分析,肯定了其含义实际就是指TCP的 —— 滑动窗口,准确说是滑动窗口中的接收窗口。负载均衡
咱们直接检查Linux内核源码,内核在收到一个TCP报文后进入以下处理逻辑:tcp
下面是内核中关于如何肯定Seq合法性的部分:分布式
Q:TCP数据交互过程当中,在一方发了RST之后,链接必定会终止么?
A:不必定会终止,须要看这个RST的Seq是否在接收方的接收窗口以内,如上例中就由于Seq号较小,因此不是一个合法的RST被Linux内核无视了。
Q:链接会当即终止么,仍是会等10s?
A:链接会当即终止,上面的例子中过了10s终止,正是由于,linux内核对RFC严格实现,无视了RST报文,可是客户端和数据库之间通过的SLB(云负载均衡设备),却处理了RST报文,致使10s(SLB 10s 后清理session)以后关闭了TCP链接
这个案例告诉咱们,透彻的掌握底层知识,实际上是颇有用的,不然一旦遇到问题,(自证清白并指向root cause)都不知道往哪一个方向排查。
咱们平时有一个常识,Linux内核一共只有65535个端口号可用,也就意味着一台机器在不考虑多网卡的状况下最多只能开放65535个TCP端口。
可是常常看到有单机百万TCP链接,是如何作到的呢,这是由于,TCP是采用四元组(Client端IP + Client端Port + Server端IP + Server端Port)做为TCP链接的惟一标识的。若是做为TCP的Server端,不管有多少Client端链接过来,本地只须要占用同一个端口号。而若是做为TCP的Client端,当链接的对端是同一个IP + Port,那确实每个链接须要占用一个本地端口,但若是链接的对端不是同一个IP + Port,那么其实本地是能够复用端口的,因此实际上Linux中有效可用的端口是不少的(只要四元组不重复便可)。
做为一个分布式数据库,其中每一个节点都是须要和其余每个节点都创建一个TCP链接,用于数据的交换,那么假设有100个数据库节点,在每个节点上就会须要100个TCP链接。固然因为是多进程模型,因此其实是每一个并发须要100个TCP链接。假若有100个并发,那就须要1W个TCP链接。但事实上1W个TCP链接也不算多,由以前介绍的背景知识咱们能够得知,这远远不会达到Linux内核的瓶颈。
可是咱们却常常遇到端口不够用的状况, 也就是“bind:Address already in use”:
其实看到这里,不少同窗已经在猜想问题的关键点了,经典的TCP time\_wait 问题呗,关于TCP的 time\_wait 的背景介绍以及应对方法不是本文的重点就不赘述了,能够自行了解。乍一看,系统中有50W的 time\_wait 链接,才65535的端口号,必然不可用:
可是这个猜想是错误的!由于系统参数 net.ipv4.tcp\_tw\_reuse 早就已经被打开了,因此不会因为 time\_wait 问题致使上述现象发生,理论上说在开启 net.ipv4.cp\_tw\_reuse 的状况下,只要对端IP + Port 不重复,可用的端口是不少的,由于每个对端IP + Port都有65535个可用端口:
Linux有多少端口能够被有效使用
理论来讲,端口号是16位整型,一共有65535个端口能够被使用,可是Linux操做系统有一个系统参数,用来控制端口号的分配:
net.ipv4.ip_local_port_range
咱们知道,在写网络应用程序的时候,有两种使用端口的方式:
例如以下状况,至关于 1-20000 是系统保留端口号(除非按方法一显式指定端口号),自动分配的时候,只会从 20000 - 65535 之间随机选择一个端口,而不会使用小于20000的端口:
为何在 tcp\_tw\_reuse=1 状况下,端口依然不够用
细心的同窗可能已经发现了,报错信息所有都是 bind() 这个系统调用失败,而没有一个是 connect() 失败。在咱们的数据库分布式节点中,全部 connect() 调用(即做为TCP client端)都成功了,可是做为TCP server的 bind(0) + listen() 操做却有不少没成功,报错信息是端口不足。
因为咱们在源码中,使用了 bind(0) + listen() 的方式(而不是bind某一个固定端口),即由操做系统随机选择监听端口号,问题的根因,正是这里。connect() 调用依然能从 net.ipv4.ip\_local\_port\_range 池子里捞出端口来,可是 bind(0) 却不行了。为何,由于两个看似行为类似的系统调用,底层的实现行为倒是不同的。
源码以前,了无秘密:bind() 系统调用在进行随机端口选择时,判断是否可用是走的 inet\_csk\_bind\_conflict ,其中排除了存在 time\_wait 状态链接的端口:
而 connect() 系统调用在进行随机端口的选择时,是走 \_\_inet\_check\_established 判断可用性的,其中不但容许复用存在 TIME\_WAIT 链接的端口,还针对存在TIME\_WAIT的链接的端口进行了以下判断比较,以肯定是否能够复用:
一张图总结一下:
因而答案就明了了,bind(0) 和 connect()冲突了,ip\_local\_port\_range 的池子里被 50W 个 connect() 遗留的 time\_wait 占满了,致使 bind(0) 失败。知道了缘由,修复方案就比较简单了,将 bind(0) 改成bind指定port,而后在应用层本身维护一个池子,每次从池子中随机地分配便可。
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究竟有多少Port可用知道一点,可是只是只知其一;不知其二,就很容易陷入思惟陷阱,忽略真正的Root Case,要掌握就要透彻。
TCP三次握手,SYN、SYN-ACK、ACK是全部人耳熟能详的常识,可是具体到Socket代码层面,是如何和三次握手的过程对应的,恐怕就不是那么了解了,能够看一下以下图,理解一下:(图源:小林coding)
这个过程的关键点是,在Linux中,通常状况下都是内核代理三次握手的,也就是说,当你client端调用 connect() 以后内核负责发送SYN,接收SYN-ACK,发送ACK。而后 connect() 系统调用才会返回,客户端侧握手成功。
而服务端的Linux内核会在收到SYN以后负责回复SYN-ACK再等待ACK以后才会让 accept() 返回,从而完成服务端侧握手。因而Linux内核就须要引入半链接队列(用于存放收到SYN,但还没收到ACK的链接)和全链接队列(用于存放已经完成3次握手,可是应用层代码尚未完成 accept() 的链接)两个概念,用于存放在握手中的链接。
咱们的分布式数据库在初始化阶段,每两个节点之间两两创建TCP链接,为后续数据传输作准备。可是在节点数比较多时,好比320节点的状况下,很容易出现初始化阶段卡死,通过代码追踪,卡死的缘由是,发起TCP握手侧已经成功完成的了 connect() 动做,认为TCP已创建成功,可是TCP对端却没有握手成功,还在等待对方创建TCP链接,从而整个集群一直没有完成初始化。
看过以前的背景介绍,聪明的小伙伴必定会好奇,假如咱们上层的 accpet() 调用没有那么及时(应用层压力大,上层代码在干别的),那么全链接队列是有可能会满的,满的状况会是如何效果,咱们下面就重点看一下全链接队列满的时候会发生什么。
当全链接队列满时,connect() 和 accept() 侧是什么表现行为?
实践是检验真理的最好途径
咱们直接上测试程序。
client.c :
server.c :
经过执行上述代码,咱们观察Linux 3.10版本内核在全链接队列满的状况下的现象。神奇的事情发生了,服务端全链接队列已满,该链接被丢掉,可是客户端 connect() 系统调用却已经返回成功,客户端觉得这个TCP链接握手成功了,可是服务端殊不知道,这个链接犹如幽灵通常存在了一瞬又消失了:
这个问题对应的抓包以下:
正如问题中所述的现象,在一个320个节点的集群中,总会有个别节点,明明 connect() 返回成功了,可是对端却没有成功,由于3.10内核在全链接队列满的状况下,会先回复SYN-ACK,而后移进全链接队列时才发现满了因而丢弃链接,这样从客户端看来TCP链接成功了,可是服务端却什么都不知道。
Linux 4.9版本内核在全链接队列满时的行为
在4.9内核中,对于全链接队列满的处理,就不同,connect() 系统调用不会成功,一直阻塞,也就是说可以避免幽灵链接的产生:
抓包报文交互以下,能够看到Server端没有回复SYN-ACK,客户端一直在重传SYN:
事实上,在刚遇到这个问题的时候,我第一时间就怀疑到了全链接队列满的状况,可是悲剧的是看的源码是Linux 3.10的,而随手找的一个本地平常测试的ECS却恰好是Linux 4.9内核的,致使写了个demo测试例子却死活没有复现问题。排除了全部其余缘由,再次绕回来的时候已是一周以后了(这是一个悲伤的故事)。
Q:当全链接队列满时,connect() 和 accept() 侧是什么表现行为?
A:Linux 3.10内核和新版本内核行为不一致,若是在Linux 3.10内核,会出现客户端假链接成功的问题,Linux 4.9内核就不会出现问题。
这个案例告诉咱们,实践是检验真理的最好方式,可是实践的时候也必定要睁大眼睛看清楚环境差别,如Linux内核这般稳定的东西,也不是一成不变的。惟一不变的是变化,也许你也是能够来数据库内核玩玩底层技术的。
本文内容由阿里云实名注册用户自发贡献,版权归原做者全部,阿里云开发者社区不拥有其著做权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。若是您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将马上删除涉嫌侵权内容。