问题是这样的:在并发并不高的反向代理环境下,用户连接出现失败。因为服务器主动关闭上游连接,并处于TIMEWAIT状态,代理在新建上游连接时,复用了服务器上处于TIMEWAIT状态的五元组源端口。虽然代理上游客户端没有bind操作,但为什么代理上刚被释放的端口这么快又被复用了,查代码,原因是系统使用的某应用层协议栈在不bind端口情况下,采用random随机去选择端口,那么即使可用端口很多,仍存在刚被释放的端口在短时间被复用的可能。
应用层不进行端口选择的情况下,传输层如何进行端口选择?看一下Linux的实现,应用层不绑定端口,分为bind时端口选择为0,或不进行bind操作直接connect两种情况。
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;
}
......
}
概括一下,在端口不复用情况下,端口随机选择;端口复用情况下,尽量选择使用量较少的端口。
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套接字并绑定虚地址,尽量减少相同五元组信息的重复使用频率。