翼度科技»论坛 编程开发 python 查看内容

[python]Gunicorn加持下的Flask性能测试

10

主题

10

帖子

30

积分

新手上路

Rank: 1

积分
30
前言

之前学习和实际生产环境的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脚本对请求的响应结果进行验证,以下脚本对响应码和响应内容进行校验
  1. wrk.method = "GET"
  2. wrk.host = "127.0.0.1:8080"
  3. wrk.path = "/health"
  4. wrk.timeout = 1.0
  5. response = function(status, headers, body)
  6.     if status ~= 200 then
  7.         print("Error: expected 200 but got " .. status)
  8.     end
  9.     if not body:find("ok") then
  10.         print("Error: response does not contain expected content.")
  11.     end
  12. end
复制代码
Flask框架的测试记录


  • 先测试默认运行方式,且没有sleep的情况下的并发性能。
  1. from flask import Flask
  2. app = Flask(__name__)
  3. @app.get("/health")
  4. def health():
  5.     return "ok"
  6. if __name__ == "__main__":
  7.     app.run(host="0.0.0.0", port=8080)
复制代码
使用命令nohup python demo.py > /dev/null启动,以下为wrk测试结果,可以看到已经出现超时请求。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency    65.47ms   81.07ms   1.99s    97.69%
  6.     Req/Sec   575.99    107.20     1.08k    66.71%
  7.   137538 requests in 1.00m, 22.82MB read
  8.   Socket errors: connect 0, read 114, write 0, timeout 144
  9. Requests/sec:   2288.49
  10. Transfer/sec:    388.86KB
复制代码

  • 还是默认启动方式,增加等待时间,模拟处理任务的时间消耗。后续测试都会增加等待时间。
  1. from flask import Flask
  2. from time import sleep
  3. app = Flask(__name__)
  4. @app.get("/health")
  5. def health():
  6.     sleep(0.1)
  7.     return "ok"
  8. if __name__ == "__main__":
  9.     app.run(host="0.0.0.0", port=8080)
复制代码
wrk测试结果,不出所料性能会有所下降。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   201.90ms  239.99ms   2.00s    95.05%
  6.     Req/Sec   479.79    185.87     1.82k    68.19%
  7.   114440 requests in 1.00m, 18.99MB read
  8.   Socket errors: connect 0, read 2, write 0, timeout 1833
  9. Requests/sec:   1904.45
  10. Transfer/sec:    323.62KB
复制代码

  • flask 更新到版本2后支持使用异步函数(需要安装异步相关依赖python -m pip install -U flask[async])
  1. from flask import Flask
  2. import asyncio
  3. app = Flask(__name__)
  4. @app.route('/health')
  5. async def health():
  6.     await asyncio.sleep(0.1)
  7.     return "ok"
  8. if __name__ == "__main__":
  9.     app.run(host="0.0.0.0", port=8080)
复制代码
wrk测试结果,性能相较于同步函数甚至还下降了,QPS几乎砍半,看来异步版Flask还有待增强。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   275.49ms  190.22ms   2.00s    95.46%
  6.     Req/Sec   242.86    104.25   720.00     65.91%
  7.   57896 requests in 1.00m, 9.61MB read
  8.   Socket errors: connect 0, read 48, write 0, timeout 611
  9. Requests/sec:    964.31
  10. Transfer/sec:    163.86KB
复制代码

  • 接管的Flask应用在本地使用gevent的WSGIServer运行,所以也来试试。
  1. from gevent.pywsgi import WSGIServer
  2. from flask import Flask
  3. from time import sleep
  4. app = Flask(__name__)
  5. @app.route('/health')
  6. def health():
  7.     sleep(0.1)
  8.     return "ok"
  9. if __name__ == "__main__":
  10.     http_server = WSGIServer(('0.0.0.0', 8080), app)
  11.     http_server.serve_forever()
复制代码
wrk测试结果,惨不忍睹,像是单线程在挨个处理请求,每个请求都会阻塞住。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   185.36ms  221.17ms   1.93s    96.30%
  6.     Req/Sec     5.54      3.39    10.00     46.55%
  7.   592 requests in 1.00m, 67.64KB read
  8.   Socket errors: connect 0, read 0, write 0, timeout 322
  9. Requests/sec:      9.85
  10. Transfer/sec:      1.13KB
复制代码

  • 按网上搜的结果加上了monkey patch
  1. from gevent.pywsgi import WSGIServer
  2. from gevent import monkey
  3. from flask import Flask
  4. from time import sleep
  5. monkey.patch_all()
  6. app = Flask(__name__)
  7. @app.route('/health')
  8. def health():
  9.     sleep(0.1)
  10.     return "ok"
  11. if __name__ == "__main__":
  12.     http_server = WSGIServer(('0.0.0.0', 8080), app)
  13.     http_server.serve_forever()
复制代码
wrk测试结果,加上monkey patch后似乎也没什么作用。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   182.89ms  209.48ms   1.82s    96.07%
  6.     Req/Sec     5.55      3.50    10.00     47.89%
  7.   592 requests in 1.00m, 67.64KB read
  8.   Socket errors: connect 0, read 0, write 0, timeout 312
  9. Requests/sec:      9.85
  10. Transfer/sec:      1.13KB
复制代码

  • 正式上gunicorn,代码没有任何改动,也不需要引用gevent的WSGServer。
  1. from flask import Flask
  2. from time import sleep
  3. app = Flask(__name__)
  4. @app.get("/health")
  5. def health():
  6.     sleep(0.1)
  7.     return "ok"
  8. if __name__ == "__main__":
  9.     app.run(host="0.0.0.0", port=8080)
复制代码
运行命令。指定-k gevent。demo:app中的demo是代码文件名。--worker-connections默认为1000
  1. gunicorn demo:app -b 0.0.0.0:8080 -w 4 -k gevent --worker-connections 2000
复制代码
wrk测试结果。性能相较于默认启动方式有了接近10倍的提升,请求响应时间也很稳定,最大响应时间也只有310.48。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   126.18ms    9.52ms 310.48ms   84.68%
  6.     Req/Sec     3.98k   165.70     4.53k    77.34%
  7.   948506 requests in 1.00m, 143.83MB read
  8. Requests/sec:  15799.31
  9. 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这一点)
  1. package main
  2. import (
  3.         "fmt"
  4.         "net/http"
  5.         "time"
  6. )
  7. func MyHandler(w http.ResponseWriter, r *http.Request) {
  8.         time.Sleep(time.Millisecond * 100)
  9.         w.Write([]byte("ok"))
  10. }
  11. func main() {
  12.         http.HandleFunc("/health", MyHandler)
  13.         err := http.ListenAndServe("0.0.0.0:8080", nil)
  14.         if err != nil {
  15.                 fmt.Println(err)
  16.         }
  17. }
复制代码
wrk结果如下,请求量是目前测试以来第一个突破百万,而且也没有timeout的出现。使用top观察资源消耗,CPU只占用了约30%,而且还只有一个进程。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   101.56ms    1.48ms 121.62ms   77.58%
  6.     Req/Sec     4.94k   191.07     5.05k    93.49%
  7.   1180108 requests in 1.00m, 132.80MB read
  8. Requests/sec:  19643.94
  9. Transfer/sec:      2.21MB
复制代码
不断加大连接,直到系统平均负载到达4(虚拟机CPU核心数为4)。连接数加了10倍,QPS差不多也是10倍于Flask + Gunicorn。这时候实际上wrk也占用了不少CPU资源,服务端的性能并没到瓶颈。
  1. $ wrk -s bm.lua -t 4 -c20000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 20000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   167.56ms   43.45ms 397.95ms   60.37%
  6.     Req/Sec    29.48k     8.84k   48.26k    63.05%
  7.   6867733 requests in 1.00m, 772.85MB read
  8. Requests/sec: 114258.70
  9. Transfer/sec:     12.86MB
复制代码
FastAPI

Go的性能已经很不错了,就性能来说还不是Flask+Gunicorn能媲美的。再来试试号称性能并肩Go的FastAPI(官网features里面写的)。FastAPI版本:0.115.4
纯uvicorn启动,用的是同步函数。
  1. from fastapi import FastAPI
  2. import uvicorn
  3. from fastapi.responses import PlainTextResponse
  4. from time import sleep
  5. app = FastAPI()
  6. @app.get("/health")
  7. def index():
  8.     sleep(0.1)
  9.     return PlainTextResponse(status_code=200,content="ok")
  10. if __name__ == '__main__':
  11.     uvicorn.run(app, host="127.0.0.1", port=8080, access_log=False)
复制代码
wrk测试结果,可以看到相当低下,甚至还不如flask的默认运行方式,超时请求数都过2w了。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency     1.22s   438.35ms   1.95s    60.00%
  6.     Req/Sec   115.03     57.31   410.00     86.69%
  7.   23494 requests in 1.00m, 3.02MB read
  8.   Socket errors: connect 0, read 0, write 0, timeout 22894
  9. Requests/sec:    390.92
  10. Transfer/sec:     51.54KB
复制代码
改用异步函数再试试。
  1. from fastapi import FastAPI
  2. import uvicorn
  3. from fastapi.responses import PlainTextResponse
  4. from time import sleep
  5. import asyncio
  6. app = FastAPI()
  7. @app.get("/health")
  8. async def health():
  9.     await asyncio.sleep(0.1)
  10.     return PlainTextResponse(status_code=200,content="ok")
  11. if __name__ == '__main__':
  12.     uvicorn.run(app, host="127.0.0.1", port=8080, access_log=False)
复制代码
wrk测试结果,可以看到性能好很多了,而且没有timeout。QPS是Flask默认启动方式的2倍,但实际性能应该不止2倍。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   484.53ms   63.18ms 654.48ms   61.26%
  6.     Req/Sec     1.09k   698.29     3.13k    63.78%
  7.   246744 requests in 1.00m, 31.77MB read
  8. Requests/sec:   4106.80
  9. Transfer/sec:    541.43KB
复制代码
uvicorn支持指定worker数,这里设置为CPU核心数。
  1. from fastapi import FastAPI
  2. import uvicorn
  3. from fastapi.responses import PlainTextResponse
  4. from time import sleep
  5. import asyncio
  6. app = FastAPI()
  7. @app.get("/health")
  8. async def health():
  9.     await asyncio.sleep(0.1)
  10.     return PlainTextResponse(status_code=200,content="ok")
  11. if __name__ == '__main__':
  12.     uvicorn.run(app="demo2:app", host="127.0.0.1", port=8080, access_log=False, workers=4)
复制代码
wrk测试结果,响应时间还是非常稳的,完全没有timeout的情况,延迟还更低。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   164.52ms   13.57ms 273.27ms   73.49%
  6.     Req/Sec     3.05k   544.20     4.64k    68.67%
  7.   727517 requests in 1.00m, 93.73MB read
  8. Requests/sec:  12123.17
  9. Transfer/sec:      1.56MB
复制代码
gunicorn也支持uvicorn,看看fastapi在gunicorn的加持下会有怎样的性能表现。
  1. gunicorn demo2:app -b 127.0.0.1:8080 -w 4 -k uvicorn.workers.UvicornWorker --worker-connections 2000
复制代码
wrk测试结果,相较于unicorn运行方式,性能提升并不多。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   146.43ms   21.21ms 263.13ms   69.52%
  6.     Req/Sec     3.43k   508.71     4.88k    71.40%
  7.   818281 requests in 1.00m, 105.35MB read
  8. Requests/sec:  13620.16
  9. Transfer/sec:      1.75MB
复制代码
Sanic

之前用过一段时间Sanic,也是个python异步框架,版本:24.6.0
  1. from sanic import Sanic
  2. from sanic.response import text
  3. import asyncio
  4. app = Sanic("HelloWorld")
  5. @app.get("/health")
  6. async def hello_world(request):
  7.     await asyncio.sleep(0.1)
  8.     return text("ok")
  9. if __name__ == "__main__":
  10.     app.run(host="127.0.0.1", port=8080, fast=True, debug=False, access_log=False)
复制代码
wrk测试结果。虽然QPS比FastAPI高,但是有timeout的情况,不是很稳定。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   104.05ms   45.30ms   1.82s    99.59%
  6.     Req/Sec     4.84k   538.65     5.07k    95.98%
  7.   1154792 requests in 1.00m, 115.64MB read
  8.   Socket errors: connect 0, read 0, write 0, timeout 88
  9. Requests/sec:  19218.08
  10. Transfer/sec:      1.92MB
复制代码
Openresty

openresty基于nginx,通过集成lua,也可以用来写api。配置如下,只是增加了一个location,稍微调整下nginx的参数
  1. worker_processes  auto;
  2. worker_cpu_affinity auto;
  3. events {
  4.     worker_connections  65535;
  5. }
  6. http {
  7.     include       mime.types;
  8.     default_type  application/octet-stream;
  9.     access_log off;
  10.     sendfile        on;
  11.     keepalive_timeout  65;
  12.     server {
  13.         listen       8080 deferred;
  14.         server_name  localhost;
  15.         location /health {
  16.             content_by_lua_block {
  17.                 ngx.sleep(0.1)
  18.                 ngx.print("ok")
  19.             }
  20.         }
  21.         location / {
  22.             root   html;
  23.             index  index.html index.htm;
  24.         }
  25.         error_page   500 502 503 504  /50x.html;
  26.         location = /50x.html {
  27.             root   html;
  28.         }
  29.     }
  30. }
复制代码
wrk测试结果,和Go语言相当。
  1. $ wrk -s bm.lua -t 4 -c2000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://192.168.0.201:8080/health
  3.   4 threads and 2000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   101.43ms    1.62ms 136.59ms   88.97%
  6.     Req/Sec     4.94k   213.51     5.33k    92.65%
  7.   1178854 requests in 1.00m, 211.36MB read
  8. Requests/sec:  19619.86
  9. Transfer/sec:      3.52MB
复制代码
top观察openresty的cpu占用并不高,加大连接再试试。连接数达到25000后,系统平均负载已经基本满了,而且wrk也占用了不少CPU资源。和Go差不多,并没有到服务端的性能瓶颈,而是受到系统资源限制。
  1. $ wrk -s bm.lua -t 4 -c25000 -d60s http://127.0.0.1:8080/health
  2. Running 1m test @ http://127.0.0.1:8080/health
  3.   4 threads and 25000 connections
  4.   Thread Stats   Avg      Stdev     Max   +/- Stdev
  5.     Latency   149.17ms   36.61ms 659.40ms   71.22%
  6.     Req/Sec    41.00k     6.64k   59.03k    65.95%
  7.   9330399 requests in 1.00m, 1.63GB read
  8. Requests/sec: 155277.58
  9. Transfer/sec:     27.84MB
复制代码
小结

整理下测试数据汇总成表格如下
项目总请求量每秒请求量平均响应时间最大响应时间备注Flask-no sleep1375382288.4965.47ms1.99s有响应超时情况Flask-同步方式1144401904.45201.90ms2.00s有响应超时情况Flask-异步函数57896964.31275.49ms2.00s有响应超时情况Flask+gevent5929.85185.36ms1.93s有响应超时情况Flask+gevent(monkeypatch)5929.85182.89ms1.82s有响应超时情况Flask+gevent+unicorn94850615799.31126.18ms310.48msGolang118010819643.94101.56ms121.62msGolang6867733114258.70167.56ms397.95mswrk的配置为t4c20000FastAPI-同步函数23494390.921.22s1.95s有响应超时情况FastAPI-异步函数2467444106.80484.53ms654.48msFastAPI-多worker72751712123.17164.52ms273.27msFastAPI+Gunicorn81828113620.16146.43ms273.27msSanic115479219218.08104.05ms1.82s有响应超时情况,不是很稳定OpenResty117885419619.86101.43ms136.59msOpenResty9330399155277.58149.17ms659.40mswrk的配置为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了,服务扩展也简单,性能不够就加实例嘛
来源:https://www.cnblogs.com/XY-Heruo/p/18522244
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具