本文的内容参考了InfoQ翻译的文章我们是如何优化 HAProxy 以让其支持 2,000,000 个并发 SSL 连接的?
负载均衡的并发测试,主要目标是测试负载均衡系统支持的最大并发连接数量。本文将介绍测试中应用的部署,测试的工具以及测试的过程。
一个TCP连接由一个五元组唯一确定:IP层协议、源IP地址、目的IP地址、源端口和目的端口。在负载均衡的测试过程中,有3个参数是不可变的:IP层协议、目的IP和目的端口,其中IP层协议是TCP,目的IP是负载均衡的IP地址,目的端口是负载均衡监听器的TCP端口。可变的参数只有源IP地址和源端口,而可用的源端口号通常只有1025~65534大约64k,所以为了达到更大的并发连接,我们就需要更多的源IP,也就是更多的客户端。
同样,我们也需要更多的应用服务器,负载均衡与应用服务器之间也需要建立TCP连接,应用服务器看到的源IP地址只是负载均衡的IP地址。
这里我们准备了20台压测客户端,20台应用服务器,理论上能够最大能够建立64k*20=1280k,大约128万。
首先介绍压力用到的两个工具。
工具一:pdsh
pdsh是一个并行登录远程主机执行命令的工具,我们需要同时从20台客户端机器发起压测,pdsh可以轻松的帮助完成。
pdsh的源码可以从github下载,按照文档编译安装
pdsh命令的使用举例如下:
代码语言:javascript复制# 需要配置SSH互信
# -w指定多个主机
pdsh -w 192.168.59.[1-10] -l root uptime
# -w使用逗号指定多个主机
pdsh -w 192.168.59.1,192.168.59.2 -l root uptime
# -x排除某个主机
pdsh -w 192.168.59.[1-10] -x 192.168.59.3 -l root uptime
# 使用主机组,把对应的主机写入到/etc/dsh/group/或~/.dsh/group/目录下的文件中即可
# 文件名就是对应组名
pdsh -g client-hosts -l root uptime
# 使用dshbak格式化输出
pdsh -g client-hosts -l root uptime | dshbak -c
工具二:vegeta
vegeta是一款优秀的HTTP压力测试工具,它本身的性能很好,保证了客户端不会成为测试中的瓶颈,同时它能以固定的频率发起连接,在并发测试中表现出众。
vegeta的源码可以从github下载,也可以直接下载编译好的二进制包。
vegeta命令的使用举例如下:
代码语言:javascript复制# Usage: vegeta [global flags] <command> [command flags]
# 执行压测的命令是attack,查看报告的命令是report
echo "GET <https://192.168.60.246>" | vegeta -cpus=32 attack -duration=10m
-rate=2000 -workers=500 | tee reports.bin | vegeta report
vegeta常用的选项:
- -cpus:指定使用CPU的个数,默认使用所有的CPU
- -body:指定request body文件
- -duration:指定测试的时长,比如10m表示10分钟
- -header:指定HTTP header
- -insecure:忽略校验server的TLS证书
- -keepalive:使用长连接(默认为true)
- -rate:请求的频率,默认是50/1s
- -workers:初始的worker个数,默认是10
- -connection:每个target打开的最大的连接数,默认是10000
- -timeout:请求的超时时间,默认是30s
接下来首先介绍如何部署测试的应用。
部署应用
最简单的方式是部署一个HTTP应用,只需要在应用服务器上安装httpd或者nginx等Web服务就好了,但是为了能够真正的测试出最大的并发连接,还需要一些额外的修饰。
假设目标是达到100万的并发连接,每个连接的平均处理时长是1秒,意味着每秒需要新建100万的连接,这对我们的模拟环境来说是一个很难达到的目标。另一方面,TCP连接断开之后的一段时间内,连接会处于time_wait
/close_wait
/fin_wait
状态,为了保证TCP连接能够正确的释放,操作系统无法立即重用这些端口,就会出现可用端口不足的问题。
所以我们的目标就是延长每一个请求的处理时长,假设一个请求的处理时长是100秒,为了达到100万的目标,每秒只需要发起1万个连接,这对大多数压测工具不是问题。
这里提供两种部署方式,一种是用go实现一个简单的HTTP server,它收到请求之后会休眠100秒,然后返回5个字节,代码如下:
代码语言:javascript复制package main
import (
"fmt"
"log"
"time"
"net/http"
)
func response(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Duration(100)*time.Second)
fmt.Fprint(w, "ping\n")
}
func main() {
http.HandleFunc("/", response)
err := http.ListenAndServe(":80", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
另一种是利用nginx的echo_sleep模块,nginx的配置文件如下:
代码语言:javascript复制# For more information on configuration, see:
# * Official English Documentation: <http://nginx.org/en/docs/>
# * Official Russian Documentation: <http://nginx.org/ru/docs/>
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
#include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1000000;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See <http://nginx.org/en/docs/ngx_core_module.html#include>
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
#include /etc/nginx/default.d/*.conf;
location / {
}
location /sleep5 {
echo_sleep 5;
echo "sleep 5s";
}
location /sleep100 {
echo_sleep 100;
echo "sleep 100s";
}
location /sleep1200 {
echo_sleep 1200;
echo "sleep 1200s";
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
除了调整应用的处理时间,还需要调整一些Linux内核参数,让应用服务器能够接受大量的并发连接。
首先编辑文件/etc/security/limits.conf,增加以下两行:
代码语言:javascript复制* soft nofile 1000000
* hard nofile 1000000
然后编辑文件/etc/sysctl.conf,增加如下内容:
代码语言:javascript复制fs.file-max = 1000000
fs.nr_open = 1000000
net.ipv4.ip_local_port_range = 1025 65534
net.core.netdev_max_backlog = 100000
net.core.somaxconn = 65534
net.ipv4.tcp_max_orphans = 5800000
net.ipv4.tcp_max_syn_backlog = 100000
net.nf_conntrack_max = 2097152
另外需要注意,如果使用systemd启动nginx的话,为了使limits的配置生效,需要新建配置文件 /etc/systemd/system/nginx.service.d/override.conf,内容如下:
代码语言:javascript复制[Service]
LimitNOFILE=1000000
最后重启应用服务器使配置生效。
测试过程
1. 准备压测机器
准备20个压测机器client1~client20,作为压力测试的客户端,同样按照前面的说明调整内核参数。
将压测工具vegeta安装到压测虚拟机的/usr/local/bin目录。
登录到client1,作为工作机,安装pdsh,配置client1到其他client虚拟机的SSH互信:
代码语言:javascript复制# 这里假设client的IP地址是192.168.56.10~192.168.56.29
# 配置互信
ssh-copy-id 192.168.56.10
ssh-copy-id 192.168.56.11
ssh-copy-id 192.168.56.12
……
# 测试互信,
pdsh -w 192.168.56.[10-29] uptime
2. 准备负载均衡
部署好负载均衡,添加前面准备的应用服务器作为成员。
另外需要注意的是调整负载均衡监听器的超时时间,因为负载均衡通常会主动断开长时间没有响应的连接,以避免异常的连接占用资源。以HAProxy为例,需要修改以下三个参数:
- timeout connect 3000
- timeout client 120000
- timeout server 120000
3. 测试
使用下面的命令测试:
代码语言:javascript复制# 登录到client1,执行下面的命令:
pdsh -w 192.168.56.[10-29] "echo 'GET <http://192.168.60.246:8080>' |
vegeta -cpus=10 attack -connections=1000000 -duration=20m -timeout=1h
-rate=500/s | tee report.bin | vegeta report"
下图显示了我们在虚拟机中部署HAProxy,最终达到100万并发连接。
因为每一个客户端连接在LB中对应两个TCP连接:从client到HAProxy和从HAProxy到应用服务器,所以图中established状态的连接有200万
如果测试过程中观察到总的socket与established状态的socket个数相差很多,可能就是某处的超时时间有问题,导致连接被异常中止造成的。