local port(本地可用端口)占满后linux系统卡顿问题分析

时间:2019-01-11
本文章向大家介绍local port(本地可用端口)占满后linux系统卡顿问题分析,主要包括local port(本地可用端口)占满后linux系统卡顿问题分析使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

@[TOC](local port(本地可用端口)占满后linux系统卡顿问题分析)

问题背景

作为NAT网关的一台机器在流量高峰期出现间歇性卡顿、丢包现象,排查发现PPS/带宽均在正常范围内,CPU间歇性全部被软中断占满。通过查询连接跟踪表发现,超过6W条连接指向同一个目的ip+端口,初步确认问题系因本地端口用尽,新建的连接无法获取可用端口导致的CPU飚高。
为了确认该原因,需对内核代码进行分析。重点梳理本地端口用尽时等待可用端口的逻辑。

代码位置

内核代码tcp协议实现中,tcp_ipv4.c中指定了协议的get_port函数为inet_csk_get_port。
inet_csk_get_port在源文件inet_connection_sock.c中实现。
该源文件分析的文章很多,不在此详述,可参考:
https://blog.csdn.net/dog250/article/details/5303572
https://blog.csdn.net/wangpengqi/article/details/9952783

重要逻辑

inet_csk_get_port函数中,随机选择一个端口开始判定,若不可用,则判定相邻端口。在遍历完全部设置的local port(NAT设置为1024-65535,共6w+个)后,若仍未找到可用端口,则重复开始,最多会重试5轮。
在本地端口用尽时的场景下,每个新建连接的数据包的到来,都会进行30W+次的查找操作。大流量下会造成软中断一直阻塞,CPU资源被占满的情况。
函数处理中首先通过local_bh_disable()关闭了softirq的下半段,函数返回前再开启。
至此,由同一目的连接数过多引起网关CPU飙升的逻辑得到确认。而同一目的连接数IP+端口的连接数受协议端口数的限制,单个源IP可支持的最大连接数的理论极限为65535,当前能够缓解该问题的唯一方案即增加源IP数量

附核心代码

int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
	//得到该sock对应协议族的全局的底层容器 hashinfo = tcp_hashinfo ,
	//其中它在struct proto tcp_prot内部初始化。而tcp_hashinfo的部分成
	//员是在 tcp_init()函数内部初始化,要搞清楚这里的关系,一定要查看 
	//tcp_init() 函数内部的实现
	struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo; 
	struct inet_bind_hashbucket *head;
	struct inet_bind_bucket *tb;
	int ret, attempts = 5;
	struct net *net = sock_net(sk);
	int smallest_size = -1, smallest_rover;
	kuid_t uid = sock_i_uid(sk); //获取当前socket对应的用户id
 
	local_bh_disable();
	//端口无效(我们的应用程序在开发的时候配置的无效端口,所以这里会随机
	//分配一个),这种情况就是随机绑定一个没有使用的端口
	if (!snum) { //端口无效
		int remaining, rover, low, high;
 
again:
		inet_get_local_port_range(&low, &high); //获取端口范围,一般就是1到65535,就是我们常用的端口号范围,当然也可以自己配置
		remaining = (high - low) + 1; //剩余端口个数
		smallest_rover = rover = net_random() % remaining + low; //随机分配一个数字作为端口
 
		smallest_size = -1;
		do {
			//是否是保留的端口
			if (inet_is_reserved_local_port(rover)) 
				goto next_nolock; //如果是保留的端口就切换到下一个,即++rover
 
			//通过端口号,即哈希值,确定其所在的链表head
			head = &hashinfo->bhash[inet_bhashfn(net, rover,
					hashinfo->bhash_size)];
 
			/* 锁住哈希桶 */ 
			spin_lock(&head->lock);
 
			/* 从头遍历哈希桶,在inet_bind_bucket_for_each函数内部运用了
			   container_of机制,通过指针成员获取其对应的结构体,这里既是tb*/
			inet_bind_bucket_for_each(tb, &head->chain)
				 /* 如果端口被使用了,就进行冲突检测 */
				if (net_eq(ib_net(tb), net) && tb->port == rover) { 
					if (((tb->fastreuse > 0 && //tb中的参数可“快速重用”
					      sk->sk_reuse && //socket参数允许快速重用
					      sk->sk_state != TCP_LISTEN) || //不在监听状态
					     (tb->fastreuseport > 0 &&
					      sk->sk_reuseport &&
					      uid_eq(tb->fastuid, uid))) && //socket用户id相等
					    (tb->num_owners < smallest_size || smallest_size == -1)) { 
						smallest_size = tb->num_owners; /* 记下这个端口使用者的个数 */  
						smallest_rover = rover; /* 记下这个端口 */
 
						/* 如果系统绑定的端口已经很多了,那么就判断端口是否有绑定冲突*/  
						if (atomic_read(&hashinfo->bsockets) > (high - low) + 1 &&
						    !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
							snum = smallest_rover; /* 没有冲突,使用此端口 */  
							goto tb_found;
						}
					}
 
					/* 检查是否有端口绑定冲突,该端口是否能重用 */ 
					if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
						snum = rover;
						goto tb_found;
					}
					goto next; /* 此端口不可重用,看下一个 */  
				}
 
			/* 找到了没被用的端口,退出 */  
			break; //如果一个桶遍历过了,没有冲突的,那么就需要在下面建立一个inet_bind_bucket
		next:
			spin_unlock(&head->lock);
		next_nolock:
			if (++rover > high)
				rover = low;
		} while (--remaining > 0);
 
		/* Exhausted local port range during search?  It is not
		 * possible for us to be holding one of the bind hash
		 * locks if this test triggers, because if 'remaining'
		 * drops to zero, we broke out of the do/while loop at
		 * the top level, not from the 'break;' statement.
		 */
		ret = 1;
		if (remaining <= 0) {
			if (smallest_size != -1) {
				snum = smallest_rover;
				goto have_snum;
			}
			goto fail;
		}
		/* OK, here is the one we will use.  HEAD is
		 * non-NULL and we hold it's mutex.
		 */
		snum = rover; /* 自动选择的可用端口 */
	} else { /* 如果应用层有指定要绑定的端口 */ 
have_snum: //有端口
		/* 走到这里,表示用户已经自己绑定了端口
		 1. inet_bhashfn(net, snum, hashinfo->bhash_size): 计算struct inet_bind_hashbucket指针索引
		 2. head = &hashinfo->bhash[*]: 返回struct inet_bind_hashbucket hash桶指针,即端口所在的哈希桶 
		 3. inet_bind_bucket_for_each(tb, &head->chain):遍历当前hash桶内部的chain(hlist)链表,该链表
		 	上注册了已被绑定端口,通过该chain链表及node成员找到(运用container_of)找到所属的结构体,即
		 	结构体为tb (struct inet_bind_bucket),具体的端口绑定到链表详见inet_bind_bucket_create()函
		 	数内部的实现
		 4. net_eq(ib_net(tb), net) && tb->port == snum: 是否是同一个net[个人理解,这个应该是创建一个socket就对应一个net] && 端口是否相等 
		*/
		head = &hashinfo->bhash[inet_bhashfn(net, snum,
				hashinfo->bhash_size)];
		spin_lock(&head->lock);
		inet_bind_bucket_for_each(tb, &head->chain)
			if (net_eq(ib_net(tb), net) && tb->port == snum) //从hash链表里获取的端口与应用配置的端口相等?
				goto tb_found; /* 发现端口在用 */ 
	}
	tb = NULL;
	goto tb_not_found;
tb_found:
	 /* 端口上有绑定sock时 */  
	if (!hlist_empty(&tb->owners)) { //为NULL表示tb未被使用
		/* 这是强制的绑定啊,不管端口是否会绑定冲突!*/ 
		if (sk->sk_reuse == SK_FORCE_REUSE)
			goto success;
 
		//根据socket的参数判断当前的端口是否快速重用
		if (((tb->fastreuse > 0 &&
		      sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
		     (tb->fastreuseport > 0 &&
		      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
		    smallest_size == -1) {  /* 指定端口的情况 */
			goto success;
		} else {
			ret = 1;
			if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {  /* 端口绑定冲突 */
				/* 自动分配的端口绑定冲突了,再次尝试,最多重试5次。  
                 * 我觉得以下if不必要,因为自动选择时goto tb_found之前都有检测过了。 
                 */
				if (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
				     (tb->fastreuseport > 0 &&
				      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
				    smallest_size != -1 && --attempts >= 0) {
					spin_unlock(&head->lock);
					goto again;
				}
 
				goto fail_unlock;
			}
		}
	}
tb_not_found: //到这里表示在hash桶里面没有找到端口
	ret = 1;
	/* 申请和初始化一个inet_bind_bucket结构, 返回一个tb hash桶*/  
	if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
					net, head, snum)) == NULL)
		goto fail_unlock;
	if (hlist_empty(&tb->owners)) { //在inet_bind_bucket_create()函数内部tb->owners初始化为NULL
		if (sk->sk_reuse && sk->sk_state != TCP_LISTEN) //sk->sk_reuse变量在inet_create()函数内部初始化的
			tb->fastreuse = 1;
		else
			tb->fastreuse = 0;
		if (sk->sk_reuseport) { //端口重用
			tb->fastreuseport = 1; 
			tb->fastuid = uid; //用户id
		} else
			tb->fastreuseport = 0;
	} else {
		if (tb->fastreuse && //重用
		    (!sk->sk_reuse || sk->sk_state == TCP_LISTEN)) //禁止端口复用 || socket状态为监听
			tb->fastreuse = 0; //禁止重用
		if (tb->fastreuseport &&
		    (!sk->sk_reuseport || !uid_eq(tb->fastuid, uid))) //端口禁止重用 || 用户id不相等
			tb->fastreuseport = 0;
	}
success:
	/* 赋值icsk中的inet_bind_bucket */ 
	if (!inet_csk(sk)->icsk_bind_hash) //未绑定hash桶, 在下面的 inet_bind_hash()函数内部绑定
		inet_bind_hash(sk, tb, snum); //重要,将hash桶绑定到sk->sk_prot->h.hashinfo上
	WARN_ON(inet_csk(sk)->icsk_bind_hash != tb);
	ret = 0;
 
fail_unlock:
	spin_unlock(&head->lock);
fail:
	local_bh_enable();
	return ret;
}
EXPORT_SYMBOL_GPL(inet_csk_get_port);