
fastapi 如何控制并发——其一
先说结论,单独靠这里的业务场景是:单机只启动一个进程,也就是,同时只能只处理一个请求,其他的请求全部拒绝,而不进行排队。我们使用了fastapi。
业务背景
先说结论,单独靠 gunicorn+fastapi
很难实现并发控制,注意这里的并发控制有特殊的含义:
假如并发设置为8,那么我们预期的结果是,如果当前已经有8个请求正在处理,那么立刻拒绝掉期间收到的其他请求,或者能够自行控制请求的等待时间。
这里的业务场景是:单机只启动一个进程,也就是 gunicorn:worker=1
,同时只能只处理一个请求,其他的请求全部拒绝,而不进行排队。
我们使用了 fastapi
实现 HTTP 服务:
# -*- coding: utf-8 -*-
from fastapi import FastAPI
import time
app = FastAPI()
@app.get("hello")
async def echo_feature():
time.sleep(6)
print("hello")
尝试过的路径
我们尝试了以下几种方法:
排队超时
事实上,gunicorn
本身并没有排队机制,issue1492 issue1190 上说明了这点,gunicorn
只会将请求的socket挂起,等待空闲的 worker,因此不存在可设置的排队超时参数,而 timeout
以及 max_request
都是为了避免后端服务阻塞或者内存泄露而重启worker的参数。
套接字限制
有些方法提到了使用 backlog 来设置 gunicorn
能够挂起的最大连接数,也就是这些连接会处于 TIME_WAIT
状态,理论上如果当前设置 backlog=1
,每次只会有一个请求正在等待连接,但事实远不如预期,操作系统会平衡 backlog 的长度以及丢弃请求的频率,issue1190 讨论了这个问题。并且,TCP底层有自己的重试机制,因此不会立刻向客户端报错,在我这里测试结果是,需要200ms客户端才能收到 dail tcp timeout
错误。
业务实现并发控制
后来我们想不让 gunicorn
进行并发控制,我们在 worker
层面通过线程锁或者线程安全计数器等实现并发控制,但我们发现想让gunicorn
把多个请求打到一个 worker
上,需要让 worker
支持异步。众所周知啊,python的异步只能用在io阻塞时,例如:
import asyncio
import logging
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI
app = FastAPI()
app.state.running = False
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def limit_requests_middleware(request, call_next):
if app.state.running:
print("reject request, cause app is running")
return JSONResponse(status_code=503, content={"message": "Service unavailable"})
app.state.running = True
try:
response = await call_next(request)
return response
finally:
app.state.running = False
@app.get("/hello")
async def echo_feature():
await asyncio.sleep(6)
print("hello")
这里使用了 middleware
查询 worker
的状态,简单实现控制单 worker
并发为1,并且能够在 worker
繁忙时,立刻拒绝其他的请求。
对于计算密集型的服务,python的异步就是笑话,因此这种方法也被pass了。
这里可以将 await asyncio.sleep(6)
修改成同步的休眠: time.sleep(6)
,会发现请求还是会阻塞等待。
Flask反向代理
最终还是选择了Flask作为反向代理实现并发控制,但是期间也不见得很顺利。一开始,我们想通过Flask自身的限流模块来实现并发控制,例如:
http {
limit_conn_zone all zone=conn_limit:10m;
server{
listen 80;
location / {
limit_conn conn_limit 1;
proxy_pass http://0.0.0.0:8080;
}
}
}
但遗憾的是 Flask 并没有将超过并发数的请求排队timeout暴露出来,比如 limit_req
的 nodelay
设置,因此直接使用 limit_conn
似乎走不通。那就换个思路,限制连接数总可以吧,OK,我们设置连接数:
events {
use epoll;
worker_connections 1;
}
那这里的 worker_connections
是不是设置成 1呢?其实不然,这里的 worker_connections
指的是单个工作进程能够同时打开的连接数:
Sets the maximum number of simultaneous connections that can be opened by a worker process.
It should be kept in mind that this number includes all connections (e.g. connections with proxied servers, among others), not only connections with clients. link
当然也包括代理服务和后端服务之间的连接,以及代理服务和客户端之间的连接:
如果设置成2,nginx能够正常运行,但是任意请求都会出先 dail tcp fail
,我猜测是 nginx
用于监听 worker
的连接,不太确定;如果设置成3,客户端会得到 nginx 给出的500错误,nginx错误日志显示:
worker_connections are not enough while connecting to upstream
也就是客户端能够与nginx worker 创建连接,但是由于连接数不够,worker不能与后端服务建立连接,因此我们需要设置 worker_connections 4;
超过并发数的连接,nginx会主动关闭连接,客户端将会收到 Post "http://0.0.0.0:80": EOF
的错误。
至此问题得到解决,解决方案确实不够优雅 :<
更多推荐
所有评论(0)