维持大量并发连接是实时通信系统的关键能力之一,而要想测出一台服务器到底能支撑多少连接有时候会比较麻烦,需要涉及到好几个系统参数的调整,在这里希望能将遇到过的各种参数调整记录一下,以备后用。

以下所说连接均指 TCP 连接。

客户端连接数限制

首先需要明确一点是单个压测客户端(单个 IP)能承载的并发连接数是有限制的,这个上限是 65535。也就是说无论压测客户端所在机器性能有多强大,单个 IP 能和服务端建立的并发连接数就只有 65535 个,不可能更多。而从服务端角度来看,服务端能承受的并发连接数又远远不止 65535 个,只要服务器内存足够,CPU 足够强大,单机承载几十上百万的并发连接完全不是问题,所以我们经常能听到评价某些实时通信服务时候说单机能承担百万并发连接等。为什么从客户端和从服务端两个角度会得到不同的限制呢?

从网络协议层面看,需要有四个信息能唯一确定一条连接:Source IP + Source Port + Destination IP + Destination Port,客户端在创建 Socket 连接服务端的时候服务端的 Destination IP + Destination Port 是固定不变的,对客户端来说如果只有一个 IP 那 Source IP 也固定不变,能唯一确定一条连接的信息中只有 Source Port 可变,所以对客户端来说单个 IP 能创建的连接数完全取决于 Source Port 数量。而反观服务端,Destination IP 和 Destination Port 是固定的,但能连上服务端的客户端 Source IP 和 Source Port 都是可变的,所以服务端连接数量最多是 Source IP 乘以 Source Port 数量。

为了得到 Source Port 的限制数量我们可以看看 TCP Header 格式:

图片来自:https://tools.ietf.org/html/rfc793

从上图能看到 Source Port 和 Destination Port 都只有 16 个 bit,也就是说上限都是 65535 个。

如果使用的是 IPv4,IP 协议报文的 Header 如下:

图片来自:https://tools.ietf.org/html/rfc791

看到 Source Address 和 Destination Address 都是 32 bit,这支持的 IP 数量就多了去了在几十亿的级别。

所以,客户端单个 IP 能创建的连接上限是 65535 个,服务端单个 IP 能创建的连接数量按目前一般机器性能来看可以认为是无限。如果压测客户端性能强大,想单个客户端上与服务端创建超过 65535 个连接,唯一的方法就是扩展客户端 IP 数量,比如使用虚拟 IP

报错信息

当因为 IP 端口不足而无法建立连接时,Java 的话会报如下错误:
Cannot assign requested address (Address not available)

Open File Descriptors 限制

ulimit 命令用于展示和设置用户进程对一些系统关键资源使用的限制。因为每一个建立的连接在 Linux 上都可视为一个打开的文件,会占用一个 File Descriptor,所以 ulimit 内各种限制中跟并发连接数最相关的是进程最大能打开的 File Descriptor 数量。这个限制经常有人和单个 IP 端口数限制搞混,认为客户端单个 IP 能建立的连接数上限为 65535 是因为 File Descriptor 限制,只要机器内存够大,将 File Descriptor 改的够大,客户端单个 IP 能建立的连接数上限就能提升,这个是不正确的。

报错信息

当因为 Open File Descriptors 数量不足受限时会报错:
Too many open files

修改方法

Linux 下,临时修改的话直接执行 ulimit -n XXXXX 即可,但这个执行完后只对当前 Shell session 有效,再次登录后会恢复原状,如果想永久有效需要修改 /etc/security/limits.conf 文件,打开该文件找到 nofile 就是 File Descriptor 限制,例如:

1
2
soft nofile 1000000
hard nofile 1000000

普通用户默认使用的是 soft 限制,并且能够通过 ulimit -n 修改 soft 限制到最大跟 hard 一样,超过 hard 的话会报错:
ulimit: open files: cannot modify limit: Operation not permitted

但普通用户只能不可逆的降低 hard 值,不能将 hard 值提高,只有 root 用户能将 hard 限制提高。目前没有看到修改 hard 值的命令,修改方法应该是只有通过系统调用 setrlimit,具体使用可以用 man 3 看看。

Mac 下修改看上去好麻烦,我没有用过本地机器做过量特别大的压测,所以没有实际做过调整,留下一些线索供以后查阅:
mac - Increase the maximum number of open file descriptors in Snow Leopard? - Super User
https://www.chrissearle.org/2016/10/01/too-many-open-files-on-osx-macos/

file-max, file-nr, nr_open

一般只要注意到 ulimit 里的 soft hard nofile 就好了,但有的时候如果真的用的连接特别特别多,可能会需要调整下面这几个值。这几个值限制的文件 Handle 最大数量都很大,默认为 1024 * 1024,但也能调整的更大:

1
2
3
fs.file-max = 1000000
fs.file-nr = 13920 0 1000000
fs.nr_open = 1048576

file-max 是 kernel 能分配的文件 Handle 最大数量。file-nar 有三个值,动态变化的,第一个值是当前已经分配的文件 Handle 数量,第二个值是分配但是未使用的文件 Handle 数一般都是 0,最后一个实际就是 file-max 的值,表示系统最大分配的文件 Handle 数量。当系统内文件 Handle 数超过 file-max 后,一样会报 Too many open files

nr_open 是单个进程能分配的最大文件 Handle 数量,这个值一定比 file-max 小,并且一定要比 limits.conf 内的 soft nofile, hard nofile 大,不然 soft nofile, hard nofile 设置再大都没用。

参考:https://www.kernel.org/doc/Documentation/sysctl/fs.txt

自动分配本地端口范围

确定一个连接需要四个元素 Source IP + Source Port + Destination IP + Destination Port,一般客户端在连服务端的时候只要获取到服务端的 Destination IP 和 Destination Port 即可,Source IP 是客户端自己的 IP,客户端系统会自动分配一个 Source Port 来建立连接。而这个 Source Port 的选择范围是可通过 sysctl net.ipv4.ip_local_port_range 参数来定制的。可以执行一下这个命令来获取当前系统的设定,例如:

net.ipv4.ip_local_port_range = 49152 65535

即表示在与 remote 服务建立连接时,系统只能自动从 49152 至 65535 中选择一个作为 Local Port,也就是 Source Port。

如果希望压测客户端和服务器建立大量的连接,则需要将该范围设置的大一些,给客户端留足端口数。如果留的端口不足的话会报错。

为什么这个范围系统不能默认就设置的非常宽呢?比如默认就是 0 ~ 65535 ?

一个是因为 0 ~ 1023 是系统预留的 Well-Known Ports,普通进程就不可使用。另一个是因为当系统作为服务器使用的时候,经常需要开启一些端口比如 MySQL 会默认开启 3306 端口。如果将 net.ipv4.ip_local_port_range 范围设置的很广,比如 2000 ~ 65535 那就可能会出现在 MySQL 启动的时候 3306 端口已经被系统自动分配给了一个进程作为 Source Port 去连接另一个服务,从而导致 MySQL 因为无法绑定 3306 端口而无法启动。所以这个 net.ipv4.ip_local_port_range 需要根据你系统提供的服务情况来酌情进行调整,在写服务端程序时候开启的端口也不是能随便定的,一定要小于系统的 net.ipv4.ip_local_port_range 才行,不然就可能出现系统启动的时候待绑定端口已经被别的连接占用。

报错信息

当因为本地端口范围限制,无法分配出空闲端口时会报错:
Cannot assign requested address
这个错误信息跟端口不足时报的一样。

修改方法

Linux 系统下,执行命令:

sysctl -w net.ipv4.ip_local_port_range="15000 61000"

端口复用

TCP 连接断开之后主动发起 FIN 的一方最终会进入 TIME_WAIT 状态,处在这个状态时连接之前所占用的端口不能被下一个新的连接使用,必须等待一段时间之后才能使用。如果是单独测试并发连接峰值,减少 TIME_WAIT 连接数可能用处不大,但如果是连续的测试,每次关闭客户端准备再来下一轮测试时必须等足 TIME_WAIT 时间,如果 TIME_WAIT 时间比较长就比较烦,所以减少 TIME WAIT 对测试有一定好处。因为一般压测都是内网,所以 TIME WAIT 清理方面能稍微激进一些。TIME WAIT 相关内容可以参看:TCP TIME-WAIT。可以考虑:

  1. Client 开启 TCP Timestamps 后开启 net.ipv4.tcp_tw_reuse 或 net.ipv4.tcp_tw_recycle;
  2. 将 net.ipv4.tcp_max_tw_buckets 设置的很小,TIME WAIT 连接超过该值后直接清理。因为一般测试都在内网,没有 NAP 的情况下 Per-Host 的 Timestamp 配合 PAWS 一般能消除跨连接数据包错误到达问题;
  3. 考虑压测结束的时候由 Client 主动断开连接,并且设置 SO_LINGER 为 0,断开连接时候直接发 RST;

修改方法

sysctl -w net.ipv4.tcp_timestamps=1
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_tw_recycle=1
sysctl -w net.ipv4.tcp_max_tw_buckets=10000

TCP Backlog

TCP Backlog 的相关信息可以参考,TCP Backlog。实时通信服务也属于高并发服务,在搞活动、服务重启等时候可能出现大量用户同时和服务器创建连接的情况,所以也需要酌情调整 TCP Backlog 的大小。

修改方法

临时修改执行:

1
2
3
sysctl -w net.core.somaxconn=2048
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -w net.ipv4.tcp_max_syn_backlog=4096

永久性修改需要修改 /etc/sysctl.conf 文件,将上面修改值写在文件中。

以后遇到更多需要调整的参数后再继续记录吧。 To be continue…