最近被求助一个问题:
使用 fastAPI 部署 python web 服务时,async 一个AI 推理函数不起作用,导致一个推理请求进来后,后续进来的请求被阻塞了。

问题分析思路

官网的异步示例代码如下:

1
2
3
4
@app.get('/predict')
async def predict():
results = await some_pytorch_predict()
return results

此时观察现象: 请求进来时 CPU 瞬间拉满,因为 pytorch 后端是通过 Cython 调用的 C++,所以可以突破 python 单进程只能跑满一个核的问题。

  1. 首先考虑是代码的问题。
    因为 fastAPI async 的特性,有些函数就是无法被打断,比如 time.sleep(1), 换成 asyncio.sleep 即可实现 async 的功能。 github issue

  2. 此时考虑到第三方库,尤其是这种 pytorch 后端实现都是跨 c++ 的,可能真的不被打断。
    还有一种情况就是此时 CPU 已经被跑满了,导致新的请求即使进来也不会被执行到。考虑限制 pytorch 最大 cpu 占用,但此时还是能跑满一个核,且此时还是不能被打断。

  3. 既然代码层面解决不了,那有没有可能从部署层面考虑?

    使用 gunicorn 启动该服务,设置多个 worker,即使一个worker 跑满那个进程,此时一个同等优先级的 worker 进程去和它竞争还是可以抢到资源的。

    此时的确解决了该问题。但因为 gunicorn 启动造成了pytorch 有多份,既造成了大量的资源浪费,也无法服务超过 worker 数的请求:即同时有 worker 数的推理请求进来,所有 worker 进程还是会跑满 worker,使得低计算消耗的请求会阻塞很久。

  4. 有没有更好的方法去优雅地解决呢?
    最后的解决方案是使用 multiThread 去包装一下第三方无法被打断的 AI 推理函数。这种方案完美的解决了这个问题。

    其实 python 里面的 multiThread 一直被调侃,毕竟一个python 进程,即使开再多线程也只能去分一个核的计算时间片,不能很显著提高效率,平时使用也会用 multiProcess 去起多个字进程占用多个核的资源来提高资源利用率。但是这类 thread_pool 类代码,肯定是要实现调度的,也就是被打断是从根本上支持的,用其来包装一个不可被打断的函数,简直绝配。

总结

这次的问题其实本质上还是因为架构设计不合理导致的,后来的解决方案也只是目前的系统最简单也最优雅的一种解决方案。关键的这类的问题最后还是落在无状态服务调用计算密集型组件下如何保证服务的高可用的问题。