当前位置: 首页 > Linux

如何实现10万个TCP连接转发

时间:2023-04-06 02:37:21 Linux

问题描述某天接到这样一个需求,假设业务设备和业务服务器之间有一个应用层代理服务器,性能指标设置为要求单台服务器至少支持1010,000个TCP长连接。当时针对单台服务器支持10万个TCP连接的问题有很多解决方案,比如nginx、libevent。但是,在TCP转发服务同时作为服务端和客户端的场景下,还有其他问题需要解决。再次记录遇到的问题以及它们是如何解决的。为什么需要原始链接?以上只是对问题的简单描述。TCP长连接转发只是它的基本功能。除此之外,还会有其他的工作,比如:将上游服务器转换为TLS服务器或者国密SSNVPN服务器。提供安全功能,如IDS/IPS、防火墙功能。为设备和业务服务器提供协议转换以实现兼容性。处理DDos流量。添加身份验证功能。文件描述符限制问题在Linux系统中,一个TCP连接会占用一个文件描述符。在转发服务器中,每转发一个TCP连接,都会占用两个文件描述符,一个代表下游与转发服务器的连接,一个代表转发服务器与上游业务服务器的连接。文件描述符的数量是有限制的,使用如下命令查看:$ulimit-n1024大多数Linux发行版都会显示1024,这是当前用户可用的文件描述符的限制。要修改此限制,可以在/etc/security/limits.conf中修改此限制,请参阅此处。#/etc/sysctl.confs.nr_open=2000000fs.file-max=2000000#/etc/security/limits.conf*softnofile600000*hardnofile600000#并设置启动操作:sysctl--system我觉得这个限制应该bewide流传甚广,以至于**云修改了他们的虚拟机镜像,将限制扩大到65536。我也听过一些传闻,说某家外卖公司几年前没有扩大这个参数,导致期间经常出现异常他们的业务达到顶峰。下降不是技术原因)。实现的时候没多想端口号的限制,但是在测试的时候遇到了这个问题。TCP头结构如下,SourcePort长度为2字节,所以源端口范围为\([0..65535]\)。012301234567890123456789012345678901+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|源端口|目的港|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|序列号|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|确认号|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|资料||U|A|P|R|S|F|||抵消|保留|R|C|S|S|Y|I|窗口||||G|K|H|T|N|N||+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|校验和|紧急指针|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|选项|填充|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|数据|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+TCPHeaderFormat所以最多只能有6万多个端口号,也就是6万多个客户端连接,不会很远来自100,000个连接,多么大的惊喜!并且默认60000个端口号在linux上无法使用,请参考linux中的\_\_inet\_hash\_connect代码获取空闲端口号:inet_get_local_port_range(net,&low,&high);高++;/*[32768,60999]->[32768,61000[*/remaining=high-low;如果(可能(剩余>1))剩余&=~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)剩余百分比;/*在第一遍中,我们尝试@low奇偶校验端口。*inet_csk_get_port()做相反的选择。*/偏移&=~1U;other_parity_scan:port=low+offset;for(i=0;i=high))port-=remaining;如果(inet_is_local_reserved_port(net,port))继续;head=&hinfo->bhash[inet_bhashfn(net,port,hinfo->bhash_size)];spin_lock_bh(&head->lock);/*不用理会rcv_saddr检查,因为*建立的检查已经足够独特了。*/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)gotonext_port;WARN_ON(hlist_empty(&tb->owners));如果(!check_established(death_row,sk,端口,&tw))转到确定;转到下一个端口;}}tb=inet_bind_bucket_create(hinfo->bind_bucket_cachep,net,head,port,l3mdev);如果(!tb){spin_unlock_bh(&head->lock);返回-ENOMEM;}tb->fastreuse=-1;tb->fastreuseport=-1;转到ok;next_port:spin_unlock_bh(&head->lock);cond_resched();偏移量++;if((offset&1)&&remaining>1)gotoother_parity_scan;返回-EADDRNOTAVAIL;ok:可以看到,linux搜索端口号并不是从0开始,而是从区间\([low,hight]\)开始寻找区间范围,可以通过以下命令查看:$cat/proc/sys/net/ipv4/ip_local_port_range3276860999和代码注释中一样,是默认值\([32768,61000]\),也就是说只能有3万左右的客户端连接。这个其实很容易解决,修改内核参数即可:#/etc/sysctl.confnet.ipv4.ip_local_port_range=204865535#不需要让连接一直处于TIME_WAIT状态。均衡器或NAT环境会出现问题。现在我们真的可以达到60000多个连接。下面介绍如何扩展到超过100,000个连接。在一个服务器中,下面的元组决定一个TCP连接:{源IP,源端口,目的IP,目的端口}最简单的就是为TCP连接转发服务器设置2个IP地址,每个IP地址可以使用6万多个端口,所以你可以有120,000个连接。一般我们TCP连接使用如下代码:s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("quant67.com",443))使用bind()方法指定源IP和源端口号,以便可以分配两个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。man7socketSO_REUSEADDR指示用于验证bind(2)调用中提供的地址的规则应允许重用本地地址。对于AF_INET套接字,这意味着套接字可以绑定,除非存在绑定到地址的活动侦听套接字。当侦听套接字绑定到具有特定端口的INADDR_ANY时,则无法为任何本地地址绑定到该端口。参数是一个整数布尔标志。简单地说,设置SO_REUSEADDR有三个目的:TIME_WAIT状态端口可以被绑定。当一个服务程序绑定0.0.0.0:8080时,另一个服务程序可以为特定IP例如10.0.0.3:8080绑定相同的端口。只要目的IP或目的端口不同,就可以在不同的连接中绑定同一个端口。代码如下: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))上面代码绑定参数设置端口号为0,意思是让内核自动安排端口号。与排列的connect()方法不同,代码在这里。他会根据是否设置了SO_REUSEADDR和SO_REUSEPORT来判断冲突。只要目的IP不同,源端口就可以复用,所以上面的代码可以达到60000多个连接。但是有一个问题:在调用bind()时,内核只知道源IP,不知道目的IP,所以实际上会产生一些冲突,connect()会报EADDRNOTAVAIL错误。这个问题可以通过哈希表来解决。但是,实际发生冲突的概率很小。如果有冲突,重试更方便。总结以下方法主要用于增加代理连接数:调整文件描述符限制调整ip_local_port_range使用多个源IP使用SO_REUSEADDR