一个问题引发的Linux端口选择算法思考

问题是这样的:在并发并不高的反向代理环境下,用户连接出现失败。因为服务器主动关闭上游连接,并处于TIMEWAIT状态,代理在新建上游连接时,复用了服务器上处于TIMEWAIT状态的五元组源端口。虽然代理上游客户端没有bind操作,但为什么代理上刚被释放的端口这么快又被复用了,查代码,原因是系统使用的某应用层协议栈在不bind端口情况下,采用random随机去选择端口,那么即使可用端口很多,仍存在刚被释放的端口在短时间被复用的可能。

应用层不进行端口选择的情况下,传输层如何进行端口选择?看一下Linux的实现,应用层不绑定端口,分为bind时端口选择为0,或不进行bind操作直接connect两种情况。

bind端口零情况

   int inet_csk_get_port(struct sock *sk, unsigned short snum)
    {
    	......
    	/* snum为0表示bind时未指定端口*/
    	if (!snum) {
    		int remaining, rover, low, high;
    
    again:
    		/* 获取net.ipv4.ip_local_port_range设置的端口范围*/
    		inet_get_local_port_range(&low, &high);
    		/* 获取可用端口个数*/
    		remaining = (high - low) + 1;
    		/* 端口号:在可用端口最小值基础上加随机偏移*/
    		smallest_rover = rover = net_random() % remaining + low;
    		/* 被引用次数最少的端口号被引用次数*/
    		smallest_size = -1;
    		do {
    			/* 根据net.ipv4.ip_local_reserved_ports排除保留端口*/
    			if (inet_is_reserved_local_port(rover))
    				goto next_nolock;
    			/* 获取该端口的bind hash节点头*/
    			head = &hashinfo->bhash[inet_bhashfn(net, rover,
    					hashinfo->bhash_size)];
    			spin_lock(&head->lock);
    			/* 遍历该端口的bind hash链*/
    			inet_bind_bucket_for_each(tb, &head->chain)
    				/* 如果在同一个网络空间该端口已被使用,
    				     需要判断端口是否可重用*/
    				if (net_eq(ib_net(tb), net) && tb->port == rover) {
    					/* sk_reuse和sk_reusepot分别由SO_REUSEADDR和SO_REUSEPORT确定。可重用条件为:
    					     1. 该bind bucket节点使能fastreuse(由该节点首个sk的sk_reuse确定),同时sk
    					     使能sk_reuse,同时sk状态不为TCP_LISTEN;
    					     2. 该bind bucket节点使能fastreuseport,同时sk使能sk_reuseport,同时两连接同
    					     用户; */
    					if (((tb->fastreuse > 0 &&
    					      sk->sk_reuse &&
    					      sk->sk_state != TCP_LISTEN) ||
    					     (tb->fastreuseport > 0 &&
    					      sk->sk_reuseport &&
    					      uid_eq(tb->fastuid, uid))) &&
    					     /* 并且该端口使用次数为目前最小,那么保存该端口。*/
    					    (tb->num_owners < smallest_size || smallest_size == -1)) {
    						smallest_size = tb->num_owners;
    						smallest_rover = rover;
    						/* 系统端口使用个数大于端口范围,必然存在复用,那么只要
    						目前选择的端口被inet_csk_bind_conflict判定为不冲突则可以使用。*/
    						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;
    						}
    					}
    					/* 无论是否配置SO_REUSEADDR或SO_REUSEPORT,但只要inet_csk_bind_conflict判定为
    					不冲突则该端口可复用*/
    					if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
    						snum = rover;
    						goto tb_found;
    					}
    					goto next;
    				}
    			/* 如果选取的端口未被使用,则直接跳出端口选择阶段。*/
    			break;
    		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;
    	}
    	......
    }

概括一下,在端口不复用情况下,端口随机选择;端口复用情况下,尽量选择使用量较少的端口。

不bind情况

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))
{
	......
	if (!snum) {
		int i, remaining, low, high, port;
		/* 一个静态变量作为偏移因子*/
		static u32 hint;
		/* port_offset为根据源地址、目的地址和端口算出的偏移量。*/
		u32 offset = hint + port_offset;
		struct inet_timewait_sock *tw = NULL;

		/* 获取net.ipv4.ip_local_port_range设置的端口范围*/
		inet_get_local_port_range(&low, &high);
		remaining = (high - low) + 1;

		local_bh_disable();
		for (i = 1; i <= remaining; i++) {
			/* 已最小端口为基础,加计算偏移量作为选择的端口*/
			port = low + (i + offset) % remaining;
			/* 根据net.ipv4.ip_local_reserved_ports排除保留端口*/
			if (inet_is_reserved_local_port(port))
				continue;
			/* 获取该端口的bind hash节点头*/
			head = &hinfo->bhash[inet_bhashfn(net, port,
					hinfo->bhash_size)];
			spin_lock(&head->lock);

			/* Does not bother with rcv_saddr checks,
			 * because the established check is already
			 * unique enough.
			 */
			inet_bind_bucket_for_each(tb, &head->chain) {
				/* 端口已被使用*/
				if (net_eq(ib_net(tb), net) &&
				    tb->port == port) {
					if (tb->fastreuse >= 0 ||
					    tb->fastreuseport >= 0)
						goto next_port;
					WARN_ON(hlist_empty(&tb->owners));
					/* 判断是否存在五元组冲突或TW连接可重用*/
					if (!check_established(death_row, sk,
								port, &tw))
						goto ok;
					goto next_port;
				}
			}
			/* 端口未被使用,选取并创建新的bind bucket节点*/
			tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
					net, head, port);
			if (!tb) {
				spin_unlock(&head->lock);
				break;
			}
			tb->fastreuse = -1;
			tb->fastreuseport = -1;
			goto ok;

		next_port:
			spin_unlock(&head->lock);
		}
		local_bh_enable();

		return -EADDRNOTAVAIL;

ok:
		/* 根据本次选择的端口偏移,增加静态因子hint,期待后续端口
		     选择递增*/
		hint += i;
	}
	......
}

概括一下,对于相同的五元组连接,总是在上一次端口选择基础上递增选取端口值。

总结一下

Linux为什么同时存在随机和线性递增两种端口选择方式不太明确,但在随机选取端口的情况下,开头所说的客户端端口被短时间复用的情况理论上仍存在,而且随着并发的上升,即使在线性递增选择端口情况下,也会存在同样的问题。

分析一下问题的解决思路,在HTTP反代环境下,可以考虑通过使能代理服务器HTTP长连接提高上游连接利用率,并且客户端主动关闭连接减少服务端TW连接数量;或使能时间戳同时服务器配合使能连接回收,让服务器TW状态连接快速回收;或代理服务器使能IP_TRANSPARENT套接字并绑定虚地址,尽量减少相同五元组信息的重复使用频率。

你可能感兴趣的:(传输层)