记一次后端性能测试与调试
先说结论:项目遇到的主要瓶颈在CPU和uwsgi的listen参数。下面以时间顺序记录排查过程。
背景
导师安排的一个项目,由于需要使用一些百度智能云的服务,服务器我选的百度BCC,预设的部署方案是两台4核4G应用服务器(uwsgi+falsk)、三台2核8G的数据服务器(Redis+MongoDB,3台保证可用)、一台2核2G的配置和部署服务器。但实际开发时为节省成本,暂时将MongoDB、redis、nginx、uwsgi以及后端代码部署于同一台2核8G服务器。
使用阿里云PTS以500并发施压时发现有较大性能瓶颈,大量请求超过设置的5s时限。每个线程依次请求的接口和顺序如下(原始接口较丑陋,功能不太合理且不符合REST风格,新版REST接口平滑替换中):
uwsgi 初始配置文件
应用服务器中uWSGI
部分配置如下:
[uwsgi]
strict = true
single-interpreter = true
die-on-term = true
need-app = true
socket=0.0.0.0:5001
pidfile=/tmp/uwsgi-exp-flask.pid
vacuum=true
# python 虚拟环境目录:
home=/root/.local/share/virtualenvs/expression-flask-p9tMTecW
pythonpath=/var/www/expression-flask
# web 应用python主程序:
wsgi-file=run_app.py
# callable=app 这个 app 是 manage.py 程序文件内的一个变量,这个变量的类型是 Flask的 application 类
callable=app
uid=root
gid=root
chdir=/var/www/expression-flask
max-fd=65535
# socket监听队列大小,默认100
listen=100
# 2进程,每个进程1线程
processes=2
# threads=1
# apscheduler requires enable-threads, if no worker threads are used
enable-threads=true
master=true
# 无master时自动结束worker
no-orphans=true
# 设置master进程名称
procname-master=uWSGI master (%p)
# 平滑重启最长等待时间(秒)
reload-mercy=6
此外/etc/sysctl.conf
部分配置如下:
net.ipv4.tcp_keepalive_time=1800
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_max_tw_buckets=25000
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=10
net.ipv4.tcp_syncookies=0
net.core.somaxconn=65535
过程1 - 调整uwsgi listen参数
发现性能问题后,首先需要定位慢在什么地方。
调整Nginx日志,记录时间:
# Nginx配置(部分)
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
worker_rlimit_nofile 65535;
events {
worker_connections 32768;
# multi_accept on;
}
http {
# ......
##
# Logging Settings
##
log_format timed_combined '$remote_addr - $remote_user [$time_local] [REQ:$request_time] [RESP:$upstream_response_time] [CONN:$upstream_connect_time] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" [$upstream_addr $upstream_status]';
access_log /var/log/nginx/access.log timed_combined;
error_log /var/log/nginx/error.log;
# ......
}
这样可以看到如下这种时间记录:
CONN(upstream_connect_time)是Nginx连接uwsgi花费的时间。这个图是后来截取的,最初REQ超5秒,CONN超2秒。CONN过大是uwsgi的listen参数过小造成的。listen队列大小默认是100,满了之后就会拒绝请求,这时候任务会被堆积在Nginx端,记录的CONN就比较大。
通过把listen参数大小改成65535(不可超过net.core.somaxconn,否则uwsgi拒绝启动),可以让uwsgi接受更多请求放入backlog,所以降低了upstream_connect_time:
过程2 - 确认是uwsgi的锅
上面通过调整listen让uwsgi一股脑地接受所有请求,并不能让整体速度提高(请求失败的都是499错误):
所以,还需要确认一下uwsgi和flask代码到底是谁慢。
查看uwsgi的日志可看出耗时最长的请求执行了不到1秒:
所以代码执行速度还可以,锅是uwsgi的。
过程3 - 是CPU遇到瓶颈了
uwsgi处理很慢,是不是worker数量太少?所以尝试把2进程增大到4/8/16/64进程,但是性能并没有变好,4进程的时候似乎快了一点,8进程似乎又慢了一点,16/64进程反而导致性能再次下降。猜测是CPU性能不够。prometheus和vps自带监控精度都太低,不能看到瞬发的CPU占用率,于是开htop肉眼观察,发现请求到达时CPU占用可以到达88%上下,是挺高的。
(可能有人问为什么要500并发瞬发而不连续压测,因为连续压测测试的是吞吐量而不是响应时间)
于是临时搭个4核4G的服务器部署Nginx、uwsgi、flask代码,redis和MongoDB仍在原来的2核8G服务器上,配置hosts通过内网访问。
这下提升很明显:
只有第2、3个接口的最大响应时间超过3秒:
此时CPU峰值占用情况(上面是4核4G的应用服务器,下面是2核8G的数据服务器):
如何解决:
按预设的两台4核4G应用服务器,第2个接口ok。第三个接口时间过长是因为逻辑过于复杂,获取题目之前先进行了试卷初始化工作。把这个接口拆分为两个接口,ok。
番外 - 验证一下listen的影响
单台4核4G应用服务器+单台2核8G数据服务器的情况下,把listen改回100看看对结果有多大影响:
此外uwsgi日志中可查找到队列满的告警信息。
根据结果看listen的影响还是比较明显的,数值过小和Nginx的bargain太多而且不能充分调动每个worker的积极性。🤦♂️