开发内功修炼@张彦飞开发内功修炼@张彦飞

talk is cheap,
show me the code!

百看不如一练,动手测试单机百万连接的保姆级教程!

很多读者在看完百万 TCP 连接的系列文章之后,反馈问我有没有测试源码。也想亲自动手做出来体验体验。这里为大家的实践精神点赞。

测试百万连接我用到的方案有两种,今天用一篇文章都给大家分享出来。

  • 第一种是服务器端只开启一个进程,然后使用很多个客户端 ip 来连接
  • 第二种是服务器开启多个进程,这样客户端就可以只使用一个 ip 即可

为了能让大部分同学都能用最低的时间成本达成百万连接结果,飞哥写了 c、java、php 三种版本的源码。两个方案对应的代码地址如下:

方案一: https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test02
方案二: https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test03

鉴于整个实验做起来还是有点小复杂,本文会配合从头到尾讲述每一个试验步骤,让大家动手起来更轻松。本文描述的步骤适用于任意一种语言。建议大家有空都动手耍耍。

为什么非得要进行实验呢,因为我觉得只有动手实践过,很多东西才能真正掌握。这里引用下埃德加 · 戴尔提出的学习金字塔理论图。根据他的研究结果可以看出,实践要比单纯的阅读效率要高好几倍。

0.png

所以我的文章中很多都是在介绍理论的同时夹杂着实际动手的实验结果,这种方式写文章投入的时间成本要高很多。但是,我觉得值!

一、TCP 并发理论基础

在做这个实验之前,需要你具备一些理论基础。这些在之前的文章都单独详细讲过,这里我把它们都简单再概括一下。

1.1 服务器理论最大并发数

TCP连接四元组是由源IP地址、源端口、目的IP地址和目的端口构成。

当四元组中任意一个元素发生了改变,那么就代表的是一条完全不同的新连接

我们算下服务器上理论上能达成的最高并发数量。拿我们常用的 Nginx 举例,假设它的 IP 是 A,端口80。这样就只剩下源IP地址、源端口是可变的。

IP 地址是一个 32 位的整数,所以源 IP 最大有 2 的 32 次方这么多个。 端口是一个 16 位的整数,所以端口的数量就是 2 的 16 次方。

2 的 32 次方(ip数)× 2的 16 次方(port数)大约等于两百多万亿。

所以理论上,我们每个 server 可以接收的连接上限就是两百多万亿。(不过每条 TCP 连接都会消耗服务器内存,实践中绝不可能达到这个理论数字,稍后我们就能看到。)

1.2 客户端理论最大并发数

注意:这里的客户端是一个角色,并不具体指的是哪台机器。当你的 java/c/go 程序响应用户请求的时候,它是服务端。当它访问 redis/mysql 的时候,你这台机器就变成客户端角色了。这里假设我们一台机器只用来当客户端角色。

我们再算一下客户端的最大并发数的上限。

很多同学认为一台 Linux 客户端最多只能发起 64 k 条 TCP 连接。因为TCP 协议规定的端口数量有 65535 个,但是一般的系统里 1024 以下的端口都是保留的,所以没法用。可用的大约就是 64 k 个。

但实际上客户端可以发出的连接远远不止这个数。咱们看看以下两种情况

情况1: 这个 64 k 的端口号实际上说的是一个 ip 下的可用端口号数量。而一台 Linux 机器上是可以配置多个 IP 的。假如配置了 20 个 IP,那这样一台客户端机就可以发起 120 万多个 TCP 连接了。

情况2: 再退一步讲,假定一台 Linux 上确实只有一个 IP,那它就只能发起 64 k 条连接了吗? 其实也不是的。

根据四元组的理论,只要服务器的 IP 或者端口不一样,即使客户端的 IP 和端口是一样的。这个四元组也是属于一条完全不同的新连接。

比如下面的两条连接里,虽然客户端的 IP 和端口完全一样,但由于服务器侧的端口不同,所以仍然是两条不同的连接。

  • 连接1:客户端IP 10000 服务器IP 10000
  • 连接2:客户端IP 10000 服务器IP 20000

所以一台客户端机器理论并发最大数是一个比服务器的两百万亿更大的一个天文数字(因为四元组里每一个元素都能变)。这里就不展开计算了,因为已经没有意义了。

1.3 Linux 最大文件描述符限制

linux 下一切皆文件,包括 socket。所以每当进程打开一个 socket 时候,内核实际上都会创建包括 file 在内的几个内核对象。该进程如果打开了两个 socket,那么它的内核对象结构如下图。

1_files.png

进程打开文件时消耗内核对象,换一句直白的话就是打开文件对象吃内存。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、进程级、用户进程级。

  • fs.file-max: 当前系统可打开的最大数量
  • fs.nr_open: 当前系统单个进程可打开的最大数量
  • nofile: 每个用户的进程可打开的最大数量

本文的实验要涉及对以上参数的修改。

1.4 TCP 连接的内存开销

介绍内存开销之前,需要先理解内核的内存使用方式。只有理解了这个,才能深刻理解 TCP 连接的内存开销。

Linux 内核和应用程序使用的是完全不同的两套机制。 Linux 给它的内核对象分配使用 SLAB 的方式。

一个 slab 一般由一个或者多个 Page 组成(每个 Page 一般为 4 KB)。在一个 slab 内只分配特定大小、甚至是特定的对象。这样当一个对象释放内存后,另一个同类对象可以直接使用这块内存。通过这种办法极大地降低了碎片发生的几率。

1_slab.png

Linux 提供了 slabtop 命令来按照占用内存从大往小进行排列,这对我们查看内核对象的内存开销非常方便。

在 Linux 3.10.0 版本中,创建一个socket 需要消耗 densty、flip、sock_inode_cache、TCP 四个内核对象。这些对象加起来总共需要消耗大约 3 KB 多一点的内存。

如果连接上有数据收发的话,还需要消耗发送、接收缓存区。这两个缓存区占用内存影响因素比较多,既受收发数据的大小,也受 tcp_rmem、tcp_wmem 等内核参数,还取决于服务器进程能否及时接收(及时接收的话缓存区就能回收)。总之影响因素比较多,不同业务之间实际情况差别太大,比较复杂。所以不在本文讨论范围之内。

二、方案一,多 IP 客户端发起百万连接

了解了理论基础后,可以开始准备实验了。

本文实验需要准备两台机器。一台作为客户端,另一台作为服务器。如果你选用的是 c 或者 php 源码,这两台机器内存只要大于 4GB 就可以。 如果使用的是 Java 源码,内存要大于 6 GB。对 cpu 配置无要求,哪怕只有 1 个核都够用。

本方案中采用的方法是在一台客户端机器上配置多个 ip 的方式来发起所有的 tcp 连接请求。所以需要为你的客户端准备 20 个 IP,而且要确保这些 IP 在内网环境中没有被其它机器使用。如果实在选不出这些 IP,那么可以直接跳到方案 2。

除了用 20 个 IP 以外,也可以使用 20 台客户端。每个客户端发起 5 万个连接同时来连接这一个server。但是这个方法实际操作起来太困难了。

客户端机和服务器分别下载源码: https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test02

下面我们来详细看每一个实验步骤。

2.1 调整客户端可用端口范围

默认情况下,Linux 只开启了 3 万多个可用端口。但我们今天的实验里,客户端一个进程要达到 5 万的并发。所以,端口范围的内核参数需要修改。

#vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000

执行 sysctl -p 使之生效。

2.2 调整客户端最大可打开文件数

我们要测试百万并发,所以客户端的系统级参数 fs.file-max 需要加大到 100 万。另外 Linux 上还会存在一些其它的进程要使用文件,所以我们需要多打一些余量出来,直接设置到 110 万。

对于进程级参数 fs.nr_open 来说,因为我们开启 20 个进程来测,所以它设置到 60000 就够了。这些都在 /etc/sysctl.conf 中修改。

#vi /etc/sysctl.conf
fs.file-max=1100000 
fs.nr_open=60000  

sysctl -p 使得设置生效。并使用 sysctl -a 查看是否真正 work。

#sysctl -p
#sysctl -a
fs.file-max = 1100000
fs.nr_open = 60000

接着再加大用户进程的最大可打开文件数量限制(nofile)。这两个是用户进程级的,可以按不同的用户来区分配置。 这里为了简单,就直接配置成所有用户 * 了。每个进程最大开到 5 万个文件数就够了。同样预留一点余地,所以设置成 55000。 这些是在 /etc/security/limits.conf 文件中修改。

注意 hard nofile 一定要比 fs.nr_open 要小,否则可能导致用户无法登陆。
# vi /etc/security/limits.conf
*  soft  nofile  55000  
*  hard  nofile  55000

配置完后,开个新控制台即可生效。 使用 ulimit 命令校验是否生效成功。

#ulimit -n
55000

2.3 服务器最大可打开文件句柄调整

服务器系统级参数 fs.file-max 也直接设置成 110 万。 另外由于这个方案中服务器是用单进程来接收客户端所有的连接的,所以进程级参数 fs.nr_open, 也一起改成 110 万。

#vi /etc/sysctl.conf
fs.file-max=1100000 
fs.nr_open=1100000  

sysctl -p 使得设置生效。并使用 sysctl -a 验证是否真正生效。

接着再加大用户进程的最大可打开文件数量限制(nofile),也需要设置到 100 万以上。

# vi /etc/security/limits.conf
*  soft  nofile  1010000  
*  hard  nofile  1010000

配置完后,开个新控制台即可生效。 使用 ulimit 命令校验是否成功生效。

2.4 为客户端配置额外 20 个 IP

假设可用的 ip 分别是 CIP1,CIP2,......,CIP20,你也知道你的子网掩码。

注意:这 20 个 ip 必须不能和局域网的其它机器冲突,否则会影响这些机器的正常网络包的收发。

在客户端机器上下载的源码目录 test02 中,找到你喜欢用的语言,进入到目录中找到 tool.sh。修改该 shell 文件,把 IPS 和 NETMASK 都改成你真正要用的。

为了确保局域网内没有这些 ip,最好先执行代码中提供的一个小工具来验证一下

make ping

当所有的 ip 的 ping 结果均为 false 时,进行下一步真正配置 ip 并启动网卡。

make ifup

使用 ifconfig 命令查看 ip 是否配置成功。

#ifconfig
eth0
eth0:0
eth0:1
...
eth:19

2.5 开始实验

ip 配置完成后,可以开始实验了。

在服务端中的 tool.sh 中可以设置服务器监听的端口,默认是 8090。启动 server

make run-srv

使用 netstat 命令确保 server 监听成功。

netstat -nlt | grep 8090
tcp  0   0.0.0.0:8090  0.0.0.0:*  LISTEN

在客户端的 tool.sh 中设置好服务器的 ip 和端口。然后开始连接

make run-cli

同时,另启一个控制台。使用 watch 命令来实时观测 ESTABLISH 状态连接的数量。

实验过程中不会一帆风顺,可能会有各种意外情况发生。 这个实验我前前后后至少花了有一周时间,所以你也不要第一次不成功就气馁。 遇到问题根据错误提示看下是哪里不对。然后调整一下,重新做就是了。 重做的时候需要重启客户端和服务器。

对于客户端,杀掉所有的客户端进程的方式是

make stop-cli

对于服务器来说由于是单进程的,所以直接 ctrl + c 就可以终止服务器进程了。 如果重启发现端口被占用,那是因为操作系统还没有回收,等一会儿再启动 server 就行。

当你发现连接数量超过 100 万的时候,你的实验就成功了。

watch "ss -ant | grep ESTABLISH"
1000013

这个时候别忘了查看一下你的服务端、客户端的内存开销。

先用 cat proc/meminfo 查看,重点看 slab 内存开销。

$ cat /proc/meminfo
MemTotal:        3922956 kB
MemFree:           96652 kB
MemAvailable:       6448 kB
Buffers:           44396 kB
......
Slab:          3241244KB kB

再用 slabtop 查看一下内核都是分配了哪些内核对象,它们每个的大小各自是多少。

2_slabtop.png

如果发现你的内核对象和上图不同,也不用惊慌。因为不同版本的 Linux 内核使用的内核对象名称和数量可能会有些许差异。

2.6 结束实验

实验结束的时候,服务器进程直接 ctrl + c 取消运行就可以。客户端由于是多进程的,可能需要手工关闭一下。

make stop-cli

最后记得取消为实验临时配置的新 ip

make ifdown

三、方案二,单 IP 客户端发起百万连接

如果不纠结于非得让一个 Server 进程达成百万连接,只要是 Linux 服务器上总共能达到就行,那么就还有另外一个方法。

那就是在服务器端的 Linux 上开启多个 server,每个 server 都监听不同的端口。然后在客户端也启动多个进程来连接。每一个客户端进程都连接不同的 server 端口。客户端上发起连接时只要不调用 bind,那么一个特定的端口是可以在不同的 server 之间复用的。

同样,实验源码也有 c、java、php 三个语言的版本。
准备好两台机器。一台作为客户端,另一台作为服务器。分别下载如下源码:

https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test03

3.1 调整可用端口范围

同方案一,客户端机端口范围的内核参数也是需要修改的。

#vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000

执行 sysctl -p 使之生效。

3.2 客户端加大最大可打开文件数

同方案一,客户端的 fs.file-max 也需要加大到 110 万。进程级的参数 fs.nr_open 设置到 60000。

#vi /etc/sysctl.conf
fs.file-max=1100000 
fs.nr_open=60000  

sysctl -p 使得设置生效。并使用 sysctl -a 查看是否真正生效

客户端的 nofile 设置成 55000

# vi /etc/security/limits.conf
*  soft  nofile  55000  
*  hard  nofile  55000

配置完后,开个新控制台即可生效。

3.3 服务器最大可打开文件句柄调整

同方案一,调整服务器最大可打开文件数。不过方案二服务端是分了 20 个进程,所以 fs.nr_open 改成 60000 就足够了。

#vi /etc/sysctl.conf
fs.file-max=1100000 
fs.nr_open=60000  

sysctl -p 使得设置生效。并使用 sysctl -a 验证。

接着再加大用户进程的最大可打开文件数量限制(nofile),这个也是 55000。

# vi /etc/security/limits.conf
*  soft  nofile  55000  
*  hard  nofile  55000
再次提醒: hard nofile 一定要比 fs.nr_open 要小,否则可能导致用户无法登陆。

配置完后,开个新控制台即可生效。

3.4 开始实验

启动 server

make run-srv 

使用 netstat 命令确保 server 监听成功。

netstat -nlt | grep 8090
tcp  0  0  0.0.0.0:8100  0.0.0.0:*  LISTEN
tcp  0  0  0.0.0.0:8101  0.0.0.0:*  LISTEN
......
tcp  0  0  0.0.0.0:8119  0.0.0.0:*  LISTEN

回到客户端机器,修改 tool.sh 中的服务器 ip。端口会自动从 tool.sh 中加载。然后开始连接

make run-cli

同时,另启一个控制台。使用 watch 命令来实时观测 ESTABLISH 状态连接的数量。

期间如果做失败了,需要重新开始的话,那需要先杀掉所有的进程。在客户端执行 make stop-cli,在服务器端是执行 make stop-srv。重新执行上述步骤。

当你发现连接数量超过 100 万的时候,你的实验就成功了。

watch "ss -ant | grep ESTABLISH"
1000013

记得查看同样一下你的服务端、客户端的内存开销。

# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 6.2 (Santiago)

# ss -ant | grep ESTAB |wc -l
1000013

# cat /proc/meminfo
MemTotal:        3925408 kB
MemFree:           97748 kB
Buffers:           35412 kB
Cached:           119600 kB
......
Slab:            3241528 kB

再用 slabtop 查看一下 top 内核对象。

3_slabtop.png

实验结束的时候,记得make stop-cli结束所有客户端进程,make stop-srv结束所有服务器进程。

最后,多扯一点

经过网络篇这几篇文章的学习,相信大家已经不会再觉得百万并发有多么的高深了。并发只是描述服务器端程序的指标之一,并不是全部

一条不活跃的 TCP 连接开销仅仅只是 3 KB 多点的内存而已。现代的一台服务器都上百 GB 的内存,如果只是说并发,单机千万(C10000K)都可以。

在互联网服务器端应用场景里,除了一些基于长连接的 push 场景以外。其它的大部分业务里讨论并发都要和业务结合起来。抛开业务逻辑单纯地说并发多高其实并没有太大的意义。 因为在这些场景中,服务器开销大头往往都不是连接本身,而是在每条连接上的数据收发、以及请求业务逻辑处理

这就好比你作为一个开发同学,在公司内建立了和十个产品经理的业务联系。这并不代表你的并发能力真的能达到十,很有可能是一位产品的需求就能把你的时间打满。

另外就是不同的业务之间,单纯比较并发也不一定有意义

假设同样的服务器配置,A 业务的单机能支撑 1 万并发,B 业务只能撑 1 千。这也并不一定就说明 A 业务的性能比 B 业务好。因为 B 业务的请求处理逻辑可能是相当的复杂,比如要进行复杂的压缩、加解密。而 A 业务的处理很简单内存读取个变量就返回了。

扩展说一下,本文配套代码中仅仅只是作为测试使用,所以写的比较简单。是直接阻塞式地 accept,将接收过来的新连接也雪藏了起来,并没有读写发生。

如果在你的项目实践中真的确实需要百万条 TCP 连接,那么一般来说还需要高效的 IO 事件管理。在 c 语言中,就是直接用 epoll 系列的函数来管理。对于 Java 语言来说,就是 NIO。(Golang 中不用操心,net 包中把 IO 事件管理都已经封装好了)

另外飞哥建立了一个技术群,欢迎大家到群里来进一步交流。先加我微信(zhangyanfei748528), 我来拉大家。

写在最后,由于我的这些知识在公众号里文章比较分散,很多人似乎没有理解到我对知识组织的体系结构。而且图文也不像视频那样理解起来更直接。所以我在知识星球上规划了视频系列课程,包括硬件原理、内存管理、进程管理、文件系统、网络管理、Golang语言、容器原理、性能观测、性能优化九大部分大约 120 节内容,每周更新。加入方式参见我要开始搞知识星球啦如何才能高效地学习技术,我投“融汇贯通”一票

Github:https://github.com/yanfeizhang/coder-kung-fu
关注公众号:微信扫描下方二维码
qrcode2_640.png

本原创文章未经允许不得转载 | 当前页面:开发内功修炼@张彦飞 » 百看不如一练,动手测试单机百万连接的保姆级教程!