深入理解Linux网络笔记(七):异常TCP连接建立情况、如何查看是否有连接队列溢出发生

本文为《深入理解Linux网络》学习笔记,使用的Linux源码版本是3.10,网卡驱动默认采用的都是Intel的igb网卡驱动

Linux源码在线阅读:https://elixir.bootlin.com/linux/v3.10/source

5、深度理解TCP连接建立过程(二)

4)、异常TCP连接建立情况
1)connect系统调用耗时失控

客户端在发起connect系统调用的时候,主要工作就是端口选择。在选择的过程中,有个大循环,从ip_local_port_range的一个随机位置开始把这个范围遍历一遍,找到可用端口则退出循环。如果端口很充足,那么循环只需要执行少数几次就可以退出。但假设端口消耗掉很多已经不充足,或者干脆就没有可用的了,那么这个循环就得执行很多遍。详细代码如下:

c 复制代码
// net/ipv4/inet_hashtables.c
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
		struct sock *sk, u32 port_offset,
		int (*check_established)(struct inet_timewait_death_row *,
			struct sock *, __u16, struct inet_timewait_sock **),
		int (*hash)(struct sock *sk, struct inet_timewait_sock *twp))
{
        ...
		inet_get_local_port_range(&low, &high);
		remaining = (high - low) + 1;
        ...
		for (i = 1; i <= remaining; i++) {
            // 其中offset是一个随机数
			port = low + (i + offset) % remaining;
			if (inet_is_reserved_local_port(port))
				continue;
			head = &hinfo->bhash[inet_bhashfn(net, port,
					hinfo->bhash_size)];
            // 加锁
			spin_lock(&head->lock);
            // 一大段的选择端口逻辑
	        ...
	        // 选择成功就goto ok
	        // 不成功就goto next_port
		next_port:
            // 解锁
			spin_unlock(&head->lock);
		}
		...
}

在每次的循环内部需要等待锁以及在哈希表中进行多次的搜索。注意这里的锁是自旋锁,是一种非阻塞的锁,如果资源被占用,进程并不会被挂起,而是会占用CPU去不断尝试获取锁

但假设端口范围ip_local_port_range配置的是10000-30000,而且已经用尽了。那么每次当发起连接的时候都需要把循环执行两万遍才退出。这时会涉及大量的哈希表查找以及自旋锁等待开销,系统态CPU将会出现大幅度上涨

下图是线上截取到的正常时的connect系统调用耗时,是22微秒

下图是一台服务器在端口不足的情况下的connect系统调用耗时,是2581微秒

异常情况下的connect耗时是正常情况下的100多倍。虽然换算成毫秒只有2毫秒多一点儿,但是要知道这消耗的全是CPU时间。理解了时间产生的原因,解决起来就非常简单了,办法很多。修改内核参数net.ipv4.ip_local_port_range多预留一些端口号、改用长连接或者尽快回收TIME_WAIT都可以

2)第一次握手丢包

服务端在响应来自客户端的第一次握手请求的时候,会判断半连接队列和全连接队列是否溢出。如果发生溢出,可能会直接将握手包丢弃,而不会反馈给客户端。接下来我们来分别详细看一下

半连接队列满

来看看半连接队列在何种情况下会导致丢包

c 复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	...
    // 看看半连接队列是否满了
	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
		want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
		if (!want_cookie)
			goto drop;
	}
    // 看看全连接队列是否满了
    ...
drop:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
	return 0;
}

在以上代码中,inet_csk_reqsk_queue_is_full如果返回true就表示半连接队列满了,另外tcp_syn_flood_action判断是否打开了内核参数tcp_syncookies,如果未打开则返回false

c 复制代码
// net/ipv4/tcp_ipv4.c
bool tcp_syn_flood_action(struct sock *sk,
			 const struct sk_buff *skb,
			 const char *proto)
{
	...
	bool want_cookie = false;
	...
	if (sysctl_tcp_syncookies) {
		...
		want_cookie = true;
		...
	}
    ...
	return want_cookie;
}

也就是说,如果半连接队列满了,而且ipv4.tcp_syncookies参数设置为0,那么来自客户端的握手包将直接丢弃

SYN Flooad攻击就是通过耗光服务端上的半连接队列来使得正常的用户连接请求无法被响应。不过在现在的Linux内核里只要打开tcp_syncookies,半连接队列满了仍然可以保证正常握手的进行

全连接队列满

当半连接队列判断通过以后,紧接着还有全连接队列满的相关判断。如果满了,服务器对握手包的处理还是会丢弃。源码如下:

c 复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	// 看看半连接队列是否满了
    ...
    // 看看全连接队列是否满了
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
  ...
drop:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
	return 0;
}

sk_acceptq_is_full判断全连接队列是否满了,inet_csk_reqsk_queue_young判断有没有young_ack(未处理完的半连接请求)

从这段代码可以看到,假如全连接队列满的情况下,且同时有young_ack,那么内核同样直接丢掉该SYN握手包

客户端发起重试

假设服务端侧发生了全/半连接队列溢出而导致的丢包,那么转换到客户端视角来看就是SYN包没有任何响应

好在客户端在发出握手包的时候,开启了一个重传定时器。如果收不到预期的synack,超时重传的逻辑就会开始执行,如下图所示。不过重传计时器的时间单位都是以秒来计算的,这意味着,如果有握手重传发生,即使第一次重传就能成功,那接口最快响应也是1秒以后得事情了。这对接口耗时影响非常大

来详细看看重传的相关逻辑。客户端在connect系统调用发出SYN握手信号后就开启了重传定时器

c 复制代码
// net/ipv4/tcp_output.c
int tcp_connect(struct sock *sk)
{
    ...
	// 实际发出SYN
	err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
	      tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
	// 启动重传定时器
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;
}

在定时器设置中传入的inet_csk(sk)->icsk_rto是超时时间,该值初始化的时候被设置为1秒

c 复制代码
// net/ipv4/tcp_output.c
void tcp_connect_init(struct sock *sk)
{
	...
    // 初始化为TCP_TIMEOUT_INIT
	inet_csk(sk)->icsk_rto = TCP_TIMEOUT_INIT;
	...
}
c 复制代码
// include/net/tcp.h
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))

如果能正常接收到服务端响应的synack,那么客户端的这个定时器会清除。这段逻辑在tcp_rearm_rto离。调用顺序tcp_rcv_state_process=>tcp_rcv_synsent_state_process=>tcp_ack=>tcp_clean_rtx_queue=>tcp_rearm_rto

c 复制代码
// net/ipv4/tcp_input.c
void tcp_rearm_rto(struct sock *sk)
{
	...
		inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);
	...
}

如果服务端发生了丢包,那么定时器到时后会进入回调函数tcp_write_timer中进行重传

其实不只是握手,连接状态的超时重传也是在这里完成的

c 复制代码
// net/ipv4/tcp_timer.c
static void tcp_write_timer(unsigned long data)
{
	...
		tcp_write_timer_handler(sk);
	...
}
c 复制代码
// net/ipv4/tcp_timer.c
void tcp_write_timer_handler(struct sock *sk)
{
	...
    // 取出定时器类型
	event = icsk->icsk_pending;

	switch (event) {
	...
	case ICSK_TIME_RETRANS:
		icsk->icsk_pending = 0;
		tcp_retransmit_timer(sk);
		break;
	...
	}
  ...
}

tcp_retransmit_timer是重传的主要函数。在这里完成重传,以及下一次定时器到期时间的设置

c 复制代码
// net/ipv4/tcp_timer.c
void tcp_retransmit_timer(struct sock *sk)
{
	...
    // 超过了重传次数则退出
	if (tcp_write_timeout(sk))
		goto out;
        ...
        // 重传
	if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 0) {
		// 重传失败
        ...
	}
    ...
// 退出前置重新设置下一次超时时间    
out_reset_timer:
	// 计算超时时间
	if (sk->sk_state == TCP_ESTABLISHED &&
	    (tp->thin_lto || sysctl_tcp_thin_linear_timeouts) &&
	    tcp_stream_is_thin(tp) &&
	    icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) {
		icsk->icsk_backoff = 0;
		icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX);
	} else {
		icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	}
    // 设置
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
	...
}

tcp_write_timeout用来判断是否重试过多,如果是则退出重试逻辑

tcp_write_timeout的判断逻辑其实也有点复杂。对于SYN握手包主要的判断依据是net.ipv4.tcp_syn_retries,但其实也并不是简单对比次数,而是转换成了时间进行对比。所以如果在线上看到实际重传次数和对应内核参数不一致也不用太奇怪

接着在tcp_retransmit_timer函数中重传了发送队列里的元素。而且还设置了下一次超时的时间,为前一次的两倍(左移操作相当于乘2)

实际抓包结果

下图是因为服务端第一次握手丢包的抓包截图

可以看到,客户端在1秒以后进行了第一次握手重试。重试仍然没有响应,那么接下来依次又分别在3秒、7秒、15秒、31秒和63秒等时间共重试了6次(tcp_syn_retries设置的是6)

假如服务端第一次握手的时候出现了半/全连接队列溢出导致的丢包,那么接口响应时间将至少是1秒以上

3)第三次握手丢包

客户端在收到服务器的synack响应的时候,就认为连接建立成功了,然后会将自己的连接状态设置为ESTABLISHED,发出第三次握手请求。但服务端在第三次握手的时候,还有可能有意外发生

c 复制代码
// net/ipv4/tcp_ipv4.c
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
				  struct request_sock *req,
				  struct dst_entry *dst)
{
	...
    // 判断全连接队列是不是满了
	if (sk_acceptq_is_full(sk))
		goto exit_overflow;
    ...
exit_overflow:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
    ...
}

从上述代码可以看出,第三次握手时,如果服务器全连接队列满了,来自客户端的ack握手包又被直接丢弃

想想也很好理解,三次握手完的请求是要放在全连接队列里的。但是假如全连接队列满了,三次握手也不会成功

不过有意思的是,第三次握手失败并不是客户端重试,而是由服务端来重发synack

下图是第三次握手丢包的抓包截图

第一个框内是第三次握手,其实这个握手请求在服务端已经被丢弃了。但是这时候客户端并不知情,它一直傻傻地以为三次握手已经妥了呢。不过还好,这时在服务端的半连接队列中仍然记录着第一次握手时存的握手请求

服务端等到半连接定时器到时后,向客户端重新发起synack,客户端收到后再重新恢复第三次握手ack。如果这期间服务端全连接队列一直都是满的,那么服务端重试5次(受内核参数net.ipv4.tcp_synack_retries控制)后就放弃了

在这种情况下还要注意另外一个问题。在实践中,客户端往往是以为连接建立成功就会开始发送数据,其实这时候连接还没有真的建立起来。它发出去的数据,包括重试将全部被服务端无视,直到连接真正建立成功后才行,如下图所示:

4)握手异常总结

如果端口不充足,会导致connect系统调用的时候过多地执行自旋锁等待与哈希查找,会引起CPU开销上涨。严重情况下会耗光CPU,影响用户业务逻辑的执行。出现这种问题处理起来的方法有这么几个:

  • 通过调整ip_local_port_range来尽量加大端口范围
  • 尽量复用连接,使用长连接来削减频繁的握手处理
  • 第三个有用,但是不太推荐的方法是开启tcp_tw_reuse和tcp_tw_recycle

服务端在第一次握手时,在如下两种情况下可能会丢包:

  • 半连接队列满,且tcp_syncookies为0
  • 全连接队列满,且有未完成的半连接请求

在这两种情况下,从客户端视角来看和网络断了没有区别,就是发出去的SYN包没有任何反馈,然后等待定时器到时后重传握手请求。第一次重传时间是1秒,接下来的等待间隔翻倍地增长,2秒、4秒、8秒......总的重传次数受net.ipv4.tcp_syn_retries内核参数影响

服务端在第三次握手时也可能出问题,如果全连接队列满,仍将发生丢包。不过第三次握手失败时,只有服务端知道(客户端误以为连接已经建立成功)。服务端根据半连接队列里的握手信息发起synack重试,重试次数由net.ipv4.tcp_synack_retries控制

如果出现了丢包的问题,该如何应对:

方法1:打开syncookie

可以通过打开tcp_syncookies来防止过多的请求打满半连接队列,包括SYN Flood攻击,来解决服务端因为半连接队列满而发生的丢包

方法2:加大连接队列长度

全连接队列的长度是min(backlog, net.core.somaxconn),半连接队列的长度是min(backlog, somaxconn, tcp_max_sync_backlog)+1再向上取整到2的N次幂,但最小不能小于16

如果需要加大全/半连接队列长度,请调节以上的一个或多个参数来达到目的。只要队列长度合适,就能很大程度降低握手异常概率的发生。其中全连接队列在修改完后可以通过ss命令输出的Send-Q来确认最终生效长度

$ ss -nlt
Recv-Q Send-Q Local Address:Port Address:Port                    
0      128    *:80               *:*

Recv-Q告诉我们当前该进程的全连接队列使用情况。如果Recv-Q已经逼近了Send-Q,那么可能不需要等到丢包也应该准备加大全连接队列了

方法3:尽快调用accept

这个虽然一般不会成为问题,但也要注意一下。应用程序应该尽快在握手成功之后通过accept把新连接取走。不要忙于处理其他业务逻辑而导致全连接队列塞满了

方法4:尽早拒绝

如果加大队列后仍然有非常偶发的队列溢出,我们可以暂且容忍。但如果仍然有较长时间处理不过来怎么办?另外一个做法就是直接报错,不要让客户端超时等待。例如将Redis、MySQL等服务器的内核参数tcp_abort_on_overflow设置为1。如果队列满了,直接发reset指令给客户端。告诉后端进程/线程不要傻等。这时候客户端会受到错误connection reset by peer

方法5:尽量减少TCP连接的次数

如果上述方法都未能根治问题,这个时候应该思考是否可以用长连接代替短连接,减少过于频繁的三次握手。这个方法不但能降低握手出问题的可能性,而且还顺带砍掉了三次握手的各种内存、CPU、时间上的开销,对提升性能也有较大帮助

5)、如何查看是否有连接队列溢出发生
1)全连接队列溢出判断

全连接队列溢出都会记录到ListenOverflows这个MIB(Management Information Base,管理信息库),对应SNMP统计信息中的ListenDrops这一项。我们来展开看一下相关的源码

服务端在响应客户端的SYN握手包的时候,有可能会在tcp_v4_conn_request调用这里发生全连接队列溢出而丢包

c 复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	// 看看半连接队列是否满了
    ...
    // 看看全连接队列是否满了
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
    ...
drop:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
	return 0;
}

从上述代码可以看到,全连接队列满了以后调用NET_INC_STATS_BH增加了LINUX_MIB_LISTENOVERFLOWS和LINUX_MIB_LISTENDROPS这两个MIB

服务端在响应第三次握手的时候,会再次判断全连接队列是否溢出。如果溢出,一样会增加这两个MIB,源码如下:

c 复制代码
// net/ipv4/tcp_ipv4.c
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
				  struct request_sock *req,
				  struct dst_entry *dst)
{
	...
	if (sk_acceptq_is_full(sk))
		goto exit_overflow;
    ...
exit_overflow:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
    ...
exit:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
	return NULL;
    ...
}

在proc.c中,LINUX_MIB_LISTENOVERFLOWS和LINUX_MIB_LISTENDROPS都被整合进了SNMP统计信息

c 复制代码
// net/ipv4/proc.c
static const struct snmp_mib snmp4_net_list[] = {
	...
	SNMP_MIB_ITEM("ListenOverflows", LINUX_MIB_LISTENOVERFLOWS),
	SNMP_MIB_ITEM("ListenDrops", LINUX_MIB_LISTENDROPS),
	...
};

netstat工具源码

在执行netstat -s的时候,该工具会读取SNMP统计信息并展现出来。netstat命令属于net-tools工具集,net-tools源码如下:

c 复制代码
// https://github.com/giftnuss/net-tools/blob/master/statistics.c
struct entry Tcpexttab[] =
{
    ...
    { "ListenOverflows", N_("%u times the listen queue of a socket overflowed"),
      opt_number },
    { "ListenDrops", N_("%u SYNs to LISTEN sockets dropped"), opt_number },
    ...
};

以上这些就是执行netstat -s时会执行到的源码。它从SNMP统计信息中获取ListenDrops和ListenOverflows这两项并显示出来,分别对应LINUX_MIB_LISTENDROPS和LINUX_MIB_LISTENOVERFLOWS这两个MIB

$ watch 'netstat -s | grep overflowed'
 198 times the listen queue of a socket overflowed

所以,每当发生全连接队列满导致的丢包的时候,会通过上述命令的结果体现出来。而且幸运的是,ListenOverflows这个SNMP统计项只有在全连接队列满的时候才会增加,内核源码其他地方没有用到

所以,通过netstat -s输出中的xx times the listen queue如果查看到数字有变化,那么一定是你的服务端上发生了全连接队列溢出了

2)半连接队列溢出判断

再来看半连接队列,溢出时更新的是LINUX_MIB_LISTENDROPS这个MIB,对应到SNMP就是ListenDrops这个统计项

c 复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	...
    // 看看半连接队列是否满了
	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
		want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
		if (!want_cookie)
			goto drop;
	}

	// 看看全连接队列是否满了
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}
    ...
drop:
	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
	return 0;
}

从上述源码可见,半连接队列满的时候goto drop,然后增加了LINUX_MIB_LISTENDROPS这个MIB

但是问题在于,不只是在半连接队列发生溢出的时候会增加该值。所以根据netstat -s看半连接队列是否溢出是不靠谱的

从前序内容可知,即使半连接队列没问题,全连接队列满了该值也会增加。另外就是当在listen状态握手发生错误的时候,进入tcp_v4_err函数时也会增加该值

对于如何查看半连接队列溢出丢包这个问题,建议是不要纠结怎么看是否丢包了。直接看服务器上的tcp_syncookies是不是1就行

如果该值是1,那么下面代码中want_cookie就返回真,是根本不会发生半连接溢出丢包的

c 复制代码
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
	...
    // 看看半连接队列是否满了
	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
		want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
		if (!want_cookie)
			goto drop;
	}
    ...
}

如果tcp_syncookies不是1,则建议改成1就完事了

如果因为各种原因就是不想打开tcp_syncookies,就想看看是否有因为半连接队列满而导致的SYN丢弃,除了netstat -s的结果,建议同时查看当前listen端口上的SYN_RECV的数量

$ netstat -antp | grep SYN_RECV
256

在深入理解Linux网络笔记(六)中讨论了半连接队列的实际长度怎么计算。如果SYN_RECV状态的连接数量达到你算出来的队列长度,那么可以确定有半连接队列溢出了。如果想加大半连接队列的长度,方法也在Linux网络笔记(六)中讲过了

3)小结

对于全连接队列来说,使用netstat -s(最好再配合watch命令动态观察)就可以判断是否有丢包发生。如果看到xx times the listen queue中的数值在增长,那么就确定是全连接队列满了

$ watch 'netstat -s | grep overflowed'
 198 times the listen queue of a socket overflowed

对于半连接队列来说,只要保证tcp_syncookies这个内核参数是1就能保证不会有因为半连接队列满而发生的丢包。如果确实想看一看,使用watch 'netstat -s | grep "SYNs"'这种方式是错的,是没有办法说明问题的。需要自己计算半连接队列的长度,再看看当前SYN_RECV状态的连接的数量

$ watch 'netstat -s | grep "SYNs"'
 258209 SYNs to LISTEN sockets dropped 
$ netstat -antp | grep SYN_RECV | wc -l
5 
6)、总结

1)为什么服务端程序都需要先listen一下?

内核在响应listen调用的时候是创建了半连接、全连接两个队列,这两个队列是三次握手中很重要的数据结构,有了它们服务端才能正常响应来自客户端的三次握手。所以服务器提供服务前都需要先listen一下才行

2)半连接队列和全连接队列长度如何确定?

服务端在执行listen的时候确定好了半连接队列和全连接队列的长度

对于半连接队列来说,其最大长度是min(backlog, somaxconn, tcp_max_sync_backlog)+1再向上取整到2的N次幂,但最小不能小于16。如果需要加大半连接队列的长度,那么需要一并考虑backlog、somaxconn和tcp_max_sync_backlog

对于全连接队列来说,其最大长度是listen时传入的backlog和net.core.somaxconn之间较小的那个值。如果需要加大全连接队列长度,那么调整backlog和somaxconn

3)Cannot assign requested address这个报错你知道是怎么回事吗?该如何解决?

一条TCP连接由一个四元组构成:Server IP、Server Port、Client IP、Client Port。在连接建立前,前面的三个元素基本是确定了的,只有Client Port是需要动态选择出来的

客户端会在connect发起的时候自动选择端口号。具体的选择过程就是随机地从ip_local_port_range选择一个位置开始循环判断,跳过ip_local_reserved_ports里设置要规避的端口,然后挨个判断是否可用。如果循环完也没有找到可用端口,会报错Cannot assign requested address

理解了这个报错的原理,解决这个问题的办法就很多了。比如扩大可用端口范围、减少最大TIME_WAIT状态连接数量等方法都是可行的

# vi /etc/sysctl.conf
# 修改可用端口范围
net.ipv4.ip_local_port_range = 5000 65000
# 设置最大TIME_WAIT数量
net.ipv4.tcp_max_tw_buckets = 10000
# sysctl -p

4)一个客户端端口可以同时用在两条连接上吗?

connect调用在选择端口的时候如果端口没有被用过那么就是可用的。但是如果被用过并不是说这个端口就不能用了

如果用过,接下来进一步判断新连接和老连接四元组是否完全一致,如果不完全一致,该端口仍然可用。例如5000这个端口号是完全可以用于下面两条不同的连接的

  • 连接1:192.168.1.101 5000 192.168.1.100 8090
  • 连接2:192.168.1.101 5000 192.168.1.100 8091

在保证四元组不相同的情况下,一个端口完全可以用在两条,甚至更多条的连接上

5)服务端半/全连接队列满了会怎么样?

服务端响应第一次握手的时候,会进行半连接队列和全连接队列满的判断。如果半连接队列满了,并未开启tcp_syncookies这个内核参数,那么该握手包将直接被丢弃,所以建议不要关闭tcp_syncookies这个内核参数。如果全连接队列满了,且有young_ack(表示刚刚有SYN到达),那么同样也是直接丢弃

服务端响应第三次握手的时候,还会再次判断全连接队列是否满。如果满了,同样丢弃握手请求

无论是哪种丢弃发生,肯定是会影响线上服务的。当收不到预期的握手或者响应包的时候,重传定时器会在最短1秒后发起重试。这样接口响应的耗时最少就得1秒起步了。如果重试也没握手成功,很有可能就会报超时了

6)新连接的socket内核对象是什么时候建立的?

socket内核对象最核心的部分是struct sock

c 复制代码
// include/linux/net.h
struct socket {
	...
	struct sock		*sk;
	...
};

内核其实在第三次握手完毕的时候就把socket对象创建好了。在用户进程调用accept的时候,直接把该对象取出来,再包装一个socket对象就返回了

7)建议一条TCP连接需要消耗多长时间?

一般网络的RTT值根据服务器物理距离的不同大约是在零点几秒、几十毫秒之间。这个时间要比CPU本地的系统调用耗时短得多。所以正常情况下,在客户端或者是服务端看来,都基本上约等于一个RTT。但是如果一旦出现了丢包,无论是哪种原因,需要重传定时器来介入的话,耗时就最少要1秒了

8)把服务器部署在北京,给纽约的用户访问可行吗?

正常情况下建立一条TCP连接耗时是双端网络一次RTT时间。那么如果服务器在北京,用户在美国,这个RTT是多少呢?

美国和中国物理距离跨越了半个地球,北京到纽约的球面距离大概是15000千米。那么抛开设备转发延迟,仅仅光速传播一个来回,需要时间=15000*2/光速=100毫秒。实际的延迟比这个还要大一些,一般都要200毫秒以上。建议在这个延迟上,想要提供用户能访问的秒级服务就很困难了。所以对于海外用户,最好在当地建机房或者购买海外的服务器

9)服务器负载很正常,但是CPU被打到底了是怎么回事?

如果在端口极其不充足的情况下,connect系统调用的内部循环需要全部执行完毕才能判断出来没有端口可用。如果要发出的连接请求特别频繁,connect就会消耗掉大量的CPU。当时服务器上进程并不是很多,但是每个进程都在疯狂地消耗CPU,这时候就会出现CPU被消耗光,但是服务器负载却不高的情况

推荐阅读:

4.1 TCP 三次握手与四次挥手面试题

4.4 TCP 半连接队列和全连接队列

内核参数 tcp_syncookies-- 默认开启tcp_syncookies

4.14 tcp_tw_reuse 为什么默认是关闭的?

从一次线上问题说起,详解 TCP 半连接队列、全连接队列

相关推荐
内核程序员kevin17 天前
使用trace-cmd跟踪Linux内核函数:一次愉快的内核探险
linux·运维·linux内核·ftrace
内核程序员kevin1 个月前
使用Go语言开发eBPF入门教程
golang·linux内核·ebpf·系统调用
问道飞鱼2 个月前
【计算机网络】计算机网络相关术语
网络·vlan·nat·linux网络·vxlan
嵌入式小李4 个月前
Linux并发与竞争
linux·linux内核·并发与竞争
格桑阿sir5 个月前
Docker核心技术:Docker原理之Namespace
linux·docker·容器·linux内核·namespace·隔离性·容器标准
格桑阿sir5 个月前
Docker核心技术:Docker原理之Cgroups
linux·docker·容器·linux内核·cpu·资源限制·cgroups
_三分糖5 个月前
【Linux内核编程--模块机制】
android·linux·运维·服务器·linux内核·c·linux驱动
flashing-c5 个月前
Linux HOOK机制与Netfilter HOOK
linux·运维·网络·linux内核·netfilter·linux hook
橘色的喵6 个月前
针对ARM64嵌入式系统的Linux内核参数优化
性能优化·linux内核·嵌入式·arm·内存管理·arm64·网络优化
回眸&啤酒鸭6 个月前
Linux 内核 (十二)进程间通讯 之 消息队列
linux·linux内核