Netty系列之Netty百万级推送服务设计要点
1.tcp连接(短链接和长连接)
什么是tcp连接?tcp(transmission control protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
当网络通信时采用tcp协议时,在真正的读写操作之前,server与client之间必须建立一个连接,当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,连接的建立是需要三次握手的,而释放则需要4次挥手,所以说每个连接的建立都是需要资源消耗和时间消耗的。
tcp短链接:短连接是指通信双方有数据交互时,就建立一个tcp连接,数据发送完成后,则断开此tcp连接(管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段)。
连接→数据传输→关闭连接;
tcp长连接:所谓长连接,指在一个tcp连接上可以连续发送多个数据包,在tcp连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(不发生rst(reset)包用于强制关闭tcp连接的包和四次挥手)。
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接(一个tcp连接通道多个读写通信);
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态。这就是tcp保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以释放客户端占用的资源。
心跳包:它像心跳一样每隔固定时间发一次,以此来告诉服务器,这个客户端还活着。用于保持长连接,至于这个包的内容,是没有什么特别规定的,不过一般都是很小的包,或者只包含包头的一个空包。在tcp的机制里面,本身是存在有心跳包的机制的,也就是tcp的选项:so_keepalive。系统默认是设置的2小时的心跳频率。但是它检查不到机器断电、网线拔出、防火墙这些断线。心跳包主要也就是用于长连接的保活和断线处理。一般的应用下,判定时间在30-40秒比较不错。如果实在要求高,那就在6-9秒。
应用场景:长连接多用于操作频繁(读写),点对点的通讯,而且连接数不能太多情况。每个tcp连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就ok了,不用建立tcp连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
而像web网站的http服务一般都用短链接(http1.0只支持短连接,http1.1keep alive 带时间,操作次数限制的长连接),因为长连接对于服务端来说会耗费一定的资源,而像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好;
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,以及报文开始结束符号,再根据这个长度去读相应长度的报文。
2.单台服务器支持最大的tcp连接数
在tcp应用中,server事先在某个固定端口监听,client主动发起连接,经过三路握手后建立tcp连接。那么对单机,其最大并发tcp连接数是多少?
如何标识一个tcp连接:系统用一个4四元组来唯一标识一个tcp连接:{local ip, local port,remote ip,remote port}。
client最大tcp连接数:client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short,因此本地端口个数最大只有65536,端口0有特殊含义,不能使用,这样可用端口最多只有65535,所以在全部作为client端的情况下,客户端发起的最大tcp连接数为65535,这些连接可以连到不同的server ip。
server最大tcp连接数:server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的so_reuseaddr选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对ipv4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方。
实际的tcp连接数:上面给出的是理论上的单机最大连接数,在实际环境中,受到机器资源、操作系统等的限制,特别是sever端,其最大并发tcp连接数远不能达到理论上限。在unix/linux下限制连接数的主要因素是内存和允许的文件描述符个数(每个tcp连接都要占用一定内存,每个socket就是一个文件描述符),另外1024以下的端口通常为保留端口。在默认2.6内核配置下,经过试验,每个socket占用内存在15~20k之间。
对server端,通过增加内存、修改最大文件描述符个数等参数,单机最大并发tcp连接数超过10万 是没问题的,国外 urban airship 公司在产品环境中已做到 50 万并发 。在实际应用中,对大规模网络应用,还需要考虑c10k 问题。我们先假设单台服务器最多只能支持万级并发连接,其实对绝大多数应用来说已经远远足够了,但是对于一些拥有很大用户基数的互联网公司,往往面临的并发连接数是百万,千万,甚至腾讯的上亿(注:qq默认用的udp协议)。虽然现在的集群,分布式技术可以为我们将并发负载分担在多台服务器上,那我们只需要扩展出数十台电脑就可以解决问题,但是我们更希望能更大的挖掘单台服务器的资源,先努力垂直扩展,再进行水平扩展,这样可以有效的节省服务器相关的开支(硬件资源,机房,运维,电力其实也是一笔不小的开支)。
补充知识:
一、文件句柄限制
在linux下编写网络服务器程序时每一个tcp连接都要占一个文件描述符,一旦这个文件描述符使用完了,新的连接到来返回给我们的错误是“socket/file:can't open so many files”即文件打开过多!。
这时你需要明白操作系统对可以打开的最大文件数的限制。
-
进程限制
-
执行 ulimit -n 输出 1024,说明对于一个进程而言最多只能打开1024个文件,所以你要采用此默认配置最多也就可以并发上千个tcp连接。
-
临时修改:ulimit -n 1000000,但是这种临时修改只对当前登录用户目前的使用环境有效,系统重启或用户退出后就会失效。
-
重启后失效的修改(不过我在centos 6.5下测试,重启后未发现失效):编辑 /etc/security/limits.conf 文件, 修改后内容为
* soft nofile 1000000
* hard nofile 1000000
-
永久修改:编辑/etc/rc.local,在其后添加如下内容
ulimit -shn 1000000
-
-
全局限制
-
执行 cat /proc/sys/fs/file-nr 输出
9344 0 592026
,分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数。但在kernel 2.6版本中第二项的值总为0,这并不是一个错误,它实际上意味着已经分配的文件描述符无一浪费的都已经被使用了 。 -
我们可以把这个数值改大些,用 root 权限修改 /etc/sysctl.conf 文件:
fs.file-max = 1000000
net.ipv4.ip_conntrack_max = 1000000
net.ipv4.netfilter.ip_conntrack_max = 1000000
-
二、端口号范围限制?
操作系统上端口号1024以下是系统保留的,从1024-65535是用户使用的。由于每个tcp连接都要占一个端口号,所以我们最多可以有60000多个并发连接。我想有这种错误思路朋友不在少数吧?(其中我过去就一直这么认为)
-
如何标识一个tcp连接:系统用一个4四元组来唯一标识一个tcp连接:{local ip, local port,remote ip,remote port}。好吧,我们拿出《unix网络编程:卷一》第四章中对accept的讲解来看看概念性的东西,第二个参数cliaddr代表了客户端的ip地址和端口号。而我们作为服务端实际只使用了bind时这一个端口,说明端口号65535并不是并发量的限制。
-
server最大tcp连接数:server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的so_reuseaddr选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对ipv4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方。
三、总结
tcp/ip 协议规定的,只用了2个字节表示端口号。容易让人误解为1个server只允许连接65535个client。
typedef struct _network_address_ip
{ ushort sin_port;//0~65535
ulong in_addr;
uchar sin_zero[8];
} network_address_ip, *pnetwork_address_ip;
(1)其实65535这个数字,只是决定了服务器端最多可以拥有65535个bind的socket。也就是说,最多可以开65535个服务器进程,但是你要知道这个能够连接客户端的数量没有任何关系,accept过来的socket是不需要bind任何ip地址的,也没有端口占用这一说。作为server端的socket本身只负责监听和接受连接操作。
(2)tcp协议里面是用[源ip+源port+目的ip+目的 port]来区别两个不同连接,所以连入和连出是两个不同的概念。连出connect就不错了,需要生成随机端口,这个是有限的连入的话, 因socket的分配受内存分页限制,而连接受限制(windows)。
(3)所以,千万不要误以为1个server只允许连接65535个client。记住,tcp连出受端口限制,连入仅受内存限制。
例如server,ip:192.168.16.254,port:8009
client1:ip:192.168.16.1,port:2378
client2:ip:192.168.16.2,port:2378
client1和client2虽然port相同,但是ip不同,所以是不同的连接。
(4)想让1个server并发高效得连接几万个client,需要使用iocp“完成端口(completion port)”的技术。
上面给出的结论都是理论上的单机tcp并发连接数,实际上单机并发连接数肯定要受硬件资源(内存)、网络资源(带宽)的限制。
这种单台机器10w并发,不考虑内存cpu的实现,主要是程序网络模型的选择。项目在github上有提供https://github.com/yaocoder/hpnetserver
linux系统优化设置调整参见:https://www.cnblogs.com/duanxz/p/4464178.html
3.too many open files(打开的文件过多)解决方法
netty作为通讯服务器,发生了打开的文件过多异常,之后在有新的连接就无法连接上来,导致程序崩溃。首先要检查程序断开后是否释放资源,确认无误后在调整文件句柄限制。
too many open files(打开的文件过多)是linux系统中常见的错误,从字面意思上看就是说程序打开的文件数过多,不过这里的files不单是文件的意思,也包括打开的通讯链接(比如socket),正在监听的端口等等,所以有时候也可以叫做句柄(handle),这个错误通常也可以叫做句柄数超出系统限制。
引起的原因就是进程在某个时刻打开了超过系统限制的文件数量以及通讯链接数,通过命令ulimit -a可以查看当前系统设置的最大句柄数是多少:
[dsuser@test02-ds-gps01 ~]$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15220
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
posix message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 10240
cpu time (seconds, -t) unlimited
max user processes (-u) 1024
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
open files那一行就代表系统目前允许单个进程打开的最大句柄数,这里是1024。
[dsuser@test02-ds-gps01 ~]$ jps -l
9314 com.hns.gps.gw.jt808.app.main
17135 sun.tools.jps.jps
使用命令lsof -p 进程id可以查看单个进程所有打开的文件详情,使用命令lsof -p 进程id | wc -l可以统计进程打开了多少文件:
[dsuser@test02-ds-gps01 ~]$ lsof -p 9314 | wc -l
226
以目前的通讯程序为例,可以看到它目前打开了226个文件数,如果文件数过多使用lsof -p 进程id命令无法完全查看的话,可以使用lsof -p 进程id > openfiles.log将执行结果内容输出到日志文件中查看。
解决方法:
1、增大允许打开的文件数——命令方式
ulimit -n 2048
这样就可以把当前用户的最大允许打开文件数量设置为2048了,但这种设置方法在重启后会还原为默认值。
ulimit -n命令非root用户只能设置到4096。
想要设置到8192需要sudo权限或者root用户。
2、增大允许打开的文件数——修改系统配置文件
vim /etc/security/limits.conf
#在最后加入
* soft nofile 4096
* hard nofile 4096
或者只加入
* - hard nofile 8192
最前的 * 表示所有用户,可根据需要设置某一用户,例如
dsuser soft nofile 8192
dsuser hard nofile 8192
注意”nofile”项有两个可能的限制措施。就是项下的hard和soft。 要使修改过得最大打开文件数生效,必须对这两种限制进行设定。 如果使用”-“字符设定, 则hard和soft设定会同时被设定。
3、检查程序问题
如果你对你的程序有一定的解的话,应该对程序打开文件数(链接数)上限有一定的估算,如果感觉数字异常,请使用第一步的lsof -p 进程id > openfiles.log命令,获得当前占用句柄的全部详情进行分析,
1)打开的这些文件是不是都是必要的?
2)定位到打开这些文件的代码
3)是否程序操作了文件写入,但是没有进行正常关闭
4)是否程序进行了通讯,但是没有正常关闭(也就是没有超时结束的机制)
如果程序中存在这些问题的话,无论系统句柄数设置的多么大,随着时间的推移,也一定会占用完。
总结完毕!延伸阅读:netty系列之netty百万级推送服务设计要点 (https://www.cnblogs.com/ruixueyan/p/6382770.html)
上一篇: 高性能索引策略二