检索增强生成RAG项目tools_04:flask➕fastapi➕高并发

前面我们介绍了Docker部署➕ollama➕logging➕bm25➕mysql➕redis➕milvus等RAG项目中各个必不可少的tools,本篇主要讲的是flask➕fastapi➕高并发!!!

从同步到异步,从单线程到高并发,理解RAG服务的性能之钥!!!

前言

在RAG系统中,服务端并发能力直接决定了系统能支撑多少用户同时使用。今天,我们深入探讨Python生态中两种主流的Web框架------Flask(同步)和FastAPI(异步),以及它们背后的并发模型:多进程、多线程、协程。


一、并发编程的三驾马车

python 复制代码
并行处理的三种方式 = {
    "多进程": "Process(真并行,适合CPU密集型)",
    "多线程": "Thread(假并行,适合IO密集型)",
    "协程": "Coroutine(超高并发,适合大量IO等待)"
}

多进程(Process)- 真并行
场景:计算 1 亿个数的平方和(CPU 密集型)>> CPU算到冒烟 → 多进程
8核CPU,同时开8个进程 >> 8个CPU同时干活,速度提升8倍 >> 计算量大 → 多进程

多线程(Thread)- 假并行
场景:爬 100 个网页(IO 密集型,等网络响应) >> 等网络/等磁盘 → 多线程

协程(Coroutine)- 超高并发
场景:Web 服务器同时处理 10,000 个用户请求 >> 单线程处理1万个并发,内存占用极低
成千上万个连接 → 协程

1.1 基础示例代码

python 复制代码
import time
import asyncio
import threading
from multiprocessing import Process

# 普通同步任务
def do_some_thing(i):
    print("开始执行任务:" + str(i))
    time.sleep(5)
    print("已经完成任务" + str(i))

# 异步协程任务
async def do_some_thing_asyncio(i):
    print("开始执行任务:" + str(i))
    await asyncio.sleep(5)  # 关键:await释放控制权
    print("已经完成任务" + str(i))

1.2 三种并发模式对比

python 复制代码
# 多进程
def test_multi_process():
    for i in range(10):
        t = Process(target=do_some_thing, args=(i,))
        t.start()
    # 10个进程,每个进程独立内存空间

# 多线程
def test_multi_thread():
    for i in range(10):
        t = threading.Thread(target=do_some_thing, args=(i,))
        t.start()
    # 10个线程,共享内存空间,受GIL限制

# 协程
async def test_asyncio():
    tasks = []
    for i in range(10):
        tasks.append(do_some_thing_asyncio(i))
    await asyncio.gather(*tasks)
    # 单线程,事件循环调度

1.3 三种模式的本质区别

特性 多进程 多线程 协程
创建开销 极小
内存占用 高(独立内存) 中(共享内存) 低(共享内存)
切换成本 高(上下文切换) 极低
真并行 ✅(多核) ❌(GIL限制) ❌(单线程)
适用场景 CPU密集型 IO密集型 高并发IO

思考

问:为什么多线程是"假并行"?

答:Python有GIL(全局解释器锁),同一时刻只有一个线程在执行Python字节码。多线程在CPU计算时是串行的,但在IO等待时会释放GIL,所以适合IO密集型任务。


二、Flask:同步框架的典范

2.1 同步的本质

python 复制代码
from flask import Flask
import time

app = Flask(__name__)

@app.route('/cook_sync/', methods=['GET', 'POST'])
def cook_sync():
    print("开始做菜 A...")
    time.sleep(5)  # 同步阻塞!整个线程被卡住
    print("菜 A 做好了!")
    return "菜 A 完成"

同步的含义

  • 执行到time.sleep(5)时,整个线程停止响应

  • 这5秒内,该线程不能处理任何其他请求

  • 如果只有一个线程,其他请求必须排队等待

2.2 Flask的并发模式

python 复制代码
if __name__ == '__main__':
    # 模式1:单线程(默认,但新版Flask默认多线程)
    app.run(threaded=False)
    
    # 模式2:多线程
    app.run(threaded=True)
    
    # 模式3:多进程(生产环境用gunicorn)
    # gunicorn -w 4 app:app

2.3 亲自验证:单线程 vs 多线程

python 复制代码
"""
# 实验:同时发送10个请求

# 单线程模式(threaded=False)
# 输出:
# 开始做菜 A...(请求1)
# (等待5秒)
# 菜 A 做好了!
# 开始做菜 A...(请求2)
# (等待5秒)
# 菜 A 做好了!
# 总耗时:50秒

# 多线程模式(threaded=True)
# 输出:
# 开始做菜 A...(请求1)
# 开始做菜 A...(请求2)
# ...
# 开始做菜 A...(请求10)
# (等待5秒)
# 菜 A 做好了!(全部几乎同时)
# 总耗时:5秒
"""

关键发现

  • 新版Flask开发服务器默认就是多线程(与文档说的不同!)

  • 多线程模式下,10个请求可以并发处理

  • 但每个请求仍然需要独立的线程(资源开销大

2.4 Flask的瓶颈

python 复制代码
Flask并发限制 = {
    "线程数上限": "约1000-2000(内存限制)",
    "1000个并发": "需要1000个线程 → 内存~8GB",
    "10000个并发": "需要10000个线程 → 内存爆炸 💥"
}

三、FastAPI:异步框架的崛起

3.1 异步的本质

python 复制代码
from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.post('/cook_async')
async def cook_async():
    print("开始做菜 A (异步)...")
    await asyncio.sleep(5)  # 关键:await让出控制权
    print("菜 A (异步) 做好了!")
    return "菜 A (异步) 完成"

异步的含义

  • 执行到await asyncio.sleep(5)时,任务被挂起

  • 事件循环立即切换到其他任务

  • 5秒后,事件循环回来继续执行

3.2 await的魔力

python 复制代码
# 异步等待的示意图
async def handle_request():
    print("1. 开始处理")
    await asyncio.sleep(5)  # ← 挂起点
    print("3. 5秒后继续")
    return "完成"

# 事件循环调度
# 请求1: 1 → 挂起 → 切换
# 请求2: 1 → 挂起 → 切换  
# 请求3: 1 → 挂起
# ... 5秒后 ...
# 请求1: 3 → 完成
# 请求2: 3 → 完成

3.3 协程 vs 线程

python 复制代码
对比 = {
    "协程": {
        "调度单位": "函数内的await点",
        "切换开销": "极低(Python级别)",
        "内存占用": "~2KB/协程",
        "10w并发": "~200MB内存 ✅"
    },
    "线程": {
        "调度单位": "操作系统线程",
        "切换开销": "高(内核级切换)",
        "内存占用": "~8MB/线程",
        "1w并发": "~8GB内存 ❌"
    }
}

四、高并发场景实战

4.1 客户端压力测试代码

python 复制代码
import threading
import requests
import time

def test_request(i):
    print(f"请求{i} 发起")
    start = time.time()
    resp = requests.post("http://localhost:8002/cook_async")
    elapsed = time.time() - start
    print(f"请求{i} 完成,耗时 {elapsed:.1f}秒")

def stress_test(concurrent=100):
    threads = []
    start_time = time.time()
    for i in range(concurrent):
        t = threading.Thread(target=test_request, args=(i,))
        t.start()
        threads.append(t)
    
    for t in threads: 
        t.join()    # 外面的同步主进程等待每个进程跑完,相当于是给主进程➕await
    
    total = time.time() - start_time
    print(f"总耗时: {total:.1f}秒")
    print(f"平均响应: {total/concurrent:.1f}秒")

if __name__ == '__main__':
    stress_test(100)

4.2 不同框架的性能对比

并发数 Flask(threaded=False) Flask(threaded=True) FastAPI
10 50秒 5秒 5秒
100 500秒 5秒 5秒
500 2500秒 5秒 5秒
1000 ❌ 内存爆炸 5秒(但内存高) 5秒
10000 ❌ 无法启动 ❌ 内存爆炸 5秒

4.3 真实案例:为什么FastAPI能扛住?

我在这里的理解是:单线程事件循环 + await 非阻塞"------遇到 IO 不傻等,切去处理其他请求,IO 完成再回来。1个线程干1000个线程的活!!!

python 复制代码
# 模拟10000个并发请求

# Flask方案
需要线程数 = 10000
内存占用 = 10000 × 8MB = 80GB  # 💥 服务器崩溃

# FastAPI方案
需要线程数 = 1(事件循环线程)
协程数 = 10000
内存占用 = 10000 × 2KB ≈ 20MB  # ✅ 轻松应对

五、深入理解:踩过的坑

5.1 坑1:Flask到底是单线程还是多线程?

python 复制代码
"""
# 实验发现
app.run()  # 默认就是多线程!

# 为什么和文档说的不一样?
# 原因:新版Werkzeug开发服务器默认threaded=True
# 这是开发服务器的行为,生产环境仍然需要用gunicorn
"""

经验教训

  • 不要完全相信文档,动手验证

  • 开发服务器和生产服务器行为可能不同

5.2 坑2:多线程真的能无限并发吗?

python 复制代码
# 验证
for i in range(10000):
    threading.Thread(target=test).start()
# 结果:内存爆炸,系统卡死

# 原因
每个线程 ≈ 8MB 内存
10000线程 = 80GB内存
还有线程切换的CPU开销

5.3 坑3:异步能提高计算速度吗?

python 复制代码
# ❌ 错误理解
async def compute():
    result = heavy_calculation()  # CPU密集
    return result
# 异步不会让计算变快!

# ✅ 正确理解
async def io_wait():
    await asyncio.sleep(5)  # IO等待
    return "done"
# 异步让等待时间可以被复用

六、RAG系统中如何选择?

6.1 决策树

python 复制代码
def choose_framework(qps, task_type):
    if qps < 100:
        return "Flask + threaded=True"
    elif qps < 1000:
        return "Flask + Gunicorn (多进程)"
    elif task_type == "IO密集型":
        return "FastAPI(异步)"
    else:  # CPU密集型
        return "FastAPI + 多进程worker"

6.2 RAG场景分析

python 复制代码
RAG请求的生命周期 = {
    "1. 接收请求": "IO(网络)",
    "2. 向量检索": "IO(Milvus网络调用)+ CPU(距离计算)",
    "3. LLM生成": "IO(等待模型响应)",
    "4. 返回结果": "IO(网络)"
}

# 结论:RAG是典型的IO密集型任务
# 最适合:FastAPI异步框架

6.3 生产环境推荐配置

python 复制代码
# 方案1:中小规模(QPS < 500)
FastAPI + Uvicorn(单worker)

# 方案2:中大规模(QPS 500-2000)
FastAPI + Uvicorn(多worker)
# uvicorn main:app --workers 4

# 方案3:超大规模(QPS > 2000)
FastAPI + Gunicorn + UvicornWorker
# gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

# 再加一层
Nginx(负载均衡)→ 多个FastAPI实例

七、性能优化建议

7.1 异步数据库驱动(评价个人:这个有点烧包/(ㄒoㄒ)/~~)

python 复制代码
# ❌ 同步驱动(会阻塞事件循环)
import pymysql

# ✅ 异步驱动
import aiomysql

async def query_db():
    # ========== 第1层:创建连接池 ==========
    # async with: 异步上下文管理器,进入/退出时会 await
    # create_pool: 创建数据库连接池(不是立即连接,而是准备好多条连接通道)
    # ... 参数: host, port, user, password, db 等
    # pool: 连接池对象,里面有多个数据库连接(比如10个)
    async with aiomysql.create_pool(host='localhost', user='root', db='test') as pool:
        
        # ========== 第2层:从池子里拿一个连接 ==========
        # pool.acquire(): 从连接池中"借"一个空闲连接
        # await: 如果没有空闲连接,就等在这里(但不会阻塞事件循环,会去干别的)
        # conn: 一个具体的数据库连接对象
        async with pool.acquire() as conn:
            
            # ========== 第3层:创建游标 ==========
            # cursor(): 创建游标,用来执行SQL语句
            # cur: 游标对象,相当于"数据库操作的手柄"
            async with conn.cursor() as cur:
                
                # ========== 第4层:执行SQL ==========
                # cur.execute("SELECT..."): 执行SQL查询
                # await: 关键!等数据库返回结果期间,事件循环去处理其他请求
                # 数据库可能耗时100ms,这100ms内能处理几千个其他请求
                await cur.execute("SELECT id, name FROM users WHERE id = 1")
                
                # ========== 第5层:获取结果 ==========
                # fetchone(): 取一条结果
                # await: 等待数据从数据库传输完成
                result = await cur.fetchone()
                
                # ========== 返回 ==========
                return result

# 使用示例
# result = await query_db()
# print(result)  # (1, '张三')
python 复制代码
"""
事件循环(单线程):
│
├── 收到请求A,执行到 await cur.execute()
│   ├── 告诉数据库:"帮我查一下"
│   ├── 把请求A挂起(存起来)
│   └── 事件循环:好,我去处理请求B
│
├── 处理请求B,执行到 await cur.execute()
│   ├── 告诉数据库:"帮我查一下"
│   ├── 把请求B挂起
│   └── 事件循环:好,我去处理请求C
│
├── 处理请求C...
│
├── 数据库返回结果A
│   └── 事件循环:唤醒请求A,执行 await cur.fetchone()
│
├── 数据库返回结果B
│   └── 事件循环:唤醒请求B
│
└── ...
"""

aiomysql 把 pymysql 的同步阻塞操作,封装成了异步非阻塞操作。

遇到 await cur.execute() 时,事件循环会"切出去"处理其他请求,等数据库返回结果了再"切回来"继续执行。

这就是 "全链路异步" ------从 Web 服务器到数据库,整个调用链都是异步的

7.2 同步代码转异步

python 复制代码
# 如果必须用同步库
import asyncio
from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=10)

async def call_sync_func():
    # 在线程池中运行同步代码,不阻塞事件循环
    result = await asyncio.get_event_loop().run_in_executor(
        executor, sync_function
    )
    return result

7.3 缓存策略

python 复制代码
from fastapi import FastAPI
import aioredis

app = FastAPI()
redis = await aioredis.create_redis_pool("redis://localhost")

@app.post("/search")
async def search(query: str):
    # 1. 检查缓存
    cached = await redis.get(f"cache:{query}")
    if cached:
        return json.loads(cached)
    
    # 2. 实际检索
    result = await do_search(query)
    
    # 3. 写入缓存
    await redis.setex(f"cache:{query}", 3600, json.dumps(result))
    
    return result

八、总结

8.1 核心概念回顾

python 复制代码
概念总结 = {
    "同步": "做一件事时,不能做其他事",
    "异步": "等待时可以做其他事",
    "多线程": "用多个工人,每个工人一次做一件事",
    "协程": "一个工人,但可以在等待时切换任务",
    "GIL": "Python的多线程限制(CPU计算不并行)",
    "await": "挂起点,告诉事件循环'我先等着,你先做别的'"
}

8.2 选型建议

场景 推荐方案 原因
内部工具、低并发 Flask 简单够用
对外API、中高并发 FastAPI 性能好、自带文档
RAG系统 FastAPI IO密集型、天然适合异步
纯计算任务 多进程 绕过GIL

8.3 学习收获

通过这次学习,应该已经理解:

Flask默认是多线程 (和文档说的不一样,验证了)

多线程有内存上限 (1w线程会内存爆炸)

FastAPI用协程实现高并发 (10w并发轻松应对)

异步适合IO密集型 (RAG是典型场景)

await让出控制权(不是等待,是切换)


附录:完整测试代码

python 复制代码
# 服务端性能测试
# 启动服务后,用客户端测试不同并发数

# Flask服务端
from flask import Flask
import time
app = Flask(__name__)

@app.route('/test')
def test():
    time.sleep(1)
    return "OK"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8002, threaded=True)

# FastAPI服务端
from fastapi import FastAPI
import asyncio
app = FastAPI()

@app.get('/test')
async def test():
    await asyncio.sleep(1)
    return {"status": "OK"}

if __name__ == '__main__':
    import uvicorn
    uvicorn.run(app, host='0.0.0.0', port=8002)

写在最后 :理解并发模型是构建高性能RAG系统的基石。Flask简单易用,FastAPI高效强大。根据你的并发需求选择合适的工具,才能在有限的硬件资源下支撑更多的用户。

相关推荐
minebmw72 小时前
Oracle 19.29 中 ORA-12751 错误完全解析:从通用问题到 minact-scn 场景
数据库·oracle
JACK的服务器笔记2 小时前
《服务器测试百日学习计划——Day19:PCIe自动检测脚本,用Python把lspci设备清点标准化》
服务器·python·学习
星晨雪海2 小时前
优惠券秒杀的核心业务逻辑
java·前端·数据库
清风6666662 小时前
基于单片机的智能门控制系统设计与故障报警实现
数据库·单片机·mongodb·毕业设计·课程设计·期末大作业
SelectDB技术团队2 小时前
AI 成为主流负载后,数据基础设施将如何演进?|Apache Doris 2026 Roadmap
数据库·人工智能·apache doris·selectdb
SPC的存折2 小时前
分布式(加一键部署脚本)LNMP-Redis-Discuz5.0部署指南-小白详细版
linux·运维·服务器·数据库·redis·分布式·缓存
YJlio2 小时前
Sysinternals实战教程专栏介绍:这不是一本到此为止的书,而是一套看穿 Windows 的排障方法
windows·python·电脑·outlook·windows部署·eixv3·pe装机
脑子加油站2 小时前
Redis数据库基础
数据库·redis·缓存
知识分享小能手2 小时前
MongoDB入门学习教程,从入门到精通,MongoDB监控完全指南(22)
数据库·学习·mongodb