如何做到十万TCP连接转发
问题描述
有一天我收到这么一个需求,在某业务设备端和业务服务器之间假设一个应用层代理服务器,
并设置了性能指标要求单个服务器支持至少 10 万 TCP 长连接。
文章图片
当时单个服务器支持 10 万 TCP 连接的问题已经有很多解决方案了,比如 nginx,
libevent。然而 TCP 转发服务这样既作为服务端,又作为客户端的场景,却也有其它问题
需要解决。再次记录遇到的问题,以及如何解决他们的。
原文链接
为什么会有这种需求?
以上只是问题的简化描述,TCP 长连接转发只是它的基本功能,在这之上,还会有别的工作,
比如:
- 将上游服务器转化为 TLS 服务器或者国密 SSN VPN 服务器。
- 提供安全功能,比如 IDS/IPS,防火墙的功能。
- 为设备和业务服务器提供协议转换,以求兼容。
- 处理 DDos 流量。
- 增加身份验证功能。
文件描述符限制问题 在 Linux 系统中,一个 TCP 连接就会占用一个文件描述符。 转发服务器里,每转发一个
TCP 连接就会占用 2 个文件描述符,其中一个代表下游和转发服务器的连接,另一个代表
转发服务器到上游业务服务器的连接。而文件描述符数量是有限的,使用下列命令查看:
$ ulimit -n
1024
大多数 Linux 发行版会显示 1024,这就是当前用户可以使用的文件描述符限制。要修改这
个限制,这个限制可以在
/etc/security/limits.conf
里修改,参考 这里 。# /etc/sysctl.conf
fs.nr_open=2000000
fs.file-max=2000000# /etc/security/limits.conf
* soft nofile 600000
* hard nofile 600000# 并设置开机运行: sysctl --system
我想这个限制应该广为流传了,以至于**云修改了他们虚拟机镜像,使得限制扩大为
65536。我也听过一些传闻,几年前某个订饭公司没有扩大这个参数,导致他们业务高峰时
再三出现异常,虽然找到了原因,但最后还是不出意外地黄了(别乱想,他们活不下去不是
技术原因)。
端口号的限制问题 实现的时候没多想,测试时遇到了这个问题。 TCP 头结构如下,
Source Port
长度是 2个字节,所以源端口范围是 \( [0..65535] \) 。
0123
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Source Port|Destination Port|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Sequence Number|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Acknowledgment Number|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Data ||U|A|P|R|S|F||
| Offset| Reserved|R|C|S|S|Y|I|Window|
|||G|K|H|T|N|N||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Checksum|Urgent Pointer|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Options|Padding|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|data|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+TCP Header Format
所以,最多只能有 6 万多个端口号,也就是 6 万多个客户端连接,离 10 万连接不是很远,
真是大惊喜! 而且默认情况下, Linux 上是不能用 6 万个端口号的,请看 Linux 中获取
空闲端口号的代码 \_\_inet\_hash\_connect :
inet_get_local_port_range(net, &low, &high);
high++;
/* [32768, 60999] -> [32768, 61000[ */
remaining = high - low;
if (likely(remaining > 1))
remaining &= ~1U;
net_get_random_once(table_perturb, sizeof(table_perturb));
index = hash_32(port_offset, INET_TABLE_PERTURB_SHIFT);
offset = (READ_ONCE(table_perturb[index]) + port_offset) % remaining;
/* In first pass we try ports of @low parity.
* inet_csk_get_port() does the opposite choice.
*/
offset &= ~1U;
other_parity_scan:
port = low + offset;
for (i = 0;
i < remaining;
i += 2, port += 2) {
if (unlikely(port >= high))
port -= remaining;
if (inet_is_local_reserved_port(net, port))
continue;
head = &hinfo->bhash[inet_bhashfn(net, port,
hinfo->bhash_size)];
spin_lock_bh(&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->l3mdev == l3mdev &&
tb->port == port) {
if (tb->fastreuse >= 0 ||
tb->fastreuseport >= 0)
goto next_port;
WARN_ON(hlist_empty(&tb->owners));
if (!check_established(death_row, sk,
port, &tw))
goto ok;
goto next_port;
}
}tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep,
net, head, port, l3mdev);
if (!tb) {
spin_unlock_bh(&head->lock);
return -ENOMEM;
}
tb->fastreuse = -1;
tb->fastreuseport = -1;
goto ok;
next_port:
spin_unlock_bh(&head->lock);
cond_resched();
}offset++;
if ((offset & 1) && remaining > 1)
goto other_parity_scan;
return -EADDRNOTAVAIL;
ok:
如你所见, Linux 查找端口号并不是从 0 开始的,而是从区间 \( [low, hight] \) 中查找。
区间范围可以通过下列命令查看:
$ cat /proc/sys/net/ipv4/ip_local_port_range
3276860999
跟代码注释里一样,是默认值 \( [32768, 61000] \) ,也就是说,只能有 3 万左右个客户端
连接。
这个其实好解决,修改内核参数即可:
# /etc/sysctl.conf
net.ipv4.ip_local_port_range = 2048 65535
# TIME_WAIT 状态的连接没必要保持了
net.ipv4.tcp_tw_reuse = 1
注意别设置
tcp_tw_recycle=1
,不然负载均衡器、或者 NAT 环境中会有问题。现在可以真的达到 6 万多连接了。 下面介绍如何扩展到 10 万连接以上。
在一个服务器中,下列元组确定一个 TCP 连接:
{ 源IP, 源端口, 目的IP,目的端口 }
最简单的就是给 TCP 连接转发服务器设置 2 个 IP 地址,每个 IP 地址可以使用 6 万多
个端口,这样就可以有 12 万连接了。
一般我们使用下面的代码进行 TCP 连接:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("quant67.com", 443))
使用
bind()
方法可以指定源 IP 和源端口号,这样就可以分散使用 2 个 IP 地址了:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("10.0.0.3", 1122))
s.connect(("quant67.com", 443))
仔细观察上面的 Linux 代码的判断条件,它查找的端口号需要满足下面的条件之一:
- 空闲的,没被占用。
- 被占用了,但是可以复用 (
check_established
)。
SO_REUSEADDR
。man 7 socketSO_REUSEADDR
Indicates that the rules used in validating addresses
supplied in a bind(2) call should allow reuse of local
addresses.For AF_INET sockets this means that a socket
may bind, except when there is an active listening socket
bound to the address.When the listening socket is bound
to INADDR_ANY with a specific port then it is not possible
to bind to this port for any local address.Argument is
an integer boolean flag.
简单地说,设置
SO_REUSEADDR
有三个用途:TIME_WAIT
状态的端口可以被bind
。- 当一个服务程序
bind
了0.0.0.0:8080
,另一个服务程序可以bind
特定IP的
同一个端口如10.0.0.3:8080
。 - 只要目的 IP 或者目的端口不同,就可以在不同的连接里
bind
相同的端口。
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("10.0.0.3", 0))
s.connect(("quant67.com", 443))
上面代码
bind
参数设置端口号为 0,表示让内核自动安排端口号。 与 connect()
的安排方法不同, 代码在 这里 。他会根据
SO_REUSEADDR
和 SO_REUSEPORT
是否设置,来判断冲突。只要目标 IP 不同,就可以复用源端口,因此上述代码可以达到超过 6 万的
连接数。
然而有个问题:调用
bind()
的时候,内核只知道源 IP,不知道目的 IP,所以实际会有一些冲突,
connect()
会报 EADDRNOTAVAIL
错误。这个问题可以通过哈希表解决冲突,然而实际冲突概率很小,如果冲突了,就重试更方便。
总结 【如何做到十万TCP连接转发】主要使用了下列方法提高代理连接数:
- 调整文件描述符限制
- 调整
ip_local_port_range
- 使用多个源 IP
- 使用
SO_REUSEADDR
推荐阅读
- 考研英语阅读终极解决方案——阅读理解如何巧拿高分
- 子龙老师语录
- 如何寻找情感问答App的分析切入点
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus使用queryWrapper如何实现复杂查询
- 如何在Mac中的文件选择框中打开系统隐藏文件夹
- 漫画初学者如何学习漫画背景的透视画法(这篇教程请收藏好了!)
- java中如何实现重建二叉树
- Linux下面如何查看tomcat已经使用多少线程
- thinkphp|thinkphp 3.2 如何调用第三方类库