具有非异步请求处理的 FastAPI Scale
微信搜索关注《Python学研大本营》,加入读者群,分享更多精彩
FastAPI是一个 Web 服务器框架,特别适合与 asyncio 兼容的流程一起使用。但是,仍然有很多框架和库不支持 asyncio,如果链上的任何部分不支持,asyncio 的好处就无法充分发挥。
def在轶事中,我们在容器化环境 ( GCP Cloud Run )中使用了 gunicorn+uvicorn+端点。据观察,当服务繁忙时,请求延迟会增加,本应花费几秒钟的请求却需要一分多钟才能完成。
下图显示了我们的一项微服务随时间变化的请求率(绿线)和平均延迟(蓝线)。可以看出,当服务在短时间内收到大量请求时,平均延迟会增加,正如之前在小示例中看到的那样。
(这些线有不同的单位——绿线是请求的计数,蓝线的单位是秒,但图表仍然说明了问题)
当我们从容器中取出并发平衡时,它看起来像这样 -
当异步使用 FastAPI 时,所有任务都在单个线程中执行,而 asyncio 事件循环通过允许它们之间的上下文切换来管理并发请求的处理。
@app.get('/')
async def read_results():
results = await some_library()
return results
但是,您也可以定义非async def端点。在这种情况下,将为每个新请求创建一个新线程 -
处理期间四个并发同步请求
这样,FastAPI 似乎可以并行处理请求,但这并不完全准确。
如果不讨论 CPython 中的全局解释器锁 (GIL),一篇关于 Python 多线程的文章是不完整的。考虑以下端点定义:
@app.get("/gil")
def gil():
start = time.time()
logger.info(f"Running on {os.getpid()}")
x = 1
for i in range(100000000):
x += 1
logger.info(f"Took {time.time()-start}")
在一个请求之后localhost:8000/gil,我们得到 -
INFO:root:Running on 43606
INFO:root:Took 8.778410911560059
INFO: 127.0.0.1:53148 - "GET /gil HTTP/1.1" 200 OK
因此,请求处理花费了 8.77 秒。在尝试一起发送两个请求后,我们得到:
INFO:root:Running on 43606
INFO:root:Running on 43606
INFO:root:Took 16.932676076889038
INFO: 127.0.0.1:53156 - "GET /gil HTTP/1.1" 200 OK
INFO:root:Took 16.911554098129272
INFO: 127.0.0.1:53157 - "GET /gil HTTP/1.1" 200 OK
即使每个请求都在多核机器上的不同线程上运行,每个请求的完成时间也是以前的两倍。什么 ?!
CPython 中的全局解释器锁 (GIL) 限制了 Python 多线程功能的有效性。GIL 是保护共享内部内存结构的全局互斥锁,必须在线程执行期间获取和释放。这意味着即使两个 Python 线程运行在不同的内核上,它们仍然会花时间等待互斥量。
因此,使用 Python 的多线程模型处理两个 CPU-bound 请求实际上会增加整体执行时间,并且随着请求数量的增加,问题会变得更糟。
要在 Python 中实现真正的并行性,通常建议使用多个进程,这可以使用内置multiprocessing库来实现。要运行具有多个进程的 Web 服务器应用程序,可以使用像gunicorn这样的库在多个相同的 worker 之间分配请求以达到负载平衡的目的。
Gunicorn 提供了几个不同的工人阶级。worker 负责接受新连接并将请求转发给特定应用程序(例如 Flask 或 FastAPI)的 HTTP 处理程序。FastAPI 使用 ASGI 框架 (starlette) 并需要一个 asyncio 事件循环来运行,但 Gunicorn 默认情况下不创建一个。
为了弥合这一差距并允许 FastAPI 与 Gunicorn 一起使用,uvicorn workers 可以与 Gunicorn 结合使用。在此配置中,Gunicorn 充当流程管理器,确保有必要数量的工作人员可用,而 uvicorn 工作人员在事件循环中处理 FastAPI 的执行,接受新连接并将它们转发给应用程序。问题仍然存在:这是否会改善之前观察到的性能问题?
让我们设置我们的 main.py 文件——
import logging
import time
from fastapi import FastAPI
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
app = FastAPI()
logger = logging.getLogger()
@app.get("/gil")
def gil():
start = time.time()
logger.info(f"Running on {os.getpid()}")
x = 1
for i in range(100000000):
x += 1
logger.info(f"Took {time.time()-start}")
并如前所述运行它 -
gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
我们一起发送 8 个请求到localhost:8000/gil. 首先,我们立即得到第一条日志——
INFO:root:Running on 46222
INFO:root:Running on 46222
INFO:root:Running on 46220
INFO:root:Running on 46222
INFO:root:Running on 46220
INFO:root:Running on 46220
INFO:root:Running on 46222
INFO:root:Running on 46222
处理结束后,我们会得到
INFO:root:Took 13.02550196647644
INFO:root:Took 13.236494064331055
INFO:root:Took 13.187334299087524
INFO:root:Took 20.621056079864502
INFO:root:Took 21.365866661071777
INFO:root:Took 22.34864592552185
INFO:root:Took 21.29911184310913
INFO:root:Took 21.295140743255615
从这个实验中可以得出两个主要结论:
尽管启动了 4 个 worker,但只打印了 2 个进程 ID,表明只有一半的 worker 正在接收请求。
全局解释器锁 (GIL) 继续导致同一工作程序上的多个线程变慢。
这表明 worker 和同一 worker 上的多个线程之间的负载平衡不佳并没有有效地利用资源。
如果将端点更改为使用async def函数,则在发送相同数量的请求时,服务器的行为会有所不同 -
INFO:root:Running on 46629
INFO:root:Running on 46626
INFO:root:Running on 46627
INFO:root:Running on 46628
INFO:root:Took 5.622344255447388
INFO:root:Running on 46629
INFO:root:Took 5.798593044281006
INFO:root:Took 5.895605087280273
INFO:root:Took 5.744417905807495
INFO:root:Took 4.698776006698608
INFO:root:Running on 46629
INFO:root:Took 4.694264888763428
INFO:root:Running on 46629
INFO:root:Took 4.973871231079102
INFO:root:Running on 46629
INFO:root:Took 4.8002729415893555
前四个请求由不同的工作人员处理。
接下来的四个请求必须等到前面的一个请求完成,因为端点的逻辑是 CPU 绑定的而不是 IO 绑定的。
每个请求都得到更快的服务(不考虑排队时间)。
在高层次上,uvicorn worker 的逻辑可以描述如下:
从 Gunicorn 获取监听套接字。
使用asyncio 低级框架创建 TCP 服务器。
对于每个新连接,使用指定的协议将请求传递给 FastAPI 应用程序。
Asyncio TCP 服务器是单线程的,旨在在事件循环中运行。这意味着它们在同一线程上处理新连接和请求,如果请求处理受 CPU 限制或与 asyncio 不兼容,服务器将无法接受新连接。
在 FastAPI 中使用def端点时,每个新请求都在单独的线程上处理,这允许 asyncio TCP 服务器接受新连接。这就是为什么同一个工作人员在使用 def 端点时会收到越来越多的请求。
至于为什么Gunicorn不对请求进行负载均衡,答案是它根本就没有执行这个功能。Gunicorn 只初始化监听套接字,创建工人,并确保他们是活的。它不会在工作人员之间分发请求。
如果在容器化环境中使用 FastAPI,则可以通过在容器管理器(例如 k8s,托管管理器)级别执行负载平衡来解决上述问题。这有助于:
通过在 Python 中不使用多线程来避免 GIL 问题。
当多个请求到达时,通过在不同的容器上运行它们而不是在同一个容器实例上同时运行来扩展应用程序。
FastAPI 被设计为异步使用。以非异步方式使用它给我们带来了问题,我们必须更好地理解如何使用上述框架来改进我们的指标。拥有良好的基本可见性和适当的监控也很重要,这样才能及时发现和调查问题。
本书针对深度学习及开源框架——PyTorch,采用简明的语言进行知识的讲解,注重实战。全书分为4篇,共19章。深度学习基础篇(第1章~第6章)包括PyTorch简介与安装、机器学习基础与线性回归、张量与数据类型、分类问题与多层感知器、多层感知器模型与模型训练、梯度下降法、反向传播算法与内置优化器。计算机视觉篇(第7章~第14章)包括计算机视觉与卷积神经网络、卷积入门实例、图像读取与模型保存、多分类问题与卷积模型的优化、迁移学习与数据增强、经典网络模型与特征提取、图像定位基础、图像语义分割。自然语言处理和序列篇(第15章~第17章)包括文本分类与词嵌入、循环神经网络与一维卷积神经网络、序列预测实例。生成对抗网络和目标检测篇(第18章~第19章)包括生成对抗网络、目标检测。
本书适合人工智能行业的软件工程师、对人工智能感兴趣的学生学习,同时也可作为深度学习的培训教程。
日月光华:网易云课堂资深讲师,经验丰富的数据科学家和深度学习算法工程师。擅长使用Python编程,编写爬虫并利用Python进行数据分析和可视化。对机器学习和深度学习有深入理解,熟悉常见的深度学习框架( PyTorch、TensorFlow)和模型,有丰富的深度学习、数据分析和爬虫等开发经验,著有畅销书《Python网络爬虫实例教程(视频讲解版)》。
购买链接(新书限时5.5折):https://item.jd.com/13528847.html
精彩回顾
《Pandas1.x实例精解》新书抢先看!
【第1篇】利用Pandas操作DataFrame的列与行
【第2篇】Pandas如何对DataFrame排序和统计
【第3篇】Pandas如何使用DataFrame方法链
【第4篇】Pandas如何比较缺失值以及转置方向?
【第5篇】DataFrame如何玩转多样性数据
【第6篇】如何进行探索性数据分析?
【第7篇】使用Pandas处理分类数据
【第8篇】使用Pandas处理连续数据
【第9篇】使用Pandas比较连续值和连续列
【第10篇】如何比较分类值以及使用Pandas分析库
微信搜索关注《Python学研大本营》
访问【IT今日热榜】,发现每日技术热点