前言

之前学习和实际生产环境的flask都是用app.run()的默认方式启动的,因为只是公司内部服务,请求量不高,一直也没出过什么性能问题。最近接管其它小组的服务时,发现他们的服务使用Gunicorn + Flask的方式运行的,本地开发用的gevent的WSGIServer。对于Gunicorn之前只是耳闻,没实际用过,正好捣鼓下看看到底能有多少性能提升。本文简单记录flask在各种配置参数和运行方式的性能,后面也会跟其他语言和框架做个对比。

  • python版本:3.11
  • flask版本:3.0.3
  • Gunicorn:23.0.0
  • wrk作为性能测试工具
  • 运行环境:vbox虚拟机,debian 12, 4C4G的硬件配置

wrk测试脚本

wrk支持用lua脚本对请求的响应结果进行验证,以下脚本对响应码和响应内容进行校验

wrk.method = "GET"
wrk.host = "127.0.0.1:8080"
wrk.path = "/health"
wrk.timeout = 1.0 response = function(status, headers, body)
if status ~= 200 then
print("Error: expected 200 but got " .. status)
end if not body:find("ok") then
print("Error: response does not contain expected content.")
end
end

Flask框架的测试记录

  1. 先测试默认运行方式,且没有sleep的情况下的并发性能。
from flask import Flask

app = Flask(__name__)

@app.get("/health")
def health():
return "ok" if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

使用命令nohup python demo.py > /dev/null启动,以下为wrk测试结果,可以看到已经出现超时请求。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 65.47ms 81.07ms 1.99s 97.69%
Req/Sec 575.99 107.20 1.08k 66.71%
137538 requests in 1.00m, 22.82MB read
Socket errors: connect 0, read 114, write 0, timeout 144
Requests/sec: 2288.49
Transfer/sec: 388.86KB
  1. 还是默认启动方式,增加等待时间,模拟处理任务的时间消耗。后续测试都会增加等待时间。
from flask import Flask
from time import sleep app = Flask(__name__) @app.get("/health")
def health():
sleep(0.1)
return "ok" if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

wrk测试结果,不出所料性能会有所下降。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 201.90ms 239.99ms 2.00s 95.05%
Req/Sec 479.79 185.87 1.82k 68.19%
114440 requests in 1.00m, 18.99MB read
Socket errors: connect 0, read 2, write 0, timeout 1833
Requests/sec: 1904.45
Transfer/sec: 323.62KB
  1. flask 更新到版本2后支持使用异步函数(需要安装异步相关依赖python -m pip install -U flask[async]
from flask import Flask
import asyncio app = Flask(__name__) @app.route('/health')
async def health():
await asyncio.sleep(0.1)
return "ok" if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

wrk测试结果,性能相较于同步函数甚至还下降了,QPS几乎砍半,看来异步版Flask还有待增强。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 275.49ms 190.22ms 2.00s 95.46%
Req/Sec 242.86 104.25 720.00 65.91%
57896 requests in 1.00m, 9.61MB read
Socket errors: connect 0, read 48, write 0, timeout 611
Requests/sec: 964.31
Transfer/sec: 163.86KB
  1. 接管的Flask应用在本地使用gevent的WSGIServer运行,所以也来试试。
from gevent.pywsgi import WSGIServer
from flask import Flask
from time import sleep app = Flask(__name__) @app.route('/health')
def health():
sleep(0.1)
return "ok" if __name__ == "__main__":
http_server = WSGIServer(('0.0.0.0', 8080), app)
http_server.serve_forever()

wrk测试结果,惨不忍睹,像是单线程在挨个处理请求,每个请求都会阻塞住。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 185.36ms 221.17ms 1.93s 96.30%
Req/Sec 5.54 3.39 10.00 46.55%
592 requests in 1.00m, 67.64KB read
Socket errors: connect 0, read 0, write 0, timeout 322
Requests/sec: 9.85
Transfer/sec: 1.13KB
  1. 按网上搜的结果加上了monkey patch
from gevent.pywsgi import WSGIServer
from gevent import monkey
from flask import Flask
from time import sleep monkey.patch_all() app = Flask(__name__) @app.route('/health')
def health():
sleep(0.1)
return "ok" if __name__ == "__main__":
http_server = WSGIServer(('0.0.0.0', 8080), app)
http_server.serve_forever()

wrk测试结果,加上monkey patch后似乎也没什么作用。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 182.89ms 209.48ms 1.82s 96.07%
Req/Sec 5.55 3.50 10.00 47.89%
592 requests in 1.00m, 67.64KB read
Socket errors: connect 0, read 0, write 0, timeout 312
Requests/sec: 9.85
Transfer/sec: 1.13KB
  1. 正式上gunicorn,代码没有任何改动,也不需要引用gevent的WSGServer。
from flask import Flask
from time import sleep app = Flask(__name__) @app.get("/health")
def health():
sleep(0.1)
return "ok" if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

运行命令。指定-k geventdemo:app中的demo是代码文件名。--worker-connections默认为1000

gunicorn demo:app -b 0.0.0.0:8080 -w 4 -k gevent --worker-connections 2000

wrk测试结果。性能相较于默认启动方式有了接近10倍的提升,请求响应时间也很稳定,最大响应时间也只有310.48。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 126.18ms 9.52ms 310.48ms 84.68%
Req/Sec 3.98k 165.70 4.53k 77.34%
948506 requests in 1.00m, 143.83MB read
Requests/sec: 15799.31
Transfer/sec: 2.40MB

其它框架和语言

在t4c2000的wrk配置下,flask+unicorn的每个进程基本都占用了85+%的CPU,再提高就得加CPU核心数了,不过这样的性能已经能满足公司内部服务的需求了,而且实际业务中,短板更可能是网络IO。

这里也测测其它语言和框架,看看Flask在Gunicorn的加持下能否打出python的牌面。

Golang

上来先试试最熟悉的Go, version: 1.22.4,使用标准库。(编译打包出来就能直接运行,不需要jvm这样的虚拟机,也不需要python这样的解释器,更不需要docker这样的容器运行时,特喜欢Go这一点)

package main

import (
"fmt"
"net/http"
"time"
) func MyHandler(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Millisecond * 100)
w.Write([]byte("ok"))
} func main() {
http.HandleFunc("/health", MyHandler)
err := http.ListenAndServe("0.0.0.0:8080", nil)
if err != nil {
fmt.Println(err)
}
}

wrk结果如下,请求量是目前测试以来第一个突破百万,而且也没有timeout的出现。使用top观察资源消耗,CPU只占用了约30%,而且还只有一个进程。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 101.56ms 1.48ms 121.62ms 77.58%
Req/Sec 4.94k 191.07 5.05k 93.49%
1180108 requests in 1.00m, 132.80MB read
Requests/sec: 19643.94
Transfer/sec: 2.21MB

不断加大连接,直到系统平均负载到达4(虚拟机CPU核心数为4)。连接数加了10倍,QPS差不多也是10倍于Flask + Gunicorn。这时候实际上wrk也占用了不少CPU资源,服务端的性能并没到瓶颈。

$ wrk -s bm.lua -t 4 -c20000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 20000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 167.56ms 43.45ms 397.95ms 60.37%
Req/Sec 29.48k 8.84k 48.26k 63.05%
6867733 requests in 1.00m, 772.85MB read
Requests/sec: 114258.70
Transfer/sec: 12.86MB

FastAPI

Go的性能已经很不错了,就性能来说还不是Flask+Gunicorn能媲美的。再来试试号称性能并肩Go的FastAPI(官网features里面写的)。FastAPI版本:0.115.4

纯uvicorn启动,用的是同步函数。

from fastapi import FastAPI
import uvicorn
from fastapi.responses import PlainTextResponse
from time import sleep app = FastAPI() @app.get("/health")
def index():
sleep(0.1)
return PlainTextResponse(status_code=200,content="ok") if __name__ == '__main__':
uvicorn.run(app, host="127.0.0.1", port=8080, access_log=False)

wrk测试结果,可以看到相当低下,甚至还不如flask的默认运行方式,超时请求数都过2w了。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.22s 438.35ms 1.95s 60.00%
Req/Sec 115.03 57.31 410.00 86.69%
23494 requests in 1.00m, 3.02MB read
Socket errors: connect 0, read 0, write 0, timeout 22894
Requests/sec: 390.92
Transfer/sec: 51.54KB

改用异步函数再试试。

from fastapi import FastAPI
import uvicorn
from fastapi.responses import PlainTextResponse
from time import sleep
import asyncio app = FastAPI() @app.get("/health")
async def health():
await asyncio.sleep(0.1)
return PlainTextResponse(status_code=200,content="ok") if __name__ == '__main__':
uvicorn.run(app, host="127.0.0.1", port=8080, access_log=False)

wrk测试结果,可以看到性能好很多了,而且没有timeout。QPS是Flask默认启动方式的2倍,但实际性能应该不止2倍。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 484.53ms 63.18ms 654.48ms 61.26%
Req/Sec 1.09k 698.29 3.13k 63.78%
246744 requests in 1.00m, 31.77MB read
Requests/sec: 4106.80
Transfer/sec: 541.43KB

uvicorn支持指定worker数,这里设置为CPU核心数。

from fastapi import FastAPI
import uvicorn
from fastapi.responses import PlainTextResponse
from time import sleep
import asyncio app = FastAPI() @app.get("/health")
async def health():
await asyncio.sleep(0.1)
return PlainTextResponse(status_code=200,content="ok") if __name__ == '__main__':
uvicorn.run(app="demo2:app", host="127.0.0.1", port=8080, access_log=False, workers=4)

wrk测试结果,响应时间还是非常稳的,完全没有timeout的情况,延迟还更低。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 164.52ms 13.57ms 273.27ms 73.49%
Req/Sec 3.05k 544.20 4.64k 68.67%
727517 requests in 1.00m, 93.73MB read
Requests/sec: 12123.17
Transfer/sec: 1.56MB

gunicorn也支持uvicorn,看看fastapi在gunicorn的加持下会有怎样的性能表现。

gunicorn demo2:app -b 127.0.0.1:8080 -w 4 -k uvicorn.workers.UvicornWorker --worker-connections 2000

wrk测试结果,相较于unicorn运行方式,性能提升并不多。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 146.43ms 21.21ms 263.13ms 69.52%
Req/Sec 3.43k 508.71 4.88k 71.40%
818281 requests in 1.00m, 105.35MB read
Requests/sec: 13620.16
Transfer/sec: 1.75MB

Sanic

之前用过一段时间Sanic,也是个python异步框架,版本:24.6.0

from sanic import Sanic
from sanic.response import text
import asyncio app = Sanic("HelloWorld") @app.get("/health")
async def hello_world(request):
await asyncio.sleep(0.1)
return text("ok") if __name__ == "__main__":
app.run(host="127.0.0.1", port=8080, fast=True, debug=False, access_log=False)

wrk测试结果。虽然QPS比FastAPI高,但是有timeout的情况,不是很稳定。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 104.05ms 45.30ms 1.82s 99.59%
Req/Sec 4.84k 538.65 5.07k 95.98%
1154792 requests in 1.00m, 115.64MB read
Socket errors: connect 0, read 0, write 0, timeout 88
Requests/sec: 19218.08
Transfer/sec: 1.92MB

Openresty

openresty基于nginx,通过集成lua,也可以用来写api。配置如下,只是增加了一个location,稍微调整下nginx的参数

worker_processes  auto;
worker_cpu_affinity auto; events {
worker_connections 65535;
} http {
include mime.types;
default_type application/octet-stream;
access_log off;
sendfile on;
keepalive_timeout 65; server {
listen 8080 deferred;
server_name localhost;
location /health {
content_by_lua_block {
ngx.sleep(0.1)
ngx.print("ok")
}
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

wrk测试结果,和Go语言相当。

$ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://192.168.0.201:8080/health
4 threads and 2000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 101.43ms 1.62ms 136.59ms 88.97%
Req/Sec 4.94k 213.51 5.33k 92.65%
1178854 requests in 1.00m, 211.36MB read
Requests/sec: 19619.86
Transfer/sec: 3.52MB

top观察openresty的cpu占用并不高,加大连接再试试。连接数达到25000后,系统平均负载已经基本满了,而且wrk也占用了不少CPU资源。和Go差不多,并没有到服务端的性能瓶颈,而是受到系统资源限制。

$ wrk -s bm.lua -t 4 -c25000 -d60s http://127.0.0.1:8080/health
Running 1m test @ http://127.0.0.1:8080/health
4 threads and 25000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 149.17ms 36.61ms 659.40ms 71.22%
Req/Sec 41.00k 6.64k 59.03k 65.95%
9330399 requests in 1.00m, 1.63GB read
Requests/sec: 155277.58
Transfer/sec: 27.84MB

小结

整理下测试数据汇总成表格如下

项目 总请求量 每秒请求量 平均响应时间 最大响应时间 备注
Flask-no sleep 137538 2288.49 65.47ms 1.99s 有响应超时情况
Flask-同步方式 114440 1904.45 201.90ms 2.00s 有响应超时情况
Flask-异步函数 57896 964.31 275.49ms 2.00s 有响应超时情况
Flask+gevent 592 9.85 185.36ms 1.93s 有响应超时情况
Flask+gevent(monkeypatch) 592 9.85 182.89ms 1.82s 有响应超时情况
Flask+gevent+unicorn 948506 15799.31 126.18ms 310.48ms
Golang 1180108 19643.94 101.56ms 121.62ms
Golang 6867733 114258.70 167.56ms 397.95ms wrk的配置为t4c20000
FastAPI-同步函数 23494 390.92 1.22s 1.95s 有响应超时情况
FastAPI-异步函数 246744 4106.80 484.53ms 654.48ms
FastAPI-多worker 727517 12123.17 164.52ms 273.27ms
FastAPI+Gunicorn 818281 13620.16 146.43ms 273.27ms
Sanic 1154792 19218.08 104.05ms 1.82s 有响应超时情况,不是很稳定
OpenResty 1178854 19619.86 101.43ms 136.59ms
OpenResty 9330399 155277.58 149.17ms 659.40ms wrk的配置为t4c25000

根据测试结果,测试的三个Python Web框架中,Flask+gevent+unicorn综合最佳,不低的QPS,而且没有请求超时的情况,也不需要将代码修改成异步方式。Sanic的QPS虽高,但是有响应超时的情况,说明并不稳定,而且代码需要是异步的。FastAPI+Gunicorn的表现也不差,在不使用Gunicorn的情况下也能提供不错的性能,但代码同样需要改成异步方式。对于Sanic和FastAPI,Gunicorn的加持并不必要,而Gunicorn对Flask的性能提升至少7倍,而且能避免请求超时的情况,生产环境下应该尽量使用Gunicorn来运行Flask。

Go比各个Python框架的性能都更好,资源占用也更低,运行方式还更简单,不需要依赖编程语言环境和其他组件,非要说缺点的话就是开发没有Python快。

OpenResty的性能在测试中是最高的,主要是nginx本身性能良好。缺点是开发更麻烦。虽然是用lua开发,但lua作为动态语言,既不如Python极其灵活,还有动态语言本身代码不够清晰的缺点。以前尝试过用openresty实现一个crud服务,后来连自己都懒得维护就放弃了,干脆只用来当网关。

鱼与熊掌不可兼得,开发速度跟运行速度往往相斥,除非代码以后都是AI来写。就公司目前这服务的使用情况来说,Flask+Gunicorn的性能已经足够,还不需要改代码,实乃社畜良伴。而且现在啥都上k8s了,服务扩展也简单,性能不够就加实例嘛

[python]Gunicorn加持,轻松提升Flask超7倍性能的更多相关文章

  1. 啊哈C!思考快你一步——用编程轻松提升逻辑力

    啊哈C!思考快你一步——用编程轻松提升逻辑力(双色)(每个人都应该学习如何编程,因为它教会你如何思考.——史蒂夫.乔布斯) 啊哈磊著 ISBN 978-7-121-21336-6 2013年9月出版 ...

  2. Python轻量Web框架Flask使用

    http://blog.csdn.net/jacman/article/details/49098819 目录(?)[+] Flask安装 Python开发工具EclipsePyDev准备 Flask ...

  3. [ 转载 ] Python Web 框架:Django、Flask 与 Tornado 的性能对比

    本文的数据涉及到我面试时遇到过的问题,大概一次 http 请求到收到响应需要多少时间.这个问题在实际工作中与框架有比较大的关系,因此特别就框架的性能做了一次分析. 这里使用 2016 年 6 月 9 ...

  4. Web开发入门教程:Pycharm轻松创建Flask项目

    Web开发入门教程:Pycharm轻松创建Flask项目 打开Pycharm的file,选择创建新的项目,然后弹出对话框,我们可以看到里面有很多的案例,Flask.Django等等,我们选择生成Fla ...

  5. (数据科学学习手札90)Python+Kepler.gl轻松制作时间轮播图

    本文示例代码及数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 Kepler.gl作为一款强大的开源地理信 ...

  6. 快速上手python的简单web框架flask

    目录 简介 web框架的重要组成部分 快速上手flask flask的第一个应用 flask中的路由 不同的http方法 静态文件 使用模板 总结 简介 python可以做很多事情,虽然它的强项在于进 ...

  7. 提升 web 应用程序的性能(一)

       提升 web 应用程序的性能,找出瓶颈,加快客户端内容的速度.    作为 web 用户,我们知道页面加载或刷新的速度对其成功至关重要.本文将帮助您更好地理解影响 web 应用程序性能的因素.学 ...

  8. 开启 NFS 文件系统提升 Vagrant 共享目录的性能

    Vagrant 默认的 VirtualBox 共享目录方式读写性能表现并不好,好在 Vagrant 支持 NFS 文件系统方式的共享,我们可以启用 NFS 提升性能 开启方法 首先要把虚拟机的网络设置 ...

  9. C#不用union,而是有更好的方式实现 .net自定义错误页面实现 .net自定义错误页面实现升级篇 .net捕捉全局未处理异常的3种方式 一款很不错的FLASH时种插件 关于c#中委托使用小结 WEB网站常见受攻击方式及解决办法 判断URL是否存在 提升高并发量服务器性能解决思路

    C#不用union,而是有更好的方式实现   用过C/C++的人都知道有个union,特别好用,似乎char数组到short,int,float等的转换无所不能,也确实是能,并且用起来十分方便.那C# ...

  10. 【转】提升你的Java应用性能:改善数据处理

    提升你的Java应用性能:改善数据处理 作者:贾小骏  发布于07月26日 10:17 许多应用程序在压力测试阶段或在生产环境中都会遇到性能问题.如果我们看一下性能问题背后的原因,会发现很多是由数据处 ...

随机推荐

  1. esphome esp8266刷写遇到的问题

    问题描述: 在尝试打开串口时出现以下错误信息: Failed to execute 'open' on 'SerialPort': Failed to open serial port. 起因: 莫名 ...

  2. java_String方法大全

    1 String a = "abcdefg"; 2 3 String a = new String(); 4 String a = new String("abcdefg ...

  3. LaTeX 插入表格

    普通表格 \begin{table}[h] % h: here \begin{center} % 一个字母代表一列 \begin{tabular}{|c|cccc|} % c: center, l: ...

  4. 关于SQLServer数据库的READ_COMMITTED_SNAPSHOT隔离级别

    默认情况下,SQL Server的事务隔离级别是READ COMMITED.刚开始我理解这个模式就是读已经提交的,那也就是说并发一个事务去更新,一个事务查询同一条数据应该是像Mysql.Oracle不 ...

  5. 13 Python面向对象编程:装饰器

    本篇是 Python 系列教程第 13 篇,更多内容敬请访问我的 Python 合集 Python 装饰器是一种强大的工具,用于修改或增强函数或方法的行为,而无需更改其源代码.装饰器本质上是一个接收函 ...

  6. manim边学边做--角度标记

    manim中绘制一个角度其实就是绘制两条直线,本篇介绍的不是绘制角度,而是绘制角度标记. 对于锐角和钝角,角度标记是一个弧,弧的度数与角的度数一样: 对于直角,角度标记是一个垂直的拐角. manim中 ...

  7. Vue.js 异步组件传参

    本文主要展示一下如何给异步组件进行参数传递: 通过 h 函数就可以啦 versions: vue@3.2.13 子组件 Async.vue <template> <div> & ...

  8. JavaScript中class的静态属性和静态方法

    我们可以把一个方法赋值给类的函数本身,而不是赋给它的 "prototype" .这样的方法被称为 静态的(static). 例如这样: class Animal { static ...

  9. CSS – border-radius (Rounded Corners)

    前言 之前的文章 CSS – W3Schools 学习笔记 (3), 这篇独立出来写, 作为整理. 参考: Youtube – Advanced CSS Border-Radius Tutorial ...

  10. Asp.net core 学习笔记之 globalization & localization 复习篇

    更新: 2022-03-22 修订版: ASP.NET Core – Globalization & Localization 更新: 2021-06-15 之前有说过, 我没有使用默认的 f ...