乐趣区

关于linux:那些你不知道的TCP冷门知识

简介:最近在做数据库相干的事件,碰到了很多 TCP 相干的问题,新的场景新的挑战,有很多之前并没有把握透彻的点,大大开了一把眼界,选了几个案例分享一下。
最近在做数据库相干的事件,碰到了很多 TCP 相干的问题,新的场景新的挑战,有很多之前并没有把握透彻的点,大大开了一把眼界,选了几个案例分享一下。

案例一:TCP 中并不是所有的 RST 都无效

背景常识:在 TCP 协定中,蕴含 RST 标识位的包,用来异样的敞开连贯。在 TCP 的设计中它是不可或缺的,发送 RST 段敞开连贯时,不用等缓冲区的数据都发送进来,间接抛弃缓冲区中的数据。而接收端收到 RST 段后,也不用发送 ACK 来确认。
问题景象:某客户连贯数据库经常出现连贯中断,然而通过重复排查,后端数据库实例排查没有执行异样或者 Crash 等问题,客户端 Connection reset 的堆栈如下图

通过复现及双端抓包的初步定位,找到了一个可疑点,TCP 交互的过程中客户端发了一个 RST(后经查明是客户端本地的一些平安相干 iptables 规定导致),然而神奇的是,这个 RST 并没有影响 TCP 数据的交互,单方很欢快的忽视了这个 RST,很开心的持续数据交互,然而 10s 钟之后,连贯忽然中断,参看如下抓包:

关键点剖析
从抓包景象看,在客户端发了一个 RST 之后,单方的 TCP 数据交互仿佛没有受到任何影响,无论是数据传输还是 ACK 都很失常,在本轮数据交互完结后,TCP 连贯又失常的闲暇了一会,10s 之后连贯忽然被 RST 掉,这里就有两个有意思的问题了:

  1. TCP 数据交互过程中,在一方发了 RST 当前,连贯肯定会终止么
  2. 连贯会立刻终止么,还是会等 10s
    查看一下 RFC 的官网解释:

简略来说,就是 RST 包并不是肯定无效的,除了在 TCP 握手阶段,其余状况下,RST 包的 Seq 号,都必须 in the window,这个 in the window 其实很难从字面了解,通过对 Linux 内核代码的辅助剖析,确定了其含意理论就是指 TCP 的 —— 滑动窗口,精确说是滑动窗口中的接管窗口。
咱们间接查看 Linux 内核源码,内核在收到一个 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 内核到底有多少 TCP 端口可用

背景常识:咱们平时有一个常识,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 个可用端口:

问题剖析

  1. Linux 中到底有多少个端口是能够被应用
  2. 为什么在 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 是零碎保留端口号(除非按办法一显式指定端口号),主动调配的时候,只会从 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 内核这般稳固的货色,也不是变化无穷的。惟一不变的是变动,兴许你也是能够来数据库内核玩玩底层技术的。
原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版