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_reqnodelay 设置,因此直接使用 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 的错误。

至此问题得到解决,解决方案确实不够优雅 :<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值