MQTT百万TCP连接(服务端+客户端)调优

  • Linux系统优化(CentOS7)
  • 打开文件句柄数(nofile)
  • 全局限制
  • 进程限制
  • TCP协议栈参数
  • JVM 11参数优化
  • 压力机优化
  • 选择哪个MQTT客户端?
  • 压力机环境配置
  • 压测结果


Linux系统优化(CentOS7)

打开文件句柄数(nofile)

nofile=number of open file descriptors,在linux下编写网络服务器程序时,每一个TCP连接都要占一个文件句柄。一旦这个文件句柄使用完了,建立新连接时就会发生文件打开过多的错误。linux对nofile的限制分为进程限制和全局限制。

全局限制

# 查看全局nofile
cat /proc/sys/fs/file-nr
# 输出结果:896   0   788492,
# 三个数字分别表示:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大nofile限制数量

# 查看单个(fs.nr_open)和所有进程(fs.file-max)nofile限制
sysctl -n fs.nr_open  或者 cat /proc/sys/fs/nr_open
sysctl -n fs.file-max 或者 cat /proc/sys/fs/file-max
# 修改单个和所有进程nofile限制
sysctl -w fs.nr_open=2097152 
sysctl -w fs.file-max=2097152    

# 持久化修改nr_open和file-max
vi /etc/sysctl.conf
# 增加配置
fs.nr_open = 2097152
fs.file-max = 2097152

# 设置服务的nofile
vi /etc/systemd/system.conf
# 启用并修改配置
DefaultLimitNOFILE=2097152

进程限制

进程的nofile限制分为:

  • 软限制(soft limit):内核实际执行的限制,任何进程都可以将软限制设置 ≤ 进程的硬限制的值。
  • 硬限制(hard limit):可以在任何时候,对任何进程中设置,但硬限制需要由超级用户修改。
# 查看软限制
ulimit -Sn 或者 ulimit -n
# 修改软限制
ulimit -Sn 1048576 或者 ulimit -n 1048576
# 查看硬限制
ulimit -Hn 
# 修改硬限制
ulimit -Hn 1048576

# 持久化修改nofile限制,需要修改limits.conf。需重启生效,并且在/etc/pam.d/中的seesion有使用到limit模块
vi /etc/security/limits.conf
# 添加nofile限制
*      soft   nofile      1048576
*      hard   nofile      1048576
# 在测试最大连接数时,可以将该值设置的较大,例如:2097152 = 2048 * 1024。
# 在实际环境中,因为还要处理数据业务逻辑,太大的值其实没有必要,例如:1048576 = 1024 * 1024。

TCP协议栈参数

# 通过sysctl命令查看(-n)和修改(-w)网络参数,例如:
sysctl -n net.core.somaxconn
sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=16384
sysctl -w net.core.netdev_max_backlog=16384

# 持久化修改参数,编辑完成后,使用sysctl -p命令,刷新生效
vi /etc/sysctl.conf
# 添加配置
# 每一个端口最大的监听队列的长度,这是个全局的参数。
net.core.somaxconn=32768
# 对于还未获得对方确认的连接请求,可保存在队列中的最大数目。如果服务器经常出现过载,可以尝试增加这个数字。
net.ipv4.tcp_max_syn_backlog=16384
# 在每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。
net.core.netdev_max_backlog=16384
# 每个tcp连接占用内存,共三个值:最小字节数 默认值 最大字节数
net.ipv4.tcp_mem = 786432 2097152 3145728
# 每个tcp连接的读缓冲(接收缓冲),缓存从对端接收的数据,后续会被应用程序读取
net.ipv4.tcp_rmem = 2048 32768 4194304
# 每个tcp连接的写缓冲(发送缓冲),缓存应用程序的数据,有序列号被应答确认的数据会从发送缓冲区删除掉
net.ipv4.tcp_wmem = 2048 8192 2097152

JVM 11参数优化

# jvm启动参数
java -server -Xss256k -Xms1g -Xmx6g -Xmn900M \
-XX:MaxMetaspaceSize=128m -XX:MaxDirectMemorySize=6g \
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC \
-jar <jar包名称>

检查CPU和内存使用情况,调整jvm启动参数

# 通过top查看CPU使用情况
top -p <进程ID>
# 系统内存使用状况
free -m
# 通过jhsdb查看堆内存使用情况
jhsdb jmap --heap --pid <进程ID>

压力机优化

选择哪个MQTT客户端?

常见的mqtt客户端,有eclipse的paho,dreamlu的mica-mqtt。一番测试和源码分析后,发现它们都有一个相同的问题:每创建一个MQTT客户端,都会创建3~4个线程。即使通过设置xss参数来减少栈容量,也无济于事,直接pass掉。最后发现了hive-mqtt,他使用的netty架构,果断选择它作为压力测试的客户端。

压力机环境配置

压力机在解除了系统文件句柄数的限制后,但仍会出现并发TCP连接数增加到一定数量时,再也无法成功建立新的TCP连接的现象。在确保内存和CPU足够的情况下,出现这种现在的原因有多种:

  1. 如果发现发现tcp连接数达到1.6w左右就连不上去了,本地端口号范围可能太小。
# 如果压力机是window,修改端口配置如下
netsh int ipv4 set dynamicport tcp start=5000 num=65535
# 如果压力机是linux,修改端口配置如下
 echo 5000 65535 > /proc/sys/net/ipv4/ip_local_port_range
  1. 经过以上的修改,客户端能建立的最大连接数,可以到6万,因为本地端口使用完,不能继续增加连接,这还是不能满足我们的需求,这是可以考虑在一个网卡上绑定多个IP地址。
cd /etc/sysconfig/network-scripts
# 绑定多个网卡地址,我的测试机网卡设备名称为ens192,根据自己不同的名称,自行调整
vi ifcfg-ens192:0
# 增加如下内容:
DEVICE="ens192:0"
IPADDR="172.16.206.190" # 新绑定的网卡地址
PREFIX="24"
GATEWAY="172.16.206.1"
# 重复上述操作,绑定多个IP地址:ens192:1、ens192:2、ens192:3、ens192:4

注意:配置多个网卡后,建立TCP连接时,客户端需要绑定指定的网卡。
3. 通过上述操作,单个压力机的连接数增加到了27万后,无法继续建立连接。换个压力机又可以继续连接,即排除服务端原因。怀疑时vmware或者交换机上的限制,暂时没有条件和资料进行排查,后继再继续。

压测结果

emqx的社区版,不支持集群和需要的转储,erlang自己也不会,只好自己写了一个MQTT Broker来用。
将自研的mqtt broker部署在一个4C8G的虚拟机上,JVM -Xmx4g,做到了60W连接。
切换到8C16G、-Xmx12g的机器上,轻松做到了130万连接,因压力机不足,没有再继续。
差不多4g内存,可以支撑60万连接,只是连接数这个性能已经超越了EMQX,小小的开心一下。