前面我们介绍了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高效强大。根据你的并发需求选择合适的工具,才能在有限的硬件资源下支撑更多的用户。