为何Gunicorn+Gevent下time.localtime执行速度远慢于纯Flask?
某接口在Gunicorn+Gevent环境下运行耗时21秒,远慢于纯Flask环境的5秒。性能分析显示该接口调用了326457次Python内置方法time.localtime,以下是简化后的测试接口:
import time def my_time(): time.localtime() @app.route('/my_test', methods=['GET']) def my_test(): """ Test endpoint """ for i in range(326457): my_time() return 'OK'
纯Flask环境性能分析
ncalls tottime percall cumtime percall filename:lineno(function) 326457 0.204 0.000 0.387 0.000 __init__.py:23(my_time) 326457 0.183 0.000 0.183 0.000 {built-in method time.localtime} 326457 0.010 0.000 0.010 0.000 {method 'disable' of '_lsprof.Profiler' objects} 0 0.000 0.000 profile:0(profiler)
Gunicorn+Gevent环境性能分析
Gunicorn启动参数:gunicorn --workers=2 --threads=5 --timeout=300 --graceful-timeout=300 --worker-connections=100 --worker-class=gevent -b :8080 main:app
ncalls tottime percall cumtime percall filename:lineno(function) 326457 0.226 0.000 15.784 0.000 __init__.py:23(my_time) 326457 15.557 0.000 15.557 0.000 {built-in method time.localtime} 326457 0.016 0.000 0.016 0.000 {method 'disable' of '_lsprof.Profiler' objects} 0 0.000 0.000 profile:0(profiler)
环境版本:Python 3.6、gevent22.10.2、gunicorn20.1.0
原因分析
1. Gevent猴子补丁的锁开销
Gevent会通过猴子补丁替换Python标准库的time模块,为保证协程和线程环境下的线程安全,替换后的time.localtime带有全局锁。每次调用都需要执行锁的获取与释放操作,当调用次数达到30万级时,锁竞争的累计开销被急剧放大,直接导致总耗时飙升。
2. 多线程配置与Gevent协程模型冲突
你的Gunicorn启动参数设置了--threads=5,但Gevent worker本质是单线程协程模型,多线程配置会让每个worker进程内启动5个线程,每个线程又运行多个协程。这种多线程+协程的混合模式,会让time.localtime的全局锁在更多执行单元之间产生竞争,进一步加剧性能损耗。
3. 老版本Python的适配问题
Python 3.6属于较老的版本,Gevent 22.10.2对该版本的time模块补丁实现可能存在未优化的性能瓶颈,相比高版本Python,锁的开销控制更差,导致高频调用下性能下降更明显。
内容的提问来源于stack exchange,提问作者Abs

