技术: 百万连接问题

探讨 CXXX K 问题的博主一大堆, 我个人对这个话题也比较感兴趣.

本文只论述一些我过去的认识错误或者容易混淆的东西. 毕竟最大连接数, 受限于软硬件处理能力. (软件只占其中一部分)

端口和连接数的谬论

关于最大连接数问题其实还附带了很多问题:

  • TCP端口号是16位无符号整数, 最大65535 (对外, 但是ipv4能用其中的部分, 大概3W多个)
  • TCP客户端(TCP的主动发起者)可以在同一 ip:port 上向不同的服务器发起主动连接, 只需在bind之前对socket设置SO_REUSEADDR选项.
    对于client端, 操作系统会自动根据不同的远端 ip:port, 决定是否重用本地端口
    cat /proc/sys/net/ipv4/ip_local_port_range
    32768 61000
  • 系统最大支持多少个连接 (fd数目,TCP 连接不再占用系统文件数; 据说现在有一些内核支持用户态运行TCP/IP协议栈):
    cat /proc/sys/fs/file-max
    580382
  • 单个进程呢
    ulimit -n
    1024
  • accept的连接使用的本地地址也是同样的 ip:port , 服务器是一直使用监听的ip:port来接受连接,那么理论上可以接受的客户端连接数量是很大的:

注: 通过修改/etc/sysctl.conf文件,在文件中添加如下行:
net.ipv4.ip_local_port_range = 1024 65000
然后重启服务sysctl -p, 可以修改本地端口号范围.

服务端同一个port, 接收N个连接(N在代表client的数目, 一般就是client_port client_ip数目; 但是client_port也限制在32758~61000范围内);
服务端3W多个port的话, 大概3W
N个连接; 再算上重用的话, 就非常多了(并且客户端连接不同的 ip:port 也会重用端口)

65536这种限制端口号的东西, 却拿来说连接数, 其中谬误, 不言自明.

用户态TCP/IP协议栈

内核的网络协议栈强调通用性,主要是为吞吐量优化(性能指标通常是 MB/s 或 packets per second),顺带兼顾大量并发连接。为了支持 C1000k,要调整内核参数让每个连接少占资源,这与内核代码的设计初衷是违背的。

用户态协议栈捅破了这层窗户纸,可以根据应用的特点来剪裁协议栈功能。优化也更直接,不再是调黑盒参数组合,而是直接上 profiling,根据结果修改应用程序和协议栈的代码。

用户态协议栈的吞吐量比不上内核,不过对 C1000k 的应用场合(例如 comet)应该不成问题。

下面要说的内容, 都是基于内核协议栈的TCP/IP连接问题, 即连接数和系统文件数相关

先说一些内核或者内核协议栈的限制.

最大连接数

首先, 还是强调一下, 基于TCP/IP内核协议栈.

此时最大连接数和下面的因素有关:

  • File Max
  • 物理&可用内存

最大打开文件数

内核中, 每个TCP连接都要创建一个socket句柄,每个socket句柄同时也是一个文件句柄.

对于绝大部分 Linux 操作系统, 默认情况下确实不支持 C1000K! 因为操作系统包含最大打开文件数(Max Open Files)限制, 分为系统全局的, 和进程级的限制.

全局

Linux系统级硬限制,所有用户级的打开文件数限制都不应超过这个数值.
cat /proc/sys/fs/file-max ; //大约是几十万级别(我的显示30W), 根据系统不同, 可能实现也不同

可以通过修改配置文件&重启服务, 支持更多:(比如修改成百万级别)
配置文件/etc/sysctl.conf, 将系统对最大跟踪的TCP连接数限制设置为100W.

1
2
3
fs.file-max = 1020000
net.ipv4.ip_conntrack_max = 1020000
net.ipv4.netfilter.ip_conntrack_max = 1020000

(网络内核对TCP连接的有关限制, 不仅仅只有这些)

重启服务sudo sysctl -p /etc/sysctl.conf

通常这个系统级硬限制是Linux系统在启动时根据系统硬件资源状况计算出来的最佳的最大同时打开文件数限制,如果没有特殊需要,不应该修改此限制,
除非想为用户级打开文件数限制设置超过此限制的值。

局部

也就是单进程打开的文件数目, 一般情况下, 应该是1024个.

1
2
ulimit -a  或
ulimit -n

这表示当前用户的每个进程最多允许同时打开1024个文件,这1024个文件中还得除去每个进程必然打开的标准输入,标准输出,标准错误,服务器监听 socket,进程间通讯的unix域socket等文件,那么剩下的可用于客户端socket连接的文件数就只有大概1024-10=1014个左右。也就是说缺省情况下,基于Linux的通讯程序最多允许同时1014个TCP并发连接。

要修改可以根据当前登录的用户进行修改(nofile)数目emacs /etc/security/limits.conf

1
2
3
# /etc/security/limits.conf
merlin hard nofile 1020000
merlin soft nofile 1020000

第一列的 merlin 表示用户 merlin, 你可以填 *, 或者 root. 然后保存退出, 重新登录服务器.

  • 软限制是指Linux在当前系统能够承受的范围内进一步限制用户同时打开的文件数
  • 硬限制则是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量

通常软限制小于或等于硬限制。

接着修改 /etc/pam.d/login 文件,在文件中添加如下行:

1
session required /lib/security/pam_limits.so

这是告诉Linux在用户完成系统登录后,应该调用 pam_limits.so 模块来设置系统对该用户可使用的各种资源数量的最大限制(包括用户可打开的最大文件数限制),
而 pam_limits.so 模块就会从 /etc/security/limits.conf 文件中读取配置来设置这些限制值。

如果重启后用 ulimit-n 命令查看用户可打开文件数限制仍然低于上述步骤中设置的最大值,这可能是因为在用户登录脚本/etc/profile中使用ulimit -n命令已经将用户可同时打开的文件数做了限制。由于通过ulimit-n修改系统对用户可同时打开文件的最大数限制时,新修改的值只能小于或等于上次 ulimit-n设置的值,因此想用此命令增大这个限制值是不可能的。所以,如果有上述问题存在,就只能去打开/etc/profile脚本文件,在文件中查找是否使用了ulimit-n限制了用户可同时打开的最大文件数量,如果找到,则删除这行命令,或者将其设置的值改为合适的值,然后保存文件,用户退出并重新登录系统即可。
通过上述步骤,就为支持高并发TCP连接处理的通讯处理程序解除关于打开文件数量方面的系统限制。

临时修改直接ulimit -n 1020000, 主要需要 root 权限.

一般情况下, 需要重新编译内核: /usr/include/linux/fs.h 限制了能设置的最大的常量数目NR_OPEN.

常见状况

在Linux上编写支持高并发TCP连接的客户端通讯处理程序时,有时会发现尽管已经解除了系统对用户同时打开文件数的限制,但仍会出现并发TCP连接数增加到一定数量时,再也无法成功建立新的TCP连接的现象。出现这种现在的原因有多种。

第一种原因可能是因为Linux网络内核对本地端口号范围有限制。
此时,进一步分析为什么无法建立TCP连接,会发现问题出在connect()调用返回失败,查看系统错误提示消息是“Can’t assign requestedaddress”。同时,如果在此时用tcpdump工具监视网络,会发现根本没有TCP连接时客户端发SYN包的网络流量。这些情况说明问题在于本地Linux系统内核中有限制。其实,问题的根本原因在于Linux内核的TCP/IP协议实现模块对系统中所有的客户端TCP连接对应的本地端口号的范围进行了限制(例如,内核限制本地端口号的范围为1024~32768之间)。当系统中某一时刻同时存在太多的TCP客户端连接时,由于每个TCP客户端连接都要占用一个唯一的本地端口号(此端口号在系统的本地端口号范围限制中),如果现有的TCP客户端连接已将所有的本地端口号占满,则此时就无法为新的TCP客户端连接分配一个本地端口号了,因此系统会在这种情况下在connect()调用中返回失败,并将错误提示消息设为“Can’t assignrequested address”。

第二种无法建立TCP连接的原因可能是因为Linux网络内核的IP_TABLE防火墙对最大跟踪的TCP连接数有限制。此时程序会表现为在 connect()调用中阻塞,如同死机,如果用tcpdump工具监视网络,也会发现根本没有TCP连接时客户端发SYN包的网络流量。由于 IP_TABLE防火墙在内核中会对每个TCP连接的状态进行跟踪,跟踪信息将会放在位于内核内存中的conntrackdatabase中,这个数据库的大小有限,当系统中存在过多的TCP连接时,数据库容量不足,IP_TABLE无法为新的TCP连接建立跟踪信息,于是表现为在connect()调用中阻塞。

/etc/sysctl.conf 是用来控制linux网络的配置文件,对于依赖网络的程序非常重要, 一般别乱动

内存限制

实际上是操作系统为了维护连接所需要的系统资源&内存占用, 可以简单的理解成存储这些文件描述(fd, 或者 socket) 所需要的内存(当然还有一些头信息, 结构体).

sizeof(int), 在64位机器(Ubuntu)下是4字节, 那么100W连接应该是占用4M, 加上管理结构体等信息载体, 也之多不过100M.

只是空连接, 即不包括收发数据的 buffer 内存, 基本也就那个样子, 这一点, 前辈已经做了验证了, 如下:

服务端创建10个端口, 模拟10台服务器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <sys/select.h>

#define MAX_PORTS 10

int main(int argc, char **argv){
struct sockaddr_in addr;
const char *ip = "0.0.0.0";
int opt = 1;
int bufsize;
socklen_t optlen;
int connections = 0;
int base_port = 7000;
if(argc > 2){
base_port = atoi(argv[1]);
}

int server_socks[MAX_PORTS];

for(int i=0; i<MAX_PORTS; i++){
int port = base_port + i;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((short)port);
inet_pton(AF_INET, ip, &addr.sin_addr);

int serv_sock;
if((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
goto sock_err;
}
if(setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1){
goto sock_err;
}
if(bind(serv_sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
goto sock_err;
}
if(listen(serv_sock, 1024) == -1){
goto sock_err;
}

server_socks[i] = serv_sock;
printf("server listen on port: %d\n", port);
}

//optlen = sizeof(bufsize);
//getsockopt(serv_sock, SOL_SOCKET, SO_RCVBUF, &bufsize, &optlen);
//printf("default send/recv buf size: %d\n", bufsize);

while(1){
fd_set readset;
FD_ZERO(&readset);
int maxfd = 0;
for(int i=0; i<MAX_PORTS; i++){
FD_SET(server_socks[i], &readset);
if(server_socks[i] > maxfd){
maxfd = server_socks[i];
}
}
int ret = select(maxfd + 1, &readset, NULL, NULL, NULL);
if(ret < 0){
if(errno == EINTR){
continue;
}else{
printf("select error! %s\n", strerror(errno));
exit(0);
}
}

if(ret > 0){
for(int i=0; i<MAX_PORTS; i++){
if(!FD_ISSET(server_socks[i], &readset)){
continue;
}
socklen_t addrlen = sizeof(addr);
int sock = accept(server_socks[i], (struct sockaddr *)&addr, &addrlen);
if(sock == -1){
goto sock_err;
}
connections ++;
printf("connections: %d, fd: %d\n", connections, sock);
}
}
}

return 0;
sock_err:
printf("error: %s\n", strerror(errno));
return 0;
}

服务器监听了 10 个端口, 这样一台测试机就可以和服务器之间创建 30 万左右个连接了.

因为只有一台客户端测试机, 最多只能跟同一个 IP 端口创建 30000 多个连接
cat /proc/sys/net/ipv4/ip_local_port_range

客户端很简单, 就是不停的创建连接:(客户端也得支持 C1000K配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>

/*7000 - 7009端口*/
int main(int argc, char **argv){
if(argc <= 2){
printf("Usage: %s ip port\n", argv[0]);
exit(0);
}

struct sockaddr_in addr;
const char *ip = argv[1];
int base_port = atoi(argv[2]);
int opt = 1;
int bufsize;
socklen_t optlen;
int connections = 0;

bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);

char tmp_data[10];
int index = 0;
while(1){
if(++index >= 10){
index = 0;
}
int port = base_port + index;
printf("connect to %s:%d\n", ip, port);

addr.sin_port = htons((short)port);

int sock;
if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1){
goto sock_err;
}
if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1){
goto sock_err;
}

connections ++;
printf("connections: %d, fd: %d\n", connections, sock);

if(connections % 10000 == 9999){
printf("press Enter to continue: ");
getchar();
}
usleep(1 * 1000);
/*
bufsize = 5000;
setsockopt(serv_sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
setsockopt(serv_sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
*/
}

return 0;
sock_err:
printf("error: %s\n", strerror(errno));
return 0;
}

10 万个连接, 这些连接是空闲的, 什么数据也不发送也不接收. 这时, 进程只占用了不到 1MB 的内存. 但是, 通过程序退出前后的 free 命令对比, 发现操作系统用了 200M(大致)内存来维护这 10 万个连接!

如果是百万连接的话, 操作系统本身就要占用 2GB 的内存! 也即 2KB 每连接.

查看进程内存占用

上面, 查看测试时和测试后系统的内存, 需要用到ps或者top, 一般用top

1
$ top

或者

1
cat /proc/meminfo

查看单个进程的内存占用, ps,(当然用top或者htop也可以) 例如:

1
2
3
$ ps aux | sort -k4nr | head -n 10 
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 146976 7392 ? Ss 10月08 0:01 /sbin/init splash

通过上面的测试代码, 可以发现, 应用程序维持百万个空闲的连接, 只会占用操作系统的内存, 通过 ps 命令查看可知, 应用程序本身几乎不占用内存.

毕竟是内核协议栈

控制tcp读写缓冲区大小

其实就是修改:
/proc/sys/net/ipv4/tcp_wmem 以及 /proc/sys/net/ipv4/tcp_rmem 的大小.

例如:

1
2
3
4
$ cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 6291456
$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304

(查看cpu前10, ps aux | sort -k3nr | head -n 10)

吞吐量

百万连接, 如果其中有20%是活跃的, 那么活跃连接数就是0.2M个(1000K * 0.2, 涉及硬件都这么算), 每个连接每秒传输 1KB 的数据,
那么需要的网络带宽是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求服务器至少是万兆网卡(10Gbps).

(个人觉得, 现在真正速度的限制不是网络了, 而是硬盘)

其他话题

这里基本就是调优内核了, 不是简单的设置参数限制, 而是采用更好的机制, 比如说异步IO.

IO问题

非阻塞同步IO, 异步IO, 来自内核层面对高并发的支持, 帮助在大量连接下提升服务器的处理能力, 简单的说就是cpu相应能力和降低内存占用.

(注意异步IO要比非阻塞事件I/O就绪通知更加彻底, 即asio比epoll更加独立)

不要让内核执行所有繁重的任务。将数据包处理,内存管理,处理器调度等任务从内核转移到应用程序高效地完成。
让Linux只处理控制层,数据层完全交给应用程序来处理。

内核规模不够,解决的办法是尽可能将业务移动到内核之外,并且自己处理所有繁重的业务. 这一点比如 epoll 的ET模式就很像, 除了触发问题, 尽可能自己解决.

或者说, 这个时候, 再去探究epoll模型等网络框架, 意义更加明确.

以epoll为例,在它的基础上抽象了一些开发框架和库,为广大软件开发者在软件开发带来了便利,比如libevent、libev等。随着当年在IO模型上的革命,衍生出了很多至今为止我们都在大量使用的优秀开源软件,比如nginx、haproxy、squid等,通过大量的创新、实践和优化,使我们在今天能够很轻易地解决一个大并发压力场景下的技术问题。

如果要看 select, 可以参考这位大佬的文章, 但robot love 也说了:

select 非获取性遍历开销问题使得它不被看好.

IO问题也可以参考我的文章网络IO, 以前探讨过.

调度问题

说到底还是内核问题, 或者说cpu亲和性问题, 探索的还是调度和分配问题, 这部分提升, 应该是靠算法, 避免(频繁)切换&调度的代价.

其他可以参考:
http://blog.csdn.net/erlib/article/details/50994440

总结

关于C1M问题, 前辈的原话:

Linux 系统需要修改内核参数和系统配置, 才能支持 C1000K. C1000K 的应用要求服务器至少需要 2GB 内存, 如果应用本身还需要内存, 这个要求应该是至少 10GB 内存. 同时, 网卡应该至少是万兆网卡. 当然, 这仅仅是理论分析, 实际的应用需要更多的内存和 CPU 资源来处理业务数据.

我个人觉得, 往深了研究, 还有很多地方可以优化; 但目前, 弄清楚异步IO以及在其基础上衍生的网络框架, 就可以应付绝大多数场景了.

参考

  1. http://www.ideawu.net/blog/archives/740.html
  2. http://blog.csdn.net/solstice/article/details/26363901
  3. https://github.com/ideawu/c1000k
  4. http://www.kegel.com/c10k.html
  5. http://www.csdn.net/article/2013-05-16/2815317-The-Secret-to-10M-Concurrent-Connections
文章目录
  1. 1. 端口和连接数的谬论
  2. 2. 用户态TCP/IP协议栈
  3. 3. 最大连接数
  4. 4. 最大打开文件数
    1. 4.1. 全局
    2. 4.2. 局部
    3. 4.3. 常见状况
  5. 5. 内存限制
    1. 5.1. 查看进程内存占用
    2. 5.2. 控制tcp读写缓冲区大小
  6. 6. 吞吐量
  7. 7. 其他话题
    1. 7.1. IO问题
    2. 7.2. 调度问题
  8. 8. 总结
  9. 9. 参考
|