FastAPI多方式部署以及压测过程记录

上一篇文翻译了部署的相关概念后,对自己真正实践部署FastAPI有了更多的信心。特别是理解其中的细节以及每个服务所起的的作用(What, Why, How)。

这边就记录下Web服务部署阿里云时候的一些过程。

1. Gunicorn与Uvicorn

虽然我们可以直接Uvicorn裸跑,而且这不仅在调试时候,生产非高并发也足够了。

因为Uvicorn也可以选择worker参数,轻量级方式,只是不提供任何进程监控。处理工作进程的能力比Gunicorn更有限。

如果想在这个级别(Python级别)有一个进程管理器,那么尝试用Gunicorn作为进程管理器可能更好一些。

在生产环境中,Gunicorn 可能是运行和管理 Uvicorn 最简单的方式。Uvicorn 包括一个Gunicorn工人类,这意味着尽可能少的配置。

1.1 使用

你可以使用 Gunicorn 管理 Uvicorn 并运行多个这些并发进程。这样,就可以最大限度地利用并发和并行性

命令如下:

gunicorn main:app --workers 17 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8989

我们参照 Gunicorn 的官方文档,把worker数量设置为 2 * CPU 核心数 + 1,我的机器是8核,所以此处为17。

1.2 日志处理

如果是使用Gunicorn运行,我们需要将FastAPI日志和Uvicorn日志整合到Gunicorn日志并输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if __name__ == "__main__":
# 修改uvicorn访问日志时间格式,加上时间戳,方便查看
log_config = uvicorn.config.LOGGING_CONFIG
log_config["formatters"]["access"]["fmt"] = "%(asctime)s - %(levelname)s - %(message)s"
# fastapi本身日志,无handler,加一个
ch = logging.StreamHandler(stream=None)
fastapi_logger.addHandler(ch)
fastapi_logger.setLevel(logging.DEBUG)
uvicorn.run("__main__:app", host="0.0.0.0", port=8989, reload=True, workers=1)
else:
# 获取gunicorn日志
gunicorn_error_logger = logging.getLogger("gunicorn.error") # 默认是info
# uvicorn日志, 可以考虑注释,提高性能
uvicorn_access_logger = logging.getLogger("uvicorn.access")
uvicorn_access_logger.handlers = gunicorn_error_logger.handlers
uvicorn_access_logger.setLevel(gunicorn_error_logger.level)
# fastapi本身日志
fastapi_logger.handlers = gunicorn_error_logger.handlers
fastapi_logger.setLevel(gunicorn_error_logger.level)

Tips:Python中logging模块StreamHandle默认是标准stderr输出!

终端上就会显示FastAPI,Uvicorn以及Gunicorn这三者的日志,后续无论使用Docker还是Supervisor部署,日志都会统一,最多再做个重定向输出,一劳永逸。

2. 容器部署

部署 FastAPI 应用程序时,一种常见的方法是构建Linux 容器映像。通常使用Docker完成。然后可以通过几种可能的方式之一部署该容器映像。

使用 Linux 容器有几个优点,包括安全性、可复制性、简单性等。

2.1 官方 Docker 镜像

有一个官方的 Docker 镜像,包含了Uvicorn和Gunicorn。

包含一个自动调整机制,用于根据可用的 CPU 内核设置工作进程的数量。

它具有合理的默认值,但您仍然可以使用环境变量或配置文件更改和更新所有配置。

它还支持在容器启动之前运行prestart.sh(下面的启动脚本中有相关代码),做一些前期操作,例如数据库迁移。

2.2 如何使用

项目里有一个文件 requirements.txt,那么Dockerfile就类似于:

1
2
3
4
5
6
7
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./app /app

入口文件在/app/app/main.py 或者在/app/main.py

然后就可以制作镜像了docker build -t myimage ./

如果涉及到PgSQL或者Redis之类的,那么Docker-Compose也完全可以来整一整,不展开。

2.3 容器配置源代码

Docker部署非常简单,但也非常弹性, 可以通过不同环境变量控制一些设置。在这官方Docker 镜像文档中我们就可以看到。具体参数不谈,这边就记录下相关内部代码。

2.3.1 容器启动时的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#! start.sh
#! /usr/bin/env sh
set -e

if [ -f /app/app/main.py ]; then
DEFAULT_MODULE_NAME=app.main
elif [ -f /app/main.py ]; then
DEFAULT_MODULE_NAME=main
fi
MODULE_NAME=${MODULE_NAME:-$DEFAULT_MODULE_NAME}
VARIABLE_NAME=${VARIABLE_NAME:-app}
export APP_MODULE=${APP_MODULE:-"$MODULE_NAME:$VARIABLE_NAME"}

if [ -f /app/gunicorn_conf.py ]; then
DEFAULT_GUNICORN_CONF=/app/gunicorn_conf.py
elif [ -f /app/app/gunicorn_conf.py ]; then
DEFAULT_GUNICORN_CONF=/app/app/gunicorn_conf.py
else
DEFAULT_GUNICORN_CONF=/gunicorn_conf.py
fi
export GUNICORN_CONF=${GUNICORN_CONF:-$DEFAULT_GUNICORN_CONF}
export WORKER_CLASS=${WORKER_CLASS:-"uvicorn.workers.UvicornWorker"}

# If there's a prestart.sh script in the /app directory or other path specified, run it before starting
PRE_START_PATH=${PRE_START_PATH:-/app/prestart.sh}
echo "Checking for script in $PRE_START_PATH"
if [ -f $PRE_START_PATH ] ; then
echo "Running script $PRE_START_PATH"
. "$PRE_START_PATH"
else
echo "There is no script $PRE_START_PATH"
fi

# Start Gunicorn
exec gunicorn -k "$WORKER_CLASS" -c "$GUNICORN_CONF" "$APP_MODULE"

2.3.2 Gunicorn配置脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import json
import multiprocessing
import os

workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
max_workers_str = os.getenv("MAX_WORKERS")
use_max_workers = None
if max_workers_str:
use_max_workers = int(max_workers_str)
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)

host = os.getenv("HOST", "0.0.0.0")
port = os.getenv("PORT", "80")
bind_env = os.getenv("BIND", None)
use_loglevel = os.getenv("LOG_LEVEL", "info")
if bind_env:
use_bind = bind_env
else:
use_bind = f"{host}:{port}"

cores = multiprocessing.cpu_count()
workers_per_core = float(workers_per_core_str)
default_web_concurrency = workers_per_core * cores
if web_concurrency_str:
web_concurrency = int(web_concurrency_str)
assert web_concurrency > 0
else:
web_concurrency = max(int(default_web_concurrency), 2)
if use_max_workers:
web_concurrency = min(web_concurrency, use_max_workers)
accesslog_var = os.getenv("ACCESS_LOG", "-")
use_accesslog = accesslog_var or None
errorlog_var = os.getenv("ERROR_LOG", "-")
use_errorlog = errorlog_var or None
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
timeout_str = os.getenv("TIMEOUT", "120")
keepalive_str = os.getenv("KEEP_ALIVE", "5")

# Gunicorn config variables
loglevel = use_loglevel
workers = web_concurrency
bind = use_bind
errorlog = use_errorlog
worker_tmp_dir = "/dev/shm"
accesslog = use_accesslog
graceful_timeout = int(graceful_timeout_str)
timeout = int(timeout_str)
keepalive = int(keepalive_str)


# For debugging and testing
log_data = {
"loglevel": loglevel,
"workers": workers,
"bind": bind,
"graceful_timeout": graceful_timeout,
"timeout": timeout,
"keepalive": keepalive,
"errorlog": errorlog,
"accesslog": accesslog,
# Additional, non-gunicorn variables
"workers_per_core": workers_per_core,
"use_max_workers": use_max_workers,
"host": host,
"port": port,
}
print(json.dumps(log_data))

3. 压力测试

ab就是Apache Benchmark的缩写,顾名思义它是Apache组织开发的一款web压力测试工具,优点是使用方便,统计功能强大。

首先是安装,Ubuntu和CentOS目前都提供自动安装命令 (至少Ubuntu 14, CentOS 6可以)

  • Ubuntu: sudo apt-get install apache2-utils

  • CentOS:yum install httpd-tools

3.1 使用

ab 一般常用参数就是 -n, -t ,和 -c。

-c (concurrency)表示用多少并发来进行测试;

-t 表示测试持续多长时间;

-n 表示要发送多少次测试请求。

一般 -t 或者 -n 选一个用。

例如模拟GET请求进行测试:

ab -n 20000 -c 1000 http://0.0.0.0:8989/firmware/latest?model=xxx

3.2 结果分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Server Software:        uvicorn
Server Hostname: 192.168.52.224
Server Port: 8989

Document Path: /firmware/latest?model=RLM2003EI
Document Length: 204 bytes

Concurrency Level: 1000
Time taken for tests: 3.843 seconds
Complete requests: 20000
Failed requests: 0
Total transferred: 6600000 bytes
HTML transferred: 4080000 bytes
Requests per second: 5204.07 [#/sec] (mean)
Time per request: 192.157 [ms] (mean)
Time per request: 0.192 [ms] (mean, across all concurrent requests)
Transfer rate: 1677.09 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 2 4.5 0 31
Processing: 1 186 285.7 77 3721
Waiting: 1 183 285.6 74 3721
Total: 1 187 286.8 78 3730

Percentage of the requests served within a certain time (ms)
50% 78
66% 138
75% 193
80% 245
90% 505
95% 783
98% 1092
99% 1470
100% 3730 (longest request)

我们对产生的结果进行分析,具体不展开,详细看Apache ab性能测试结果分析

3.3 socket: Too many open files

ulimit -n 查看最大文件打开数,一般情况下都是1024

我们可以ulimit -n 65535,临时设置下最大文件数,重启终端无效。

永久设置方法:vim /etc/security/limits.conf 加入 * - nofile 8192