【30天做一个生产级RAG知识库系统】第8篇:并发优化与缓存设计,解决多用户访问崩服务的问题

从单机单用户到千级并发商用服务,保姆级实现RAG系统全链路并发优化、多级缓存架构、限流熔断与资源隔离,附完整工程化代码、压测方案、踩坑避坑指南


前言:demo与商用服务的核心差距,90%的项目都死在并发上

前七篇我们完成了生产级RAG系统的全链路商用能力建设:

  • 第1-2篇:完成了架构设计与文档预处理,解决了数据质量问题
  • 第3-4篇:完成了向量库搭建与检索引擎优化,解决了召回准确率问题
  • 第5篇:完成了Prompt工程与LLM封装,解决了幻觉与答案质量问题
  • 第6篇:完成了标准化后端接口开发,实现了从代码到可对接服务的跨越
  • 第7篇:完成了多租户与RBAC权限体系,打通了ToB商用的核心壁垒

到这里,你的RAG系统已经具备了完整的商用功能,本地单用户测试完美运行,但一旦上线,面对十几个、几十个用户同时使用,就会出现致命问题:

几个用户同时上传大文档,服务直接卡死,所有请求都超时;

高峰时段同时发起问答,LLM调用堆积,主服务直接无响应;

向量库检索并发上来,查询耗时从毫秒级涨到秒级,甚至直接OOM崩溃;

没有任何限流保护,一次恶意刷接口就打满服务器资源,所有租户都无法使用;

频繁出现内存溢出,服务动不动就重启,用户体验极差,客户直接投诉。

这就是demo级项目生产级商用服务的核心鸿沟:功能跑通只是第一步,能稳定支撑高并发多用户访问,才是商用的底线。

很多新手有一个致命误区:照搬普通CRUD系统的并发优化方案,却忽略了RAG系统的本质特性------它是一个「CPU/GPU密集型+IO密集型双高」的系统,和普通的增删改查系统完全不同。

RAG系统的并发瓶颈,从来不是数据库的增删改查,而是这6个重负载环节:

  1. 文档处理环节:大文件解析、OCR、分块、向量化,是CPU/GPU密集型任务,单任务耗时从几秒到几十秒,极易占满服务器资源
  2. 向量检索环节:高并发下的多路召回、重排序,是计算密集型任务,慢查询会直接拖垮整个服务
  3. LLM调用环节:大模型接口响应慢(单轮请求几百ms到几秒),同步调用会直接占满Web服务的worker,导致新请求进不来
  4. 内存瓶颈:模型重复加载、大文件一次性读入内存,极易导致OOM内存溢出
  5. 资源争抢:不同租户、不同类型的任务争抢CPU/GPU/内存资源,一个租户的大文件处理,会导致所有租户的问答请求卡顿
  6. 无保护机制:没有限流、熔断、隔离,突发流量直接打满服务,引发雪崩效应

一个核心结论必须记死

RAG系统的并发优化,核心不是「让单个请求跑得更快」,而是「在高并发下,让服务稳定运行,核心请求不卡顿、不崩溃,资源可控」。所有的优化,都必须围绕「隔离、缓存、限流、异步化」这四个核心方向展开。

本篇我们就彻底啃下高并发这个硬骨头,从全链路瓶颈拆解、异步化任务隔离、多级缓存架构、连接池优化、限流熔断、资源管控全链路落地,附完整可直接复用的工程化代码,完全对齐前七篇的项目架构,跟着做完,你的系统就能稳定支撑上千用户同时在线访问。


一、先搞懂:RAG系统全链路并发瓶颈拆解

在做优化之前,我们必须先精准定位RAG系统全链路的瓶颈点,对症下药,而不是盲目优化。我们把RAG系统的核心链路拆成6个环节,逐个拆解瓶颈点、影响范围、优化优先级:

链路环节 核心瓶颈点 任务类型 影响范围 优化优先级
文档上传与处理 大文件解析、分块、向量化耗时久,占满CPU/GPU/内存 CPU/GPU密集型 全局阻塞,所有用户请求卡顿 最高
LLM问答调用 大模型接口响应慢,同步调用阻塞事件循环,worker占满 IO密集型(长耗时) 核心功能不可用,新请求无法进入 最高
向量检索与重排序 高并发下检索耗时飙升,连接数打满,重排序占满GPU CPU/GPU密集型 问答响应变慢,超时率飙升
权限与元数据查询 每次请求都查数据库校验权限、获取会话/文档信息 IO密集型(短耗时) 接口响应变慢,数据库连接打满
大文件上传 大文件一次性读入内存,导致OOM,带宽占满 IO密集型 服务崩溃,其他用户无法上传
日志与审计 同步写日志/审计数据阻塞主流程 IO密集型(短耗时) 接口响应抖动,低

优化的核心原则(必须严格遵守)

  1. 隔离优先原则:把不同类型、不同耗时的任务彻底隔离,慢任务绝对不能阻塞快任务,CPU密集型和IO密集型任务必须分开,不同租户的资源必须隔离。
  2. 缓存为王原则:能缓存的绝对不重复计算,尤其是重复检索、重复LLM调用、重复权限查询,缓存是提升并发能力成本最低、效果最好的手段。
  3. 异步化原则:所有耗时超过100ms的非实时任务,全部异步化处理,绝对不能阻塞主服务的事件循环。
  4. 保护机制原则:必须有限流、熔断、降级机制,哪怕服务降级,也绝对不能完全崩溃,保障核心功能可用。
  5. 资源可控原则:所有耗资源的操作,必须有上限控制,比如批量向量化的批次大小、并发文档处理数、单用户并发请求数,绝对不能放任资源无限制占用。

二、生产级高并发RAG系统整体架构设计

下面是完全对齐前七篇架构的高并发优化架构图,用Mermaid代码实现,你直接复制就能渲染,每个模块对应后面的代码实现:
客户端请求
接入层
全局限流

IP/租户/用户级
HTTPS/负载均衡
请求日志全链路追踪
FastAPI Web服务层
事件循环优化
权限校验缓存
接口分级限流
全局异常与熔断保护
实时请求链路

问答/查询类快任务
异步任务队列

文档处理/批量操作类慢任务
多级缓存体系
本地内存缓存

静态配置/高频权限
Redis分布式缓存

问答结果/检索结果/会话上下文
检索引擎

多路召回+重排序
向量库连接池优化
检索结果缓存
LLM调用引擎
HTTP连接池复用
异步非阻塞调用
熔断降级保护
Celery分布式任务队列
文档解析/分块任务
向量化任务

GPU资源隔离
批量操作/离线任务
任务状态持久化

重启不丢失
Worker横向扩展
关系型数据库PostgreSQL
连接池优化
向量数据库Milvus
连接池与索引优化
Redis缓存/消息中间件
连接池优化
GPU资源池
向量化/重排序任务隔离
监控告警体系
接口耗时/错误率监控
资源使用率监控
队列积压/熔断告警
多租户资源管控
租户并发数/配额限制
租户资源隔离

架构核心亮点

  1. 快慢任务彻底隔离:问答等实时快任务,和文档处理等慢任务完全分离,慢任务走独立的分布式任务队列,绝对不会阻塞主服务。
  2. 多级缓存全覆盖:从本地内存到分布式Redis,覆盖权限、检索、问答、会话全场景,把重复计算降到最低。
  3. 全链路保护机制:从接入层到业务层,全链路限流、熔断、降级,哪怕底层依赖出问题,主服务也不会崩溃。
  4. 可横向扩展:所有耗资源的模块都支持分布式横向扩展,比如文档处理、向量化任务,加机器就能提升并发能力。
  5. 多租户资源隔离:从接入层就做租户级限流和资源管控,避免单个租户的流量影响整个服务的稳定性。

三、核心优化一:异步化与任务隔离,从根源解决阻塞问题

FastAPI是基于Asyncio的异步Web框架,它的高性能核心在于事件循环不被阻塞。但90%的新手都会犯一个致命错误:在异步接口里调用耗时的同步方法,直接阻塞事件循环,导致整个服务的并发能力暴跌。

比如文档处理、向量化、LLM同步调用,这些耗时几秒甚至几十秒的同步操作,一旦在异步接口里执行,整个事件循环就会被卡住,所有新请求都进不来,这就是服务一卡全卡的根源。

3.1 核心方案:线程池隔离+分布式异步任务队列

我们分两层解决这个问题:

  1. 短耗时同步操作 :比如数据库查询、短耗时LLM调用,用独立线程池隔离执行,不阻塞主事件循环。
  2. 长耗时离线操作 :比如文档处理、批量向量化,用Celery分布式任务队列处理,彻底和主服务解耦,主服务只负责接收请求和返回任务状态,不等待任务执行完成。

3.2 工程化实现1:线程池隔离,适配不同类型任务

首先,我们实现自定义线程池,把CPU密集型和IO密集型任务分开,用不同的线程池配置,避免慢任务拖垮快任务。

新建app/core/thread_pool.py

python 复制代码
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Callable, Any, TypeVar
from functools import wraps
from loguru import logger

# 泛型类型,用于类型提示
T = TypeVar("T")

class ThreadPoolManager:
    """线程池管理器,分离IO密集型和CPU密集型任务,避免阻塞事件循环"""
    def __init__(self):
        # IO密集型任务线程池:比如LLM调用、数据库查询、HTTP请求,线程数可以设大一点
        self.io_executor = ThreadPoolExecutor(
            max_workers=50,
            thread_name_prefix="io-task-pool"
        )
        # CPU/GPU密集型任务线程池:比如向量化、分块、重排序,线程数和CPU核心数匹配,避免资源争抢
        import multiprocessing
        cpu_core_count = multiprocessing.cpu_count()
        self.cpu_executor = ThreadPoolExecutor(
            max_workers=min(cpu_core_count, 8),  # 最多8个,避免GPU/CPU占满
            thread_name_prefix="cpu-task-pool"
        )
        logger.info(f"线程池初始化完成:IO线程数=50,CPU线程数={self.cpu_executor._max_workers}")

    async def run_io_task(self, func: Callable[..., T], *args, **kwargs) -> T:
        """在IO线程池中执行同步函数,异步返回结果"""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(self.io_executor, func, *args, **kwargs)

    async def run_cpu_task(self, func: Callable[..., T], *args, **kwargs) -> T:
        """在CPU线程池中执行同步函数,异步返回结果"""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(self.cpu_executor, func, *args, **kwargs)

    def shutdown(self):
        """服务关闭时关闭线程池"""
        self.io_executor.shutdown(wait=True)
        self.cpu_executor.shutdown(wait=True)
        logger.info("线程池已关闭")

# 全局单例
_thread_pool_manager = None

def get_thread_pool() -> ThreadPoolManager:
    global _thread_pool_manager
    if _thread_pool_manager is None:
        _thread_pool_manager = ThreadPoolManager()
    return _thread_pool_manager

# 装饰器:自动把同步函数放到IO线程池执行
def io_task(func: Callable[..., T]) -> Callable[..., asyncio.Future[T]]:
    @wraps(func)
    async def wrapper(*args, **kwargs):
        return await get_thread_pool().run_io_task(func, *args, **kwargs)
    return wrapper

# 装饰器:自动把同步函数放到CPU线程池执行
def cpu_task(func: Callable[..., T]) -> Callable[..., asyncio.Future[T]]:
    @wraps(func)
    async def wrapper(*args, **kwargs):
        return await get_thread_pool().run_cpu_task(func, *args, **kwargs)
    return wrapper
核心使用示例:改造之前的同步方法

比如之前的问答服务、检索服务的同步方法,都可以用装饰器改成异步非阻塞,不会阻塞事件循环:

python 复制代码
# 改造前:同步方法,会阻塞事件循环
def chat(self, query: str, tenant_id: str):
    # 检索+LLM调用,耗时操作
    return result

# 改造后:异步非阻塞,放到IO线程池执行
@io_task
def chat(self, query: str, tenant_id: str):
    # 检索+LLM调用,耗时操作
    return result

# 向量化方法,CPU/GPU密集型,放到CPU线程池
@cpu_task
def encode(self, texts: List[str]):
    # 批量向量化,GPU密集型操作
    return embeddings

3.3 工程化实现2:Celery分布式任务队列,处理长耗时任务

对于文档处理这种耗时几十秒的长任务,FastAPI的BackgroundTasks完全不够用------服务重启任务就会丢失,无法分布式扩展,多个worker之间无法共享任务,生产级绝对不能用。

我们用Celery+Redis实现分布式任务队列,核心优势:

  • 任务持久化,服务重启不丢失,支持任务重试、失败回调
  • 支持分布式横向扩展,加机器就能提升任务处理能力
  • 支持任务优先级、队列隔离,核心任务和普通任务分开
  • 支持任务状态查询,前端可以实时展示处理进度
第一步:补充依赖到requirements.txt
txt 复制代码
# 本篇新增:分布式任务队列
celery>=5.4.0
redis>=5.0.0
第二步:Celery配置与初始化

新建app/core/celery_app.py

python 复制代码
import os
from celery import Celery
from celery.signals import worker_process_init, worker_shutdown
from loguru import logger

# 从环境变量获取Redis地址
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")

# 初始化Celery应用
celery_app = Celery(
    "rag-tasks",
    broker=REDIS_URL,
    backend=REDIS_URL,
    include=[
        "app.tasks.document_tasks",
        "app.tasks.common_tasks"
    ]
)

# Celery配置
celery_app.conf.update(
    # 任务序列化方式
    task_serializer="json",
    result_serializer="json",
    accept_content=["json"],
    # 任务超时时间:文档处理最多10分钟
    task_time_limit=600,
    task_soft_time_limit=540,
    # 任务重试次数
    task_max_retries=3,
    # 结果过期时间:24小时
    result_expires=86400,
    #  worker预取任务数,避免慢worker堆积任务
    worker_prefetch_multiplier=1,
    #  worker最大任务数,执行一定数量任务后重启,避免内存泄漏
    worker_max_tasks_per_child=100,
    # 队列配置:分离不同类型的任务
    task_routes={
        "app.tasks.document_tasks.*": {"queue": "document_process"},
        "app.tasks.common_tasks.*": {"queue": "default"},
    }
)

# worker进程初始化时,加载模型,避免每个任务都加载模型
@worker_process_init.connect
def init_worker_process(**kwargs):
    """worker进程启动时,初始化全局模型和服务,避免重复加载"""
    logger.info("Worker进程启动,初始化核心服务...")
    # 预加载Embedding模型、文档处理服务,避免每个任务都加载
    from app.service.document_service import document_process_service
    from app.core.embedding.embedding_engine import get_embedding_engine
    get_embedding_engine()
    logger.info("Worker进程初始化完成,核心服务加载成功")

# worker关闭时清理资源
@worker_shutdown.connect
def shutdown_worker_process(**kwargs):
    """worker进程关闭时,清理资源"""
    logger.info("Worker进程关闭,清理资源...")
    from app.core.embedding.vector_db import get_vector_db_manager
    get_vector_db_manager().close()
    logger.info("Worker进程资源清理完成")
第三步:文档处理任务实现

新建app/tasks/document_tasks.py,实现文档处理的异步任务,和之前的document_service无缝衔接:

python 复制代码
from celery import Task, shared_task
from loguru import logger
from sqlalchemy.orm import Session

from app.core.celery_app import celery_app
from app.db.relational_db import SessionLocal
from app.service.document_service import document_process_service
from app.models.document import Document
from app.service.tenant_service import tenant_service

class BaseDatabaseTask(Task):
    """基础任务类,自动管理数据库会话"""
    _db = None

    @property
    def db(self) -> Session:
        if self._db is None:
            self._db = SessionLocal()
        return self._db

    def after_return(self, *args, **kwargs):
        """任务结束后关闭数据库会话"""
        if self._db is not None:
            self._db.close()
            self._db = None

@celery_app.task(
    bind=True,
    base=BaseDatabaseTask,
    name="document_process.process_document",
    max_retries=2,
    queue="document_process"
)
def process_document_task(self, document_id: str, tenant_id: int):
    """
    文档处理异步任务:解析→清洗→分块→向量化→存入向量库
    :param document_id: 文档唯一ID
    :param tenant_id: 所属租户ID
    """
    logger.info(f"开始处理文档:document_id={document_id}, tenant_id={tenant_id}")
    db = self.db
    try:
        # 1. 查询文档信息
        document = db.query(Document).filter(
            Document.document_id == document_id,
            Document.tenant_id == tenant_id
        ).first()
        if not document:
            logger.error(f"文档不存在:document_id={document_id}")
            raise Exception("文档不存在")

        # 2. 校验租户配额
        quota_ok = tenant_service.check_quota(db, tenant_id, "document")
        if not quota_ok:
            document.status = 2
            document.failed_reason = "租户文档数量配额已超限"
            db.commit()
            raise Exception("租户文档数量配额已超限")

        # 3. 执行文档全流程处理
        chunk_count = document_process_service.process_document(
            file_path=document.file_path,
            tenant_id=str(tenant_id),
            upload_user_id=document.created_by
        )

        # 4. 更新文档状态
        document.status = 1
        document.chunk_count = chunk_count
        # 更新租户已用配额
        tenant_service.update_quota_usage(db, tenant_id, "document", 1)
        tenant_service.update_quota_usage(db, tenant_id, "storage", document.file_size)
        db.commit()

        logger.info(f"文档处理完成:document_id={document_id}, 分块数量={chunk_count}")
        return {
            "document_id": document_id,
            "tenant_id": tenant_id,
            "status": "success",
            "chunk_count": chunk_count
        }

    except Exception as e:
        logger.error(f"文档处理失败:document_id={document_id}, 错误信息={str(e)}", exc_info=True)
        # 更新文档状态为处理失败
        try:
            document = db.query(Document).filter(Document.document_id == document_id).first()
            if document:
                document.status = 2
                document.failed_reason = str(e)
                db.commit()
        except:
            pass
        # 重试任务
        raise self.retry(exc=e, countdown=60)

@celery_app.task(
    bind=True,
    base=BaseDatabaseTask,
    name="document_process.delete_document",
    queue="document_process"
)
def delete_document_task(self, document_id: str, tenant_id: int):
    """文档删除异步任务,清理向量库、关键词索引、本地文件"""
    logger.info(f"开始删除文档:document_id={document_id}, tenant_id={tenant_id}")
    db = self.db
    try:
        document = db.query(Document).filter(
            Document.document_id == document_id,
            Document.tenant_id == tenant_id
        ).first()
        if not document:
            raise Exception("文档不存在")

        # 执行删除
        document_process_service.delete_document(document_id, str(tenant_id), db)
        # 更新租户配额
        tenant_service.update_quota_usage(db, tenant_id, "document", -1)
        tenant_service.update_quota_usage(db, tenant_id, "storage", -document.file_size)

        logger.info(f"文档删除完成:document_id={document_id}")
        return {
            "document_id": document_id,
            "tenant_id": tenant_id,
            "status": "success"
        }
    except Exception as e:
        logger.error(f"文档删除失败:document_id={document_id}, 错误信息={str(e)}", exc_info=True)
        raise self.retry(exc=e, countdown=30)
第四步:改造文档上传接口,用异步任务处理

修改app/api/v1/document.py的上传接口,不再同步处理文档,而是把任务丢给Celery,立即返回,不阻塞主服务:

python 复制代码
@document_router.post("/upload", summary="上传文档", description="上传文档,后台异步处理", dependencies=[Depends(require_permission("document:upload"))])
async def upload_document(
    background_tasks: BackgroundTasks,
    file: UploadFile = File(..., description="上传的文档文件"),
    current_user: CurrentUser = Depends(),
    db: DBSession = Depends()
):
    # 1. 保存文件,创建元数据
    document = await document_process_service.save_upload_file(
        file=file,
        tenant_id=current_user.tenant_id,
        user_id=current_user.id,
        db=db
    )
    # 2. 提交异步任务到Celery,不再用BackgroundTasks
    from app.tasks.document_tasks import process_document_task
    process_document_task.delay(document.document_id, current_user.tenant_id)
    # 3. 立即返回,无需等待处理完成
    return ResponseModel.success(
        data={
            "document_id": document.document_id,
            "file_name": document.file_name,
            "status": document.status,
            "task_id": process_document_task.request.id,
            "message": "文档上传成功,正在后台处理中,请稍后刷新查看状态"
        },
        message="文档上传成功"
    )

# 新增:查询任务状态接口
@document_router.get("/task/{task_id}", summary="查询文档处理任务状态", description="查询异步任务的处理进度")
async def get_document_task_status(task_id: str):
    from app.core.celery_app import celery_app
    task = celery_app.AsyncResult(task_id)
    result = {
        "task_id": task_id,
        "status": task.status,
        "result": task.result if task.successful() else None,
        "failed_reason": str(task.info) if task.failed() else None
    }
    return ResponseModel.success(data=result)
第五步:Celery Worker启动命令
bash 复制代码
# 启动默认队列worker
celery -A app.core.celery_app worker --loglevel=info -Q default -c 4

# 启动文档处理队列worker,单独隔离,避免影响其他任务
celery -A app.core.celery_app worker --loglevel=info -Q document_process -c 2

核心优势

  • 文档处理任务彻底和主服务隔离,哪怕处理任务崩溃,主服务也完全不受影响
  • 支持分布式扩展,文档处理量大的时候,单独给document_process队列加worker机器即可
  • 任务持久化,服务重启、机器重启,任务都不会丢失,会自动重试
  • 支持任务状态查询,前端可以实时展示处理进度,用户体验更好

四、核心优化二:多级缓存架构,把并发能力提升100倍

缓存是提升并发能力成本最低、效果最好的手段。对于RAG系统,80%的请求都是重复的:用户会反复问相同的问题,反复查询相同的文档列表,每次请求都重复做检索、重复调用LLM,不仅浪费资源,还极大限制了并发能力。

我们设计三级缓存架构,覆盖RAG系统全场景,从高频静态数据到动态问答结果,全部做缓存,把重复计算降到最低。

4.1 三级缓存架构设计

缓存层级 缓存介质 缓存内容 响应时间 过期策略
一级缓存 本地内存 系统静态配置、租户基础信息、权限配置、预置角色/权限 微秒级 服务重启刷新,主动更新
二级缓存 Redis分布式缓存 高频问答结果、检索结果、会话上下文、用户权限、文档元数据 毫秒级 过期时间+主动更新,避免脏数据
三级缓存 本地磁盘/向量库 文档分块结果、向量化结果 毫秒级 文档更新时主动删除

4.2 工程化实现:Redis缓存工具类与装饰器

首先,实现Redis缓存工具类,统一管理缓存的读写、过期、删除,新建app/core/cache.py

python 复制代码
import os
import json
from typing import Any, Callable, TypeVar
from functools import wraps
from datetime import timedelta
from redis import asyncio as aioredis
from loguru import logger

# 泛型类型
T = TypeVar("T")

# Redis连接配置
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")

class CacheManager:
    """Redis缓存管理器,异步实现,不阻塞事件循环"""
    def __init__(self):
        self.redis = None
        self.default_expire = 3600  # 默认过期时间1小时

    async def init_redis(self):
        """初始化Redis连接,服务启动时调用"""
        try:
            self.redis = await aioredis.from_url(
                REDIS_URL,
                encoding="utf-8",
                decode_responses=True,
                max_connections=100,  # 连接池大小
                socket_connect_timeout=5,
                socket_timeout=5
            )
            # 测试连接
            await self.redis.ping()
            logger.info("Redis缓存初始化成功")
        except Exception as e:
            logger.error(f"Redis初始化失败:{str(e)}", exc_info=True)
            raise e

    async def close(self):
        """服务关闭时关闭Redis连接"""
        if self.redis:
            await self.redis.close()
            logger.info("Redis连接已关闭")

    async def get(self, key: str) -> Any:
        """获取缓存"""
        if not self.redis:
            return None
        try:
            data = await self.redis.get(key)
            if data:
                return json.loads(data)
            return None
        except Exception as e:
            logger.error(f"缓存读取失败:key={key}, 错误={str(e)}")
            return None

    async def set(self, key: str, value: Any, expire: int = None):
        """设置缓存"""
        if not self.redis:
            return
        try:
            expire = expire or self.default_expire
            await self.redis.set(
                key,
                json.dumps(value, ensure_ascii=False),
                ex=expire
            )
        except Exception as e:
            logger.error(f"缓存写入失败:key={key}, 错误={str(e)}")

    async def delete(self, key: str):
        """删除缓存"""
        if not self.redis:
            return
        try:
            await self.redis.delete(key)
        except Exception as e:
            logger.error(f"缓存删除失败:key={key}, 错误={str(e)}")

    async def delete_by_prefix(self, prefix: str):
        """按前缀批量删除缓存,用于更新时批量清理脏数据"""
        if not self.redis:
            return
        try:
            keys = await self.redis.keys(f"{prefix}*")
            if keys:
                await self.redis.delete(*keys)
        except Exception as e:
            logger.error(f"批量删除缓存失败:prefix={prefix}, 错误={str(e)}")

    async def exists(self, key: str) -> bool:
        """判断缓存是否存在"""
        if not self.redis:
            return False
        return await self.redis.exists(key) > 0

# 全局单例
_cache_manager = None

def get_cache_manager() -> CacheManager:
    global _cache_manager
    if _cache_manager is None:
        _cache_manager = CacheManager()
    return _cache_manager

# 缓存装饰器:自动缓存函数返回结果
def cacheable(key_prefix: str, expire: int = 3600, key_builder: Callable = None):
    """
    缓存装饰器,异步函数专用
    :param key_prefix: 缓存key前缀
    :param expire: 过期时间,秒
    :param key_builder: 自定义key生成函数,参数为函数的args和kwargs
    """
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        async def wrapper(*args, **kwargs):
            cache_manager = get_cache_manager()
            # 生成缓存key
            if key_builder:
                cache_key = key_builder(*args, **kwargs)
            else:
                # 默认key:前缀+函数名+参数哈希
                args_str = "_".join([str(arg) for arg in args[1:]])  # 跳过self
                kwargs_str = "_".join([f"{k}_{v}" for k, v in sorted(kwargs.items())])
                cache_key = f"{key_prefix}:{func.__name__}:{args_str}:{kwargs_str}"
            
            # 先查缓存
            cached_data = await cache_manager.get(cache_key)
            if cached_data is not None:
                logger.debug(f"缓存命中:{cache_key}")
                return cached_data
            
            # 缓存未命中,执行函数
            result = await func(*args, **kwargs)
            # 写入缓存
            await cache_manager.set(cache_key, result, expire)
            return result
        return wrapper
    return decorator

4.3 核心场景缓存落地

我们针对RAG系统最高频、最耗资源的4个场景,落地缓存,效果立竿见影。

场景1:高频问答结果缓存(效果最明显)

相同的用户问题,不需要重复检索、重复调用LLM,直接缓存答案,响应时间从几秒降到几毫秒,并发能力提升100倍以上。

修改app/service/chat_service.py,给chat方法加上缓存:

python 复制代码
from app.core.cache import cacheable, get_cache_manager

class ChatService:
    # 问答结果缓存:key前缀+租户ID+问题哈希,过期时间1小时
    # 注意:相同问题,不同租户的缓存是隔离的
    @cacheable(key_prefix="rag:chat:answer", expire=3600, key_builder=lambda self, query, tenant_id, **kwargs: f"rag:chat:{tenant_id}:{hash(query)}")
    async def chat(
        self,
        user_query: str,
        tenant_id: str = "default",
        document_ids: List[str] = None,
        chat_history: List[Dict[str, str]] = None,
        enable_citation: bool = True
    ) -> Dict[str, Any]:
        # 原有逻辑不变,注意要把同步方法改成异步,用线程池隔离
        # ...

缓存更新策略:当租户上传/修改/删除文档时,自动清空该租户的所有问答缓存,避免脏数据:

python 复制代码
# 在文档删除/修改成功后,调用:
await get_cache_manager().delete_by_prefix(f"rag:chat:{tenant_id}:")
场景2:检索结果缓存

相同的Query,不需要重复查询向量库、重复做多路召回和重排序,缓存检索结果,提升问答速度,降低向量库压力。

修改app/core/retrieval/retrieval_engine.py,给search方法加上缓存:

python 复制代码
from app.core.cache import cacheable

class RetrievalEngine:
    @cacheable(key_prefix="rag:retrieval:result", expire=1800, key_builder=lambda self, query, tenant_id, **kwargs: f"rag:retrieval:{tenant_id}:{hash(query)}")
    async def search(
        self,
        query: str,
        tenant_id: str = "default",
        document_ids: List[str] = None,
        use_llm_rewrite: bool = False,
        split_complex: bool = False,
        recall_top_k: int = 50,
        final_top_k: int = 5
    ) -> Dict[str, Any]:
        # 原有逻辑不变,改成异步方法
        # ...
场景3:权限与租户信息缓存

每次接口请求都要查数据库校验权限、获取租户信息,高并发下会给数据库带来极大压力,缓存这些静态数据,降低数据库压力。

修改app/api/deps.py的get_current_user方法,加上缓存:

python 复制代码
from app.core.cache import get_cache_manager

async def get_current_user(
    request: Request,
    credentials: TokenCredentials,
    db: DBSession
) -> User:
    # 原有Token校验逻辑不变
    # ...
    user_id = payload.get("user_id")
    tenant_id = payload.get("tenant_id")
    
    # 先查缓存
    cache_manager = get_cache_manager()
    cache_key = f"sys:user:{tenant_id}:{user_id}"
    cached_user = await cache_manager.get(cache_key)
    if cached_user:
        # 把缓存数据转换成User对象
        user = User(**cached_user)
        return user
    
    # 缓存未命中,查数据库
    user = db.query(User).filter(User.id == user_id, User.tenant_id == tenant_id, User.status == 1).first()
    if not user:
        raise BusinessException(code=401, message="用户不存在或已被禁用")
    
    # 写入缓存,过期时间30分钟
    await cache_manager.set(cache_key, {
        "id": user.id,
        "username": user.username,
        "nickname": user.nickname,
        "email": user.email,
        "phone": user.phone,
        "tenant_id": user.tenant_id,
        "is_admin": user.is_admin,
        "is_super_admin": user.is_super_admin,
        "status": user.status
    }, expire=1800)
    
    # 初始化租户上下文
    set_tenant_context(TenantContext(
        tenant_id=tenant_id,
        tenant_code=payload.get("tenant_code", "default"),
        is_super_admin=user.is_super_admin
    ))
    
    @request.add_finalizer
    def clear_context():
        clear_tenant_context()
    
    return user
场景4:会话上下文缓存

多轮对话中,每次都要查数据库获取对话历史,缓存会话上下文,降低数据库压力,提升多轮对话速度。

修改app/service/chat_service.py的get_chat_history方法,加上缓存:

python 复制代码
@cacheable(key_prefix="rag:chat:history", expire=600, key_builder=lambda self, session_id, tenant_id, **kwargs: f"rag:chat:history:{tenant_id}:{session_id}")
async def get_chat_history(self, session_id: str, tenant_id: int, db: Session) -> List[Dict[str, str]]:
    messages = db.query(ChatMessage).filter(
        ChatMessage.session_id == session_id,
        ChatMessage.tenant_id == tenant_id,
        ChatMessage.is_deleted == False
    ).order_by(ChatMessage.created_at.asc()).all()
    return [{"role": msg.role, "content": msg.content} for msg in messages]

4.4 缓存最佳实践与防坑

  1. 租户隔离:所有缓存key必须带上tenant_id,绝对不能出现不同租户共享缓存的情况,避免数据泄露。
  2. 过期时间合理设置:静态数据(权限、角色)过期时间长一点(30分钟-1小时),动态数据(问答结果、检索结果)短一点(30分钟-1小时),会话上下文最短(10分钟)。
  3. 主动更新机制:当数据发生变化时,主动删除对应的缓存,绝对不能等缓存过期,避免脏数据。比如文档更新后,立即删除该租户的问答和检索缓存。
  4. 缓存穿透防护:对于不存在的查询结果,也要缓存空值,过期时间短一点(比如5分钟),避免恶意请求穿透缓存打满数据库。
  5. 缓存雪崩防护:缓存过期时间加随机值,避免大量缓存同时过期,导致数据库压力突增。

五、核心优化三:连接池与资源池优化,解决高并发连接瓶颈

高并发下,很多时候服务卡顿不是因为业务逻辑慢,而是连接池配置错误,导致频繁创建销毁连接、连接数打满、请求排队等待。

RAG系统有4个核心的连接池,必须针对性优化,我们逐个拆解,给生产级最优配置。

5.1 PostgreSQL数据库连接池优化

更新app/db/relational_db.py,优化连接池配置:

python 复制代码
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from loguru import logger
from app.config.settings import settings

SQLALCHEMY_DATABASE_URL = f"postgresql://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}"

# 生产级最优连接池配置
engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    pool_pre_ping=True,  # 每次获取连接前,先ping一下,确保连接有效
    pool_recycle=3600,   # 连接1小时后自动回收,避免失效连接
    pool_size=20,         # 核心连接数,和服务器CPU核心数匹配,一般是CPU核心数*2
    max_overflow=30,      # 最大溢出连接数,高峰时最多额外创建30个连接
    pool_timeout=30,      # 获取连接的超时时间,30秒还没拿到连接就报错
    echo=False,           # 生产环境关闭SQL打印,避免日志爆炸
    connect_args={
        "connect_timeout": 10  # 数据库连接超时时间
    }
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db() -> Session:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

5.2 Milvus向量库连接池优化

更新app/core/embedding/vector_db.py,优化Milvus连接配置,避免频繁创建连接:

python 复制代码
from pymilvus import connections, Collection
from app.config.settings import settings

class VectorDBManager:
    def __init__(self, host: str = None, port: int = None, collection_name: str = None):
        self.host = host or settings.MILVUS_HOST
        self.port = port or settings.MILVUS_PORT
        self.collection_name = collection_name or "rag_document_chunks"
        self.collection = None
        self.dimension = settings.EMBEDDING_DIMENSION
        # 连接池配置
        self._connect()

    def _connect(self):
        """连接Milvus,使用长连接,保持连接复用"""
        try:
            # 检查是否已连接
            if not connections.has_connection("default"):
                connections.connect(
                    alias="default",
                    host=self.host,
                    port=self.port,
                    keepalive=True,  # 开启TCP keepalive,保持长连接
                    keepalive_idle=30,  # 空闲30秒发送keepalive包
                    keepalive_interval=10,  # keepalive包间隔10秒
                    keepalive_count=3  # 3次失败断开连接
                )
                logger.info(f"Milvus连接成功:{self.host}:{self.port}")
        except Exception as e:
            logger.error(f"Milvus连接失败:{str(e)}")
            raise Exception(f"Milvus连接失败:{str(e)}")

5.3 Redis连接池优化

我们在前面的CacheManager里已经做了连接池配置,核心参数:

  • max_connections=100:最大连接数100,足够支撑上千并发
  • socket_timeout=5:超时时间5秒,避免请求卡死

5.4 LLM调用HTTP连接池优化

大模型API调用是高频HTTP请求,每次请求都新建TCP连接会极大降低性能,我们用复用的HTTP连接池,提升LLM调用速度,降低耗时。

更新app/core/llm/llm_engine.py,优化OpenAI客户端的连接池:

python 复制代码
from httpx import Client, AsyncClient, Limits

class OpenAICompatibleEngine(BaseLLMEngine):
    def __init__(self, model_name: str, api_key: str, base_url: str = None):
        super().__init__(model_name, api_key, base_url)
        # 配置HTTP连接池,最大100个并发连接,复用连接
        limits = Limits(
            max_connections=100,
            max_keepalive_connections=20,
            keepalive_expiry=300
        )
        # 同步客户端,配置连接池
        self.client = OpenAI(
            api_key=self.api_key,
            base_url=self.base_url,
            timeout=self.default_timeout,
            max_retries=self.default_max_retries,
            http_client=Client(limits=limits, timeout=self.default_timeout)
        )
        # 异步客户端,配置连接池
        self.async_client = AsyncOpenAI(
            api_key=self.api_key,
            base_url=self.base_url,
            timeout=self.default_timeout,
            max_retries=self.default_max_retries,
            http_client=AsyncClient(limits=limits, timeout=self.default_timeout)
        )
        logger.info(f"OpenAI兼容引擎初始化完成,Base URL:{base_url}")

六、核心优化四:全链路限流、熔断与降级,服务的保命机制

哪怕你做了再多的优化,也总会遇到突发流量、依赖服务故障的情况,比如大模型接口超时、向量库响应慢、恶意用户刷接口。如果没有保护机制,这些异常会直接导致请求堆积,拖垮整个服务,引发雪崩效应。

生产级服务必须有限流、熔断、降级三大保命机制,哪怕服务降级,也绝对不能完全崩溃,保障核心功能可用。

6.1 全链路限流:防止流量打满服务

我们用SlowAPI+Redis实现分布式限流,支持IP级、用户级、租户级、接口级的多级限流,从接入层就拦截异常流量。

第一步:扩展限流配置,更新app/main.py
python 复制代码
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.storage import RedisStorage
from redis import asyncio as aioredis

# 用Redis做分布式限流存储,支持多实例部署
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
limiter = Limiter(
    key_func=get_remote_address,
    storage=RedisStorage(redis_url=REDIS_URL),
    enabled=os.getenv("ENV", "dev") != "dev"  # 开发环境关闭限流
)
第二步:多级限流实现

我们实现4个层级的限流,层层防护:

  1. 全局限流:每个IP每分钟最多100次请求,防止CC攻击
  2. 租户级限流:每个租户每分钟最多1000次请求,防止单个租户占满所有资源
  3. 用户级限流:每个用户每秒最多1次问答请求,防止恶意刷接口
  4. 接口级限流:文档上传接口每个用户每分钟最多10次,防止大量上传占满资源

示例:给问答接口加上用户级限流,修改app/api/v1/chat.py

python 复制代码
from app.main import limiter
from fastapi import Request

@chat_router.post("/sync", summary="同步问答接口", dependencies=[Depends(require_permission("chat:query"))])
@limiter.limit("60/minute", key_func=lambda request: request.state.current_user.id)  # 每分钟最多60次
async def chat_sync(
    request: Request,  # 必须加request参数,limiter需要
    data: ChatRequest,
    current_user: CurrentUser = Depends(),
    db: DBSession = Depends()
):
    # 把当前用户存入request.state,供limiter使用
    request.state.current_user = current_user
    # 原有逻辑不变
    # ...
租户级限流实现,用依赖项实现:

新建app/api/deps.py,新增租户限流依赖项:

python 复制代码
def tenant_rate_limit(limit: str = "1000/minute"):
    """租户级限流依赖项"""
    async def dependency(
        request: Request,
        current_user: CurrentUser
    ):
        request.state.tenant_id = current_user.tenant_id
        return await limiter.limit(limit, key_func=lambda request: request.state.tenant_id)(request)
    return dependency

6.2 熔断降级:依赖故障时保护服务

当大模型接口、向量库等依赖服务出现故障时,比如超时、报错率飙升,我们需要触发熔断,停止调用故障服务,直接返回兜底话术,避免请求堆积,拖垮整个服务。

我们用pybreaker实现熔断器,先补充依赖:

txt 复制代码
# 新增依赖
pybreaker>=1.0.0

新建app/core/circuit_breaker.py,实现熔断器:

python 复制代码
import pybreaker
from loguru import logger

# 定义熔断器:失败率超过50%,触发熔断,熔断时间30秒
# 熔断期间,直接返回降级结果,不调用故障服务
llm_circuit_breaker = pybreaker.CircuitBreaker(
    fail_max=5,  # 连续5次失败触发熔断
    reset_timeout=30,  # 30秒后进入半开状态,尝试恢复
    on_failure=lambda exc: logger.error(f"LLM调用失败,熔断器计数+1,错误:{str(exc)}"),
    on_open=lambda: logger.warning("LLM熔断器已打开,停止调用LLM服务,启用降级策略"),
    on_close=lambda: logger.info("LLM熔断器已关闭,恢复正常调用"),
    on_half_open=lambda: logger.info("LLM熔断器进入半开状态,尝试恢复服务")
)

# 向量库熔断器
vector_circuit_breaker = pybreaker.CircuitBreaker(
    fail_max=10,
    reset_timeout=30,
    on_failure=lambda exc: logger.error(f"向量库调用失败,熔断器计数+1,错误:{str(exc)}"),
    on_open=lambda: logger.warning("向量库熔断器已打开,启用降级策略"),
    on_close=lambda: logger.info("向量库熔断器已关闭,恢复正常调用")
)
熔断器使用示例:LLM调用加熔断保护

修改app/core/llm/llm_engine.py的chat方法,加上熔断器:

python 复制代码
from app.core.circuit_breaker import llm_circuit_breaker

class OpenAICompatibleEngine(BaseLLMEngine):
    def chat(self, messages: List[Dict[str, str]], **kwargs) -> Dict[str, Any]:
        try:
            # 用熔断器包裹LLM调用
            @llm_circuit_breaker
            def _call_llm():
                params = self._build_params(**kwargs, stream=False)
                prompt_tokens = sum(self.count_tokens(msg["content"]) for msg in messages)
                start_time = time.time()
                response = self.client.chat.completions.create(
                    messages=messages,
                    **params
                )
                end_time = time.time()
                completion_tokens = self.count_tokens(response.choices[0].message.content)
                total_tokens = prompt_tokens + completion_tokens
                return {
                    "content": response.choices[0].message.content,
                    "finish_reason": response.choices[0].finish_reason,
                    "usage": {
                        "prompt_tokens": prompt_tokens,
                        "completion_tokens": completion_tokens,
                        "total_tokens": total_tokens
                    },
                    "latency": round(end_time - start_time, 3)
                }
            return _call_llm()
        except pybreaker.CircuitBreakerError:
            # 熔断器打开,返回降级结果
            logger.error("LLM服务熔断,返回降级结果")
            raise BusinessException(code=503, message="当前问答服务繁忙,请稍后再试")
        except Exception as e:
            logger.error(f"LLM调用失败:{str(e)}")
            raise Exception(f"大模型调用失败:{str(e)}")
降级策略设计

我们设计3个层级的降级策略,保障核心功能可用:

  1. 一级降级:LLM服务熔断时,关闭流式输出,切换到备用大模型接口,保障问答功能可用
  2. 二级降级:所有大模型接口都故障时,只返回检索到的参考内容,告诉用户"当前生成服务繁忙,为您找到相关参考内容"
  3. 三级降级:向量库故障时,关闭问答功能,只保留文档上传、管理功能,告诉用户"当前检索服务维护中,请稍后再试"

6.3 多租户资源隔离:避免单个租户影响全局

商用SaaS服务,必须做租户级的资源隔离,避免某个租户的大流量、大文件处理,占满所有服务器资源,影响其他租户。

核心实现方案:

  1. 租户级并发限制:每个租户最多同时处理2个文档任务,最多同时发起10个问答请求,超过的请求排队等待
  2. 任务队列隔离:不同租户的文档处理任务,放到不同的队列,或者用Celery的租户路由,避免单个租户的任务占满所有worker
  3. 资源配额限制:每个租户有明确的文档数量、存储空间、月问答次数上限,超过就拦截,第七篇已经实现
  4. GPU资源隔离:向量化、重排序等GPU任务,给每个租户限制最大并发数,避免单个租户占满GPU

七、核心优化五:内存与资源优化,解决OOM崩溃

RAG系统最容易出现的崩溃问题,就是OOM内存溢出,尤其是在高并发下,我们针对性优化,从根源避免OOM。

7.1 模型单例模式,绝对禁止重复加载

所有耗内存的模型(Embedding、重排序、LLM本地模型),必须用全局单例模式,服务启动时加载一次,绝对不能每次请求都加载模型。

我们前面的代码已经全部用了单例模式,比如:

python 复制代码
# 全局单例,只加载一次
_embedding_engine = None
def get_embedding_engine() -> BaseEmbeddingEngine:
    global _embedding_engine
    if _embedding_engine is None:
        _embedding_engine = EmbeddingEngineFactory.get_engine()
    return _embedding_engine

7.2 大文件流式处理,绝对不一次性读入内存

新手最容易犯的错,就是大文件上传时,一次性把整个文件读入内存,10个用户同时上传100MB的文件,内存直接占满,OOM崩溃。

修改app/service/document_service.py的save_upload_file方法,用流式读取文件:

python 复制代码
async def save_upload_file(self, file: UploadFile, tenant_id: int, user_id: int, db: Session) -> Document:
    # 1. 校验文件格式
    file_ext = os.path.splitext(file.filename)[1].lower().replace(".", "")
    if file_ext not in settings.ALLOWED_FILE_EXTENSIONS:
        raise BusinessException(code=400, message=f"不支持的文件格式")
    # 2. 流式读取文件,分块写入,不一次性读入内存
    file_size = 0
    document_id = uuid.uuid4().hex
    save_file_name = f"{document_id}_{file.filename}"
    save_path = os.path.join(self.upload_dir, save_file_name)
    # 分块写入,每次读1MB
    chunk_size = 1024 * 1024  # 1MB
    with open(save_path, "wb") as f:
        while chunk := await file.read(chunk_size):
            file_size += len(chunk)
            # 校验文件大小
            if file_size > settings.MAX_UPLOAD_FILE_SIZE:
                os.remove(save_path)
                raise BusinessException(code=400, message=f"文件大小超过限制,最大支持{settings.MAX_UPLOAD_FILE_SIZE/1024/1024}MB")
            f.write(chunk)
    # 重置文件指针
    await file.seek(0)
    # 后续逻辑不变
    # ...

7.3 批量向量化动态批次控制

批量向量化时,固定batch_size很容易导致OOM,比如文本很长的时候,batch_size=32可能直接占满显存。我们实现动态batch_size,根据文本长度和显存占用,自动调整批次大小:

python 复制代码
def encode(self, texts: Union[str, List[str]], batch_size: int = 32) -> np.ndarray:
    if isinstance(texts, str):
        texts = [texts]
    # 动态调整batch_size:文本越长,batch_size越小
    avg_text_length = sum(len(text) for text in texts) / len(texts)
    if avg_text_length > 1000:
        batch_size = 8
    elif avg_text_length > 500:
        batch_size = 16
    else:
        batch_size = 32
    # 批量推理
    embeddings = self.model.encode(
        texts,
        batch_size=batch_size,
        show_progress_bar=False,
        normalize_embeddings=True
    )
    return embeddings

7.4 FastAPI Worker数优化

FastAPI的worker数不是越多越好,worker数太多会导致内存占用飙升,每个worker都会加载一次模型,很容易OOM。

生产级最优配置

  • CPU密集型服务:worker数 = CPU核心数
  • IO密集型服务:worker数 = CPU核心数 * 2
  • RAG系统(带GPU模型):worker数 = 1-2,因为GPU模型是全局单例,多worker会重复加载模型,占满显存。用异步+线程池提升并发能力,而不是加worker数。

启动命令示例:

bash 复制代码
# 生产环境启动,2个worker,足够支撑上千并发
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 --loop uvloop --http httptools

八、压测方案与调优

做完所有优化,必须通过压测验证效果,找到瓶颈,针对性调优。

8.1 压测工具:Locust

Locust是Python编写的压测工具,非常适合API压测,支持异步,能模拟上千用户并发。

8.2 核心压测指标

  1. QPS:每秒处理的请求数,核心指标
  2. 平均响应时间:接口的平均耗时,问答接口控制在2秒以内,查询接口控制在200ms以内
  3. 错误率:请求错误率必须低于0.1%
  4. CPU/内存/GPU使用率:压测时CPU使用率不超过80%,内存不超过80%,GPU显存不超过90%
  5. 服务稳定性:持续压测30分钟,服务不崩溃、不重启、无内存泄漏

8.3 压测场景

  1. 核心场景压测:问答接口,模拟100个用户同时发起问答,持续5分钟
  2. 混合场景压测:70%问答请求、20%文档查询请求、10%文档上传请求,模拟真实用户行为
  3. 峰值压测:模拟1000个用户同时发起请求,看服务的峰值承载能力
  4. 稳定性压测:50个并发用户,持续压测30分钟,看服务是否稳定,有无内存泄漏

九、踩坑记录&避坑指南:新手并发优化必踩的8个致命大坑

坑1:同步操作阻塞事件循环,并发能力暴跌

踩坑场景 :在异步接口里调用耗时的同步方法,比如LLM同步调用、向量化操作,直接阻塞事件循环,导致整个服务的并发能力暴跌,哪怕加再多worker也没用。
避坑方案:所有耗时超过100ms的同步操作,必须用线程池隔离执行,长耗时任务必须用Celery异步队列处理,绝对不能阻塞主事件循环。

坑2:缓存没有做租户隔离,出现严重数据泄露

踩坑场景 :缓存key没有带tenant_id,A租户的问答结果,B租户也能查到,出现严重的数据泄露,合规直接不通过。
避坑方案:所有业务缓存key,必须带上tenant_id,从架构层面保证租户隔离,绝对不能出现跨租户的缓存共享。

坑3:用BackgroundTasks处理长耗时任务,服务重启任务丢失

踩坑场景 :用FastAPI的BackgroundTasks处理文档处理这种长耗时任务,服务重启、机器重启,任务直接丢失,用户上传的文档一直卡在处理中,投诉不断。
避坑方案:所有长耗时任务,必须用Celery分布式任务队列处理,任务持久化,服务重启不丢失,支持重试、状态查询,生产级绝对不能用BackgroundTasks处理长耗时任务。

坑4:连接池配置错误,高并发下连接打满

踩坑场景 :数据库连接池的pool_size设得太小,max_overflow设得太大,高并发下创建了几百个数据库连接,导致数据库崩溃,服务无响应。
避坑方案 :严格按照我们给的生产级配置,pool_size=CPU核心数2,max_overflow=pool_size1.5,绝对不能盲目加大连接数,数据库的连接数不是越多越好。

坑5:没有限流熔断,突发流量直接打崩服务

踩坑场景 :没有任何限流保护,一次营销活动带来的突发流量,或者恶意用户刷接口,直接打满服务器资源,LLM账单爆炸,服务完全崩溃。
避坑方案:必须实现全链路限流、熔断、降级机制,从接入层就拦截异常流量,依赖故障时触发熔断,哪怕服务降级,也绝对不能完全崩溃。

坑6:模型重复加载,内存/显存直接占满OOM

踩坑场景 :每次请求都重新加载Embedding模型、重排序模型,几个请求过来,显存/内存直接占满,服务OOM崩溃。
避坑方案:所有模型必须用全局单例模式,服务启动时加载一次,后续请求复用同一个模型实例,绝对不能每次请求都加载模型。

坑7:大文件一次性读入内存,OOM崩溃

踩坑场景 :文件上传时,一次性把整个文件读入内存,几个用户同时上传大文件,内存直接占满,服务OOM崩溃。
避坑方案:大文件必须用流式分块读取,分块写入磁盘,绝对不能一次性读入内存,同时严格限制单文件大小。

坑8:多租户没有资源隔离,一个租户搞崩整个服务

踩坑场景 :没有租户级的资源隔离,某个租户一次性上传几百个大文档,占满了所有CPU/GPU资源,导致其他所有租户的问答请求全部超时,服务卡顿。
避坑方案:必须实现租户级的并发限制、配额限制、任务队列隔离,单个租户的资源使用有上限,绝对不能影响其他租户的服务可用性。


十、下一篇预告

本篇我们完成了生产级RAG系统的全链路并发优化,从异步化、缓存、连接池、限流熔断、资源管控全维度优化,现在你的系统已经能稳定支撑上千用户同时在线访问,具备了大规模商用的能力。

下一篇预告:第9篇《token成本控制与降级熔断方案,降本增效》,我会手把手带你完成:

  1. RAG系统全链路token成本拆解,找到成本浪费的核心环节
  2. 从Prompt、上下文管理、模型选型三个维度,实现token成本降低70%的方案
  3. 精细化的降级熔断策略,不同场景自动切换模型,平衡成本与效果
  4. 用量统计、账单预估、成本告警体系,实现成本全链路管控
  5. 完整的工程化代码,和本篇的熔断机制无缝衔接

结尾互动

本篇是《30天做一个生产级RAG知识库系统》全系列的第八篇,我们彻底搞定了高并发这个商用核心门槛,现在你的系统已经能稳定支撑大规模用户访问,完全具备了商用上线的能力。

最后想问一下大家:

  • 你在做系统并发优化的时候,遇到过最头疼的问题是什么?是服务阻塞、OOM崩溃,还是并发上不去?
  • 你的系统目前能支撑多少用户同时在线?遇到了哪些性能瓶颈?

欢迎在评论区留言,我会在后续的文章中,针对性地给你解决方案!

如果觉得这个系列对你有帮助,欢迎点赞、收藏、关注,后续的文章会第一时间推送给你,跟着更完,就能上线属于你的商用级RAG系统!

相关推荐
无忧智库1 小时前
智库级深度研判:数字中国浪潮下的医疗行业数字化转型与智慧医疗架构全景解构(PPT)
架构
贵慜_Derek2 小时前
我们能从 DeerFlow 学到哪些优秀的技术架构设计
人工智能·设计模式·架构
Coder个人博客2 小时前
19_apollo_cyber_base子模块软件架构分析
架构
LONGZETECH2 小时前
龙泽科技新能源充电设备仿真教学软件|技术解析+职教落地指南
科技·架构·汽车·汽车仿真教学软件·新能源汽车仿真教学软件
龙码精神2 小时前
JMeter压测QPS不翻倍问题排查与性能优化全记录
性能优化·架构
码界奇点3 小时前
基于Spring Boot的插件化微服务热更新系统设计与实现
spring boot·后端·微服务·架构·毕业设计·源代码管理
chaofan9803 小时前
2026年企业级AI基建:AWS Bedrock高并发架构深度实践与成本治理实操录
人工智能·架构·aws
qq_12084093713 小时前
Three.js 模型加载与线上稳定性实战:路径、跨域、缓存与降级全链路指南
开发语言·javascript·缓存·vue3
小江的记录本3 小时前
【 AI工程化】AI工程化:MLOps、大模型全生命周期管理、大模型安全(幻觉、Prompt注入、数据泄露、合规)
java·人工智能·后端·python·机器学习·ai·架构