大模型应用卡顿?数据库才是性能瓶颈的关键
1. 卡顿的真相一次请求要穿过五道数据关口1.1 模型出手之前数据层已经忙活半天了前两天一个朋友拉我去看他们刚上线的AI客服用户问一句我的订单到哪了系统要转十几秒才回复。他第一反应是模型参数不够准备换一个700B的更大模型。我说你先别急把全链路的调用日志拉出来看一眼。结果模型推理只花了不到400毫秒而用户会话历史的查询整整用了3秒。很多人觉得大模型应用就是请求进来-模型吐字-返回实际上在模型出手之前数据层已经忙活半天了。一次普通的AI问答至少要经历这么几个数据动作查用户信息确认身份和权限加载最近20轮会话历史保证上下文连贯如果走RAG还要把当前问题做向量化再检索知识库片段拼好Prompt送给模型模型返回后把答案、token用量、用户反馈写回库这么一算一次请求下来动不动就是四五次数据库交互。单次看着都不慢但每次都多出几百毫秒累计起来非常可观。我之前在一个项目里统计过模型调用只有350ms全链路却要2.8秒多出来的2.45秒全部耗在数据读取上。需要强调一点这个卡顿不一定每次都触发。低并发时数据库响应很快并发一上来慢查询、连接排队、锁等待全都来了表现就是系统聊着聊着突然卡住。大模型应用的服务端压力模型和传统Web应用有个明显区别它既要处理请求还要承担高吞吐的写入和检索。很多团队把原来做后端API的那套数据库设计直接搬过来结果就是模型再聪明数据喂不进去响应照样慢。1.2 为什么大家一遇到卡顿就盯着模型参数模型参数越大越聪明这个观念太强了。遇到卡顿第一反应往往是我的模型不够大、推理速度不够快、微调做得不到位。但仔细想一下模型参数影响的是生成质量和推理速度而数据库影响的是等待时间。这是两条完全不同的链路。用一个不太严谨但很好懂的比喻模型参数是高速公路上的车数据库是收费站。高速上车子性能再好时速能到200公里收费站前面排着长队你也只能在那里等着。大模型应用里数据库通常就是那个绕不开的收费站。所以你会看到一些很典型的场面团队为了治卡顿把7B模型换成70B模型结果延迟没降反升——更大模型推理本身就慢数据库瓶颈还在整体更卡。做了一次微调发现效果也没改善因为微调能改善回答质量治不了数据访问的慢。1.3 卡顿的真实分布数据库才是大头根据我接触过的线上问题复盘大模型应用的可感知卡顿真正由模型推理拖慢的比例其实不高。比较常见的延迟分布大概是数据库慢查询约40%连接池排队等待约30%模型响应约20%网络、序列化、周边服务约10%这些数字不是某个标准报告纯粹是我个人复盘多个项目后的经验汇总但趋势很说明问题绝大多数肉眼可见的卡顿锅不在模型参数上。如果一上来就砸钱换模型、堆显卡、搞微调基本是南辕北辙。2. 选型错了后面优化到死也没用按数据场景挑数据库2.1 关系型数据库管好业务账本别让它背AI的包袱MySQL、PostgreSQL这类关系型数据库最擅长的是结构化数据、事务和一致性。用户表、订单、权限、会话元数据放这里非常合理。但如果你把对话原文、向量、埋点日志全塞进去事情就开始变味了。我见过一个案例团队把一周的聊天记录全存在一张表里加了个JSON字段存对话内容。结果每次加载会话历史都要去那张已经膨胀到几百万行的表里做扫描。刚开始还好数据量一上来业务主库也跟着被拖慢。这就是典型的关系型数据库干了不该干的活。一个很实用的原则关系型数据库里只放数据和关系不要把内容大字段和检索负载硬塞进去。会话记录如果要存可以把元数据放关系库把消息体放到对象存储或者MongoDB这类更适合大文本的存储里。大模型应用对会话历史的读取频率极高表结构设计直接决定P99延迟。另外提一句MySQL改表的问题。业务上线后难免要加字段如果表已经很大直接ALTER TABLE会锁写对线上影响很大。这种操作要用gh-ost或者pt-online-schema-change这类在线改表工具避免在用户访问高峰期把整个业务卡死。2.2 向量数据库语义检索有专用赛道RAG是目前大模型应用最主流的知识库方案核心是先把文档切块、向量化存入向量库用户提问时再做语义检索。这里就涉及向量数据库的选型。常见的方案有Milvus、Qdrant、Chroma也可以直接用PostgreSQL的pgvector扩展。向量数据库和关系型数据库最大的区别是索引方式它对高维向量建立HNSW、IVF这类专用索引查询时是在海量向量里做近似最近邻搜索而不像传统关系库那样做精确匹配。这里有个常见误区认为向量数据库能解决所有存储问题。实际上向量库通常只负责找到哪些片段最相关原始文本内容、权限信息、版本信息还是要另外存一份。你需要先把向量库返回的ID拿回来再去原始存储里取内容。也就是说在使用向量库的前提下你的应用依然要维护一套和关系型存储或对象存储的关联逻辑。向量索引的超参数也值得说。HNSW索引里常见的两个参数是M和efConstruction。M控制每个节点的最大连接数M太大会导致内存暴涨M太小召回率下降efConstruction控制建索引时的搜索宽度不是越大越好太大了建索引时间会指数级上升。很多人在这一步容易拍脑袋我的建议是先按照官方文档的默认值建然后用真实数据召回率评测去调而不是一次性拉到最大。2.3 时序数据库日志和指标不该占着业务库大模型应用天然会产生大量可观测性数据token用量、推理耗时、请求来源、模型调用失败率、用户的调用频次。这类数据用TDengine、InfluxDB这类时序数据库来承接比硬塞进MySQL靠谱得多。时序库的几个优势是普通关系库很难替代的写入吞吐高支持按时间窗口聚合有内置的保留策略数据过期自动清理。比如你统计过去一小时每个用户平均token消耗在时序库里一条SQL搞定在MySQL里你会变成一个定时任务还要跟业务表争抢磁盘I/O。我自己的经验是接入监控和埋点越早越好不然等系统上线后再补会非常痛苦。顺便说一个细节TDengine的写入接口支持预编译方式类似数据库的prepare如果做大量写入用这种方式能有效降低解析开销。C调用时我记得对应的是taos_stmt_prepare那一套接口Python也有类似封装。这类先准备语句、再绑定参数的做法在大批量写入场景下收益很明显。2.4 一张选型对照表数据类型推荐方案不推荐方案原因业务结构化数据用户、订单、权限MySQL / PostgreSQLRedis / 向量库需要事务和一致性关系型库最稳会话历史、聊天记录PostgreSQL JSONB / MongoDB一张大表硬存大文本和元数据混合会拖垮主库知识库语义检索Milvus / Qdrant / pgvectorMySQL LIKE查询向量索引效率远高于文本扫描日志、指标、token统计TDengine / InfluxDB业务MySQL写入吞吐和聚合能力不在一个量级热点数据缓存RedisMySQL频繁读的业务内存缓存能扛住高并发这套对照不是金科玉律它只是一个起点。真实场景里还会涉及Elasticsearch做全文检索、对象存储做大文件、消息队列做异步削峰等。核心还是先想清楚数据的访问模式再谈选型。3. 实测同一个模型数据库不同卡顿差出十倍3.1 压测场景怎么搭为了把数据库选错这件事讲透我把我自己的一套压测环境大致描述出来。这是一个RAG知识库问答应用知识库里有200万条文本片段模型统一用同一个API接口唯一区别是存储和检索层方案。三种对比方案方案AMySQL 8.0一张表存id、title、content查询用LIKE加全文索引不引入任何向量库方案BPostgreSQL 15加pgvector在同一个表里同时存原始文本和向量建HNSW索引方案C原始文本放对象存储向量放Milvus应用先查向量库拿到top5的ID再回对象存储取内容压测用50个并发连续跑5分钟每个请求模拟一次完整的问答流程。我记录P50、P95、P99延迟和数据库侧CPU占用。需要说明具体数字会随硬件配置和数据分布浮动但相对趋势是稳定的。3.2 延迟和资源占用对比方案P50P95P99数据库CPU占用AMySQL文本检索800ms3200ms6500ms90%以上Bpgvector单表120ms580ms950ms45%CMilvus 对象存储90ms240ms420ms30%方案A在压测一开始还能跑并发一上去就开始崩很多请求的延迟是数据库连接排队造成的。方案B明显改善方案C最稳。这里说的数据库CPU占用是整体存储层的资源消耗方案C因为把文本读取和向量检索分开了单点压力小很多。3.3 为什么结果差这么多方案A惨是必然的。文本检索用LIKE本质上就是字符串扫描索引很难有效利用。200万条数据每条几百字一次扫描就是全表级别的I/O。再加上高并发数据库连接池很快被打满后面的请求只能等待。这不是模型参数的问题是存储方案从根上就不适配。方案B比A好是因为HNSW索引把暴力扫描变成了图搜索查询时间大幅下降。但pgvector单表方案也有一个隐患原始文本和向量存在同一张表里每条记录都带一个很大的content字段表会很宽数据量大了以后缓存命中率下降。压测里P95接近580ms就是这个原因。方案C快核心原因是职责分离。向量检索只负责返回ID原始文本由对象存储承担两边各自做自己最擅长的事。数据库不需要一次查询返回大字段I/O路径短了延迟自然低。这个测试给我的启发是别迷信单一数据库。选型阶段多花一天做压测比上线后通宵救火划算得多。4. 定位瓶颈三步确认卡顿到底是不是数据库的锅4.1 把调用链日志打出来遇到卡顿最先要做的事是看调用链耗时分布而不是猜。很多项目连基础的链路日志都没有一上来就怀疑模型非常容易误判。一个标准的做法是在应用层给每个外部调用打点记录耗时。伪日志长这样[request] user_id123 query我的订单到哪了 [session_load] cost2100ms [retrieval] cost850ms [llm_call] cost380ms [session_save] cost120ms这段日志出来谁慢一目了然。session_load占了2.1秒模型才380毫秒。你还要怎么定位直接去查会话历史的表结构和SQL就完事了。我见过不少团队日志只记了接口整体耗时和模型调用耗时中间的数据访问根本没埋点。这样排查起来就只能靠猜效率极低。建议从项目一开始就给关键数据访问加耗时打点这是后续所有优化工作的基础。4.2 数据库侧指标怎么盯应用日志确认数据访问慢之后接下来要去看数据库侧的真实情况。主要看三个东西连接数、慢查询日志、锁等待。MySQL里可以执行SHOW PROCESSLIST; SHOW ENGINE INNODB STATUS;PostgreSQL可以用pg_stat_statements来统计慢SQL。重点是判断是查询本身慢还是连接等不到。如果数据库CPU很高、慢查询日志里刷出一堆扫描几百万行的语句那是SQL和索引的问题。如果数据库CPU不高但应用侧大量请求卡在获取连接那是连接池参数的问题。这里有个容易忽略的特征连接池等待往往表现为并发一高就卡并发一降就恢复而且CPU曲线并不一定打满。很多人看到数据库CPU不高就以为数据库不是瓶颈其实连接池排队根本不体现在CPU上。4.3 别让这三个假凶手背锅排查下来有几个特别常见的误判我单独拿出来说误判真实原因验证方法模型上下文太长拖慢响应每次请求都重复加载大量会话历史慢的是DB查询看会话读取耗时网络跨机房导致慢模型调用后还多查了一次审核或权限表看调用链里每个环节耗时数据库性能不行要换库索引没建或者SQL写法导致全表扫描看EXPLAIN执行计划第一个误判我自己犯过。有一阵子总感觉模型响应慢后来发现是应用每次请求都把最近50轮对话全部查出来而且没做缓存。查询越慢服务端等待时间越长最终表现的延迟和模型上下文过长非常像但根因完全在数据层。5. 实用优化从缓存、连接池到索引和数据同步5.1 缓存分层先把重复查询干掉大模型应用里有大量重复查询。同一个用户的会话历史短时间内会被反复读取热门知识片段可能同时被几百个用户检索。这种场景最直接的优化就是加缓存。我的分层思路是会话级缓存当前对话过程中的上下文放进程内缓存或Redis设置短TTL用户级缓存用户资料、权限、偏好按用户ID做缓存知识库热点缓存高频命中的知识片段在向量检索后缓存原始内容注意缓存穿透问题。如果用户反复问一个知识库里根本不存在的冷门问题每次都穿透到数据库和向量库还是一样慢。可以对空结果也做短时间缓存或者对用户维度做限流。还有一个很实际的技巧在向量检索前加一层关键词命中缓存。如果用户问题在短时间内反复出现比如退换货政策是什么可以先用缓存挡住减少向量库查询压力。AI客服场景里高频问题占比非常高缓存收益远比想象中明显。5.2 连接池和SQL写法最便宜的两个优化点连接池不是越大越好。很多人以为maxActive设成500就能扛住高并发结果数据库线程切换开销反而把性能拖垮了。我一般建议从50开始压测观察延迟拐点再逐步调整。连接池里更关键的参数是maxWait也就是获取连接的最大等待时间。要设置一个合理的超时让失败快速暴露而不是让请求无限排队。SQL写法上大模型应用的查询模式比较固定基本都是按用户ID查最近会话按问题向量查相似片段。这类固定查询很适合用预编译方式减少SQL解析开销。另外两个常见问题尽量别用SELECT *只查需要的字段不要在WHERE子句中对列做函数运算否则索引失效。分页也有讲究。知识库里如果做加载更早的会话用传统的OFFSET方式在深分页时会越来越慢建议用游标或者基于ID的keyset分页。这是老生常谈但在大模型应用里依然频繁出现。5.3 索引不是越多越好向量索引更是如此关系型数据库里为高频查询建立联合索引很有必要。比如按用户ID创建时间查会话历史就应该建(user_id, created_at)的联合索引而不是单列索引。但索引不是越多越好每次写入都要维护索引索引过多会放大写开销。向量索引也一样。HNSW的M和efConstruction不是盲目调大就好。我常用的做法是先用官方默认参数建索引压测出P95延迟如果召回率不够再调M和efConstruction。参数改动后必须做召回率验证别只看延迟。还有个大表在线加字段的问题前面提到过。MySQL里直接ALTER TABLE会锁写我的习惯是能用gh-ost做的绝不停机改表。上线窗口再短也不能拿线上写流量开玩笑。5.4 数据同步分析负载和生产负载要分开大模型应用经常需要把业务库的数据同步到向量库或分析库比如用户行为、订单状态、知识文档更新。这时候不要直接在生产事务里做跨库写入而是用数据同步工具。常见的思路是CDC同步比如Flink CDC、Canal监听业务库的binlog把增量变化实时同步到下游。全量初始化可以用DataX这类批量工具。选择同步工具时重点看几个能力断点续传、位点管理、异常报警。否则数据同步链路断了你都不知道向量库里的知识片段过期了检索结果自然不准。我特别想强调一点分析负载和查询负载一定要分开。有一种做法是用同步工具把生产库的数据复制到只读分析库再让后台统计、报表、向量构建去分析库跑而不是在生产库上跑重型查询。大模型应用的token统计、调用量报表这类任务如果直接压在生产库上很容易把在线服务拖垮。6. 协同调优模型侧和数据库侧一起想办法6.1 模型侧真正能打的牌微调、裁剪、缓存说完数据库再说模型侧。大模型微调能改善模型对特定领域问题的回答质量但它治不了数据访问慢。RAG系统的延迟优化模型侧能做的主要是三件事。第一控制检索片段数量。很多RAG实现默认取top10甚至top20的片段每次检索返回的数据量大拼接进Prompt的token也多模型处理自然更慢。建议先压测不同片段数下的回答质量和延迟很多场景top3到top5就够了。少取几个片段数据库查询量下降模型推理也变快。第二对高频问题做答案缓存。如果用户问的是完全相同或高度相似的问题直接把缓存的回复拿出来既不打向量库也不打模型。这一招在AI客服、内部知识助手里效果极好很多问题的重复率能到30%以上。第三微调不是万能的但适当微调可以降低对检索的依赖。如果某些知识是高度固定的比如公司制度、产品参数把这些内容微调进模型业务上可以减少一部分RAG查询。不过知识更新频繁的内容还是得靠检索微调解决不了动态知识。6.2 数据库侧该调哪些参数数据库侧的参数调整我的顺序从来都是先压测再调参一次只改一个。常见的几类参数如下。MySQL和PostgreSQL这类关系库核心是缓冲池和内存参数。MySQL的innodb_buffer_pool_size要尽量容纳热数据PostgreSQL的shared_buffers、effective_cache_size、work_mem要一起配合work_mem太小会导致排序走磁盘。注意不要直接照抄网上的配置不同实例的内存、磁盘类型差别很大。向量库方面HNSW的M和efConstruction是首要参数另外要关注构建线程数。向量数据量大的时候构建索引消耗的CPU很高如果和在线检索共用资源会出现明显抖动。时序库方面要注意保留策略和聚合粒度。TDengine这类时序库对保留策略支持很好按天或按小时聚合能大幅减少存储压力和查询时间。如果不设置保留策略时间长了磁盘会被埋点数据塞满反过来拖慢一切。6.3 用QPS反推容量别拍脑袋买硬件很多团队扩容靠感觉我推荐一个小公式。假设业务并发是C每个请求平均对数据库发起的查询次数是N那数据库需要支撑的QPS基本约等于C乘以N。再结合单次查询的P95耗时T需要的并发连接数大约是C乘以N乘以T再除以1000。举例来说100个并发请求每个请求要查5次库单次查询P95是20毫秒那么数据库QPS需求就是500需要的连接数约等于100×5×0.0210。如果单次查询因为某条慢SQL变成200毫秒需要的连接数就变成100。连接数需求上了100连接池和数据库的线程调度压力明显变大卡顿自然就来了。这个计算非常粗糙但能帮你快速判断瓶颈在连接数数量还是单查询耗时。绝大多数卡顿最终都落在单查询耗时不达标上而不是连接数不够。6.4 什么时候才轮到大模型参数我的判断顺序很明确先确认数据层没问题再谈模型侧优化。具体说是这样打开调用链日志确认延迟分布优化数据库查询让数据访问P95降到百毫秒级加缓存和连接池调优把重复查询和高并发排队解决然后才看回答质量如果质量差、幻觉多再考虑微调、换更大的模型如果前三步没做完就急着堆参数大概率是浪费预算。反过来说如果数据层已经优化到很低的延迟但模型回答还是不行那才真正到了该换参数的时候。最后分享一个排查习惯我经手的大模型项目都会在数据库侧开启慢查询日志并设置超过500毫秒的SQL告警。这个习惯帮我们抓到过很多隐形问题尤其是RAG检索里那些看似正常、实则扫描了大量无效数据的语句。大模型应用的性能问题永远要先看数据流而不是先看参数。数据链路通畅了模型的价值才能真正发挥出来。