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 的错误。

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

### FastAPI 中实现并发后台任务及通知机制 在现代 Web 应用程序中,处理长时间运行的任务而不阻塞主线程至关重要。对于这种情况,在 FastAPI 中可以通过 `BackgroundTasks` 来执行并发后台任务,并利用消息队列或其他异步通信工具来发送通知。 #### 使用 BackgroundTasks 执行并发任务 FastAPI 提供了一个简单的接口用于启动后台任务——即 `BackgroundTasks` 对象。这允许应用程序在接受请求之后继续执行某些操作而不会影响响应时间[^1]。 ```python from fastapi import FastAPI, BackgroundTasks app = FastAPI() def write_log(message: str): with open("log.txt", mode="a") as log_file: log_file.write(f"{message}\n") @app.post("/send-notification/") async def send_notification(email: str, background_tasks: BackgroundTasks): background_tasks.add_task(write_log, f"Notification sent to {email}") return {"message": "Notification will be sent"} ``` 上述例子展示了如何定义一个 POST 请求处理器 `/send-notification/` ,它接受电子邮件地址作为参数并将日志记录工作交给背景进程去完成。这样做的好处是可以立即返回 HTTP 响应给客户端,同时确保后续的日志写入动作会在后台被执行。 #### 结合 Celery 发送通知 为了更复杂的通知逻辑或需要分布式处理的情况,推荐使用像 Celery 这样的任务队列系统。Celery 支持多种消息代理(如 RabbitMQ 或 Redis),能够很好地与 FastAPI 整合起来以实现高效的消息传递和任务调度功能。 安装依赖: ```bash pip install celery redis ``` 配置 Celery 和创建任务函数: ```python from celery import Celery celery_app = Celery('tasks', broker='redis://localhost') @celery_app.task def notify_user(user_id): # 模拟向用户发送邮件的过程... pass ``` 修改之前的路由方法调用 Celery 任务而不是直接添加到 `background_tasks`: ```python @app.post("/notify-user/{user_id}/") async def trigger_notify(user_id: int): notify_user.delay(user_id) return {"message": "User notification has been scheduled."} ``` 这种方式不仅实现了真正的异步处理,还具备更好的可扩展性和可靠性特性,适用于生产环境下的大规模应用部署场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值