详解tcp端口自连接,成因及后果

发表于2017-03-14
评论1 4.9k浏览

TCP提供的是面向连接、可靠的字节流服务。TCP端口在连接时出现问题要如何去解决呢?这就给需要从TCP的原理上找解决办法,一起来看看吧。

 

背景:两个后台服务有相互调用关系,A调用B,部署在同一台机器上,B因某种原因挂掉了,然后自动拉起一直因端口被占用而失败,netstat查看有如下情况:

tcp       0      010.133.36.14:29422          10.133.36.14:29422    ESTABLISHED  A进程

奇怪,首先源目IP端口一模一样,这样的TCP连接没见过。其次这个连接是A进程发起而占用的。再次是这一现象两天遇到两次,复现率还挺高

后果就是本来正常运行的clisvrsvr偶然挂掉之后,本来可以指望自动拉起的,但可能再也不能自动拉起了,除非人为干预。

本文着重分析这一现象成因。

1、现象分析

我对这一现象的第一反应是存在某种竞争条件,触发了内核tcpbug。那我们就仔细梳理一下tcp建立连接的过程。

要建立一个tcp连接,首先svr要在b端口上listencli再使用a端口connect,端口选择一般是用户不显示bind,由内核代为选择一个空闲端口号,那么,即使svrcli在一台机器上,因为svr已将b端口占用,cli不管用户bind还是内核选择,都不可能选到b。所以这样看来,同一个端口自连接就是一个伪命题。

还不知道答案的同学,不妨想想你遇到这种情况会怎么去想。

2、实验自连接

不妨试试,在svrbind时,cli再使用这个端口是会报错的。

那如果svr没有绑定呢?使用以下实验:

testport=23534

netstat -a |grep $testport#如果端口被占用就换一个

nc localhost $testport -p$testport

netstat -a |grep $testport

恭喜你,问题重现了。如果端口被占用可以换个端口。怎么回事呢?熟悉tcp的同学可能已经知道了,就是tcp的同时打开

3、同时打开

关于同时打开,有一些比较好的描述。可以参考《TCP/IP协议族(Behrouz A. Forouzan)》15.5.1节、《TCP-IP详解 卷一:协议》18.8节、《linux内核源码剖析-TCP/IP实现 下册》28.9节。对此已了解的同学可以跳过这一节

下面解释一下,什么是同时打开。正常的tcp连接建立过程是这样的:

1svr 好想谈恋爱,怎么没人向我表白(listen

2cli 我喜欢你SYN

3svr 收到,我也喜欢你ACK+SYN

4cli 收到ACK),想她喜欢我我也喜欢她,在一起了ESTABLISHED

5svr 听到之后想我喜欢他他也喜欢我,在一起了ESTABLISHED

但是同时打开的情况是女生比较主动:

1svrcli异口同声我喜欢你SYN

2svrcli 又分别回答收到,我也喜欢你ACK+SYN

3)双方听到后都想我确认了对方喜欢我,并且我的表白也被确认了,在一起了ESTABLISHED

需要特别注意的是,同时打开是TCP协议中明确表明的正确的行为,即使它没有listen监听,而是两方都使用主动发SYN来建联

在同时打开的过程中,我们站在一方的角度想,它先发了一个SYN,然后收到了对端的SYN(而不是SYN+ACK),这时回复SYN+ACK,当再次收到SYN+ACK时就认为建连成功。我们的情况是不是这样呢?当执行“nclocalhost $destport -p $srcport”时,为了建立连接,向destport发送一个SYN(完成了步骤),因为目的ip是自己,因此会被loopback网络接口处理回送给本机TCP/IP协议栈,又因为port是自己,于是这个SYN给了正在等SYN的自己(完成了步骤),这时需要执行步骤发送SYN+ACK,同样原理,这个报文段会送给了自己(完成了步骤),于是连接建立了。

所以背景中的问题不难解释了,svr挂掉了,端口释放了,cliconnect这个目的端口的时候正好选择了这个端口作为源端口,此时端口没人用,使用是合法的。于是自连接形成了。最关键的问题已经弄清楚了

3、为什么会形成源端口扫描

还有一个疑问,我这边同事前一天遇到过同样的问题,怎么会有这么高的重现率?

抓包,发现另外一台机器在不断尝试用SYN报文连接本机的某个端口,但被RESET报文回绝了,另一台机器又不断重试。有趣的是源端口是连续递增的。我印象中源端口的选择是随机的,除非源端bind了?但是看了业务代码没有,看了所使用的yaaf框架没有,看了所使用的zmq组件也没有。继续往下吧,看看内核。

分别看了2.6版本的linux内核和3.10版本的,还真有所发现。

2.6版本的内核选择源端口的策略是:

int rover =net_random() % (high-low) + low

3.10的策略是:

1
2
3
4
5
6
7
static u32 hint;
u32 offset = hint + port_offset;
for (i = 1; i <= remaining; i++) {
           port = low + (i + offset) % remaining;
           if(port可用) break;
}
hint += i;

恭喜你又解决了一个问题,内核的确是使用连续递增的端口号,只要应用层不断重试,内核就会选择连续递增的端口号重试。那么应用有没有不断重试呢?

1
2
3
4
5
6
7
8
9
10
void zmq::tcp_connecter_t::start_connecting ()
{
  ...
    //  Handle any other error condition by eventual reconnect.
    else {
        if (s != retired_fd)                   
            close ();
        add_reconnect_timer ();
    }
}

 有,zmq在重试,用户调用了connectzmq就悄悄的帮你一直重试。

4、何时断开

根据TCP协议,一个空闲的TCP连接,除非主动断连,否则连接一直维持,即使中间路由器崩溃重启,即使过去数月,连接一直在。甚至对端主机崩溃,另一端都是不知道的

而为了避免这种情况,许多TCP协议栈实现都提供了保活定时器的能力,但要注意的是保活并不是TCP规范的一部分,并且不保证所有实现都支持,并且linuxTCP实现默认是不开启保活功能的。

那么我们的应用有没有主动关闭连接呢?没有,我的这个case中,cli 使用了连接池,一旦连接永不断开,除非cli进程关闭

5、总结

总结来说,造成这一结果的原因有:

1svrcli在同一机器(同一IP)上

2cli先于svr启动

3cliconnect失败时不断重试,这可能不是你故意为之,而是你所使用的组件偷偷干的

4cliconnect成功后一直持有连接,不主动断开

5)使用较新的linux内核版本,如3.10

以上条件每一个都不苛刻,只要同时满足它们,就会造成由cli发起却不自知的tcp端口自连接而错误占用一个svr端口。而在这一过程中,所有的部件都没有做错事,它们都在做自己正确的事,然而却导致了一个诡异的不易察觉的错误的结果。

当说到要如何避免错误时,都是从出错的地方着手,然而造成这一错误的原因却没有人做错:cli 不断重试connect和一直持有连接是业务层面的问题,如果有这方面的需求而这样做无可厚非;clisvr在同一机器是很常见的情况,不算错误;cli先于svr启动这一点也不能指责什么,况且有可能是正常运行时svr偶然down掉;linux内核故意改用顺序选择源端口而不用之前的随机方式,虽然不知道目的何在,但也是既定事实,况且这一策略本身并没有任何问题。所以如何避免这个错误,我的建议是,了解它,并避免这些条件组合成立

后记:虽说我们做业务的会尽量不去涉及底层细节,尽量使用造好的轮子,但有时候真的得去懂轮子怎么转的,像这个例子,上层的使用都是正确的,如果不深入的了解TCP原理及实现,这个问题可能还要困扰我们更久。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引