09_TiDB AI应用性能优化与成本控制策略

张开发
2026/4/12 19:08:25 15 分钟阅读

分享文章

09_TiDB AI应用性能优化与成本控制策略
09_TiDB AI 应用性能优化与成本控制策略标签TiDB性能优化成本控制向量索引TiDB CloudServerless分区表关键词TiDB性能优化、向量索引优化、TiDB Cloud Serverless成本、分区表全局索引、预过滤、慢查询优化、批量操作、连接池优化、TiDB 8.5新特性、AI应用成本控制一、一次令人痛苦的账单某次我们一个 RAG 应用上线前三天运行良好第四天运维突然告警TiDB Cloud 计费超出预算 400%。查下来发现两个问题向量搜索没建索引全表扫描每次查询扫了 200 万行每个用户请求都实时调用 OpenAI Embedding API没有缓存产生大量重复请求这次事故之后我把 TiDB AI 应用的性能和成本优化整理成了一套系统性方法本文分享出来帮大家少踩坑。二、TiDB Cloud Serverless 成本模型深入理解2.1 计费维度TiDB Cloud Serverless 的计费包含三个维度TiDB Cloud Serverless 计费 存储费 计算费RU 网络传输费 --------------------------------------------- | 费用类型 | 单价参考 | --------------------------------------------- | 存储所有数据含向量 | ~$0.023/GB/月 | | 计算Request Units, RU | ~$0.10/百万RU | | 网络出口流量 | ~$0.09/GB | --------------------------------------------- 免费额度Free Plan - 5GB 存储 - 每月 5000 万 RU - 1GB 出口流量2.2 RU 消耗机制RURequest Unit是 Serverless 计算费用的核心计量单位操作类型 典型 RU 消耗 -------------------------------------------------- 简单 SELECT走索引1行 1 RU 向量搜索有索引返回10行 10-50 RU 全表扫描100万行 10,000 RU 写入1条记录 2 RU 批量写入100条记录 50 RURU 消耗的核心原则读取的行数和返回的数据量决定 RU 消耗。这直接说明了为什么向量索引和过滤条件对成本控制如此重要。2.3 向量搜索的 RU 成本对比100 万条向量数据查询 Top-10 无向量索引全表扫描 - 扫描行数100 万行 - 向量距离计算100 万次 - 每次查询 RU~50,000 RU - 月查询成本1万次/天$1,500/月 有向量索引 - 扫描行数~200 行ANN 近似搜索 - 每次查询 RU~50 RU - 月查询成本1万次/天$1.5/月 差距1000 倍这个数字清晰说明向量索引不仅是性能需求更是成本需求。三、向量索引深度优化3.1 索引创建时机-- 方案1建表时创建适合新表CREATETABLEknowledge_base(idBIGINTPRIMARYKEYAUTO_INCREMENT,contentTEXT,embedding VECTOR(1536));-- 先建表-- 插入少量数据测试-- 数据量超过 10 万条后再创建向量索引索引需要一定数据量才有优化效果ALTERTABLEknowledge_baseADDVECTORINDEXidx_emb_cos((VEC_COSINE_DISTANCE(embedding)));-- 方案2批量导入时的最佳实践-- Step 1: 关闭向量索引如果已存在则删除ALTERTABLEknowledge_baseDROPINDEXidx_emb_cos;-- Step 2: 批量导入所有数据没有索引时导入更快-- ... 批量 INSERT ...-- Step 3: 数据导入完成后创建索引一次构建比逐条维护高效ALTERTABLEknowledge_baseADDVECTORINDEXidx_emb_cos((VEC_COSINE_DISTANCE(embedding)));3.2 验证索引使用情况-- 确认向量搜索使用了索引关键EXPLAINSELECTid,content,VEC_COSINE_DISTANCE(embedding,[0.1, 0.2, ...])ASdistFROMknowledge_baseORDERBYdistASCLIMIT10;-- 期望输出中 type 字段包含 annIndex-- 如果看到 ALL 说明全表扫描需要检查索引是否正确创建-- 查看向量索引状态SHOWINDEXFROMknowledge_base;-- Key_name idx_emb_cos 且 Index_type 包含 VECTOR 说明创建成功3.3 向量维度选择精度 vs 成本不同维度对存储、索引和查询速度的影响维度 存储/条 索引构建 查询速度 语义精度MTEB ------------------------------------------------------- 512 2KB 快 最快 基准 768 3KB 中 快 3% 1024 4KB 中 中 5% 1536 6KB 慢 中 8% 3072 12KB 很慢 慢 12% 结论 - 768维BGE-base是性价比最佳点适合绝大多数中文场景 - 1536维OpenAI small是通用场景推荐 - 3072维仅当精度要求极高时使用实战建议先用 1536 维快速验证业务效果上生产后考虑换 768 维节省 50% 存储和索引成本。四、SQL 查询优化4.1 预过滤先缩小范围后向量搜索-- 错误示范先全量向量搜索后过滤扫描全量向量SELECTid,content,VEC_COSINE_DISTANCE(embedding,$vec)ASdistFROMknowledge_baseORDERBYdistASCLIMIT10;-- 然后在应用层过滤 category 技术文档 -- 浪费-- 正确示范先结构化过滤后向量搜索减少向量计算量SELECTid,content,VEC_COSINE_DISTANCE(embedding,$vec)ASdistFROMknowledge_baseWHEREcategory技术文档-- 先用 B-tree 索引过滤减少向量计算量ANDstatuspublishedORDERBYdistASCLIMIT10;预过滤能减少多少向量计算取决于过滤后的数据占比总数据量100 万条 category技术文档 后10 万条 (10%) 无预过滤计算 100 万次向量距离RU ~50,000 有预过滤计算 10 万次向量距离RU ~5,000 节省90% 的 RU4.2 避免 SELECT *-- 低效传输向量数据每条 6KB浪费带宽和 RUSELECT*FROMknowledge_baseORDERBYVEC_COSINE_DISTANCE(embedding,$vec)LIMIT10;-- 高效只返回需要的字段SELECTid,title,content,VEC_COSINE_DISTANCE(embedding,$vec)ASdistFROMknowledge_baseORDERBYdistASCLIMIT10;-- 如果下游不需要 embedding 列几乎总是如此绝对不要 SELECT *4.3 LIMIT 控制返回量-- 在 RAG 场景通常只需要 Top-5 到 Top-10 就够了-- 不要无脑设 LIMIT 100会拉取大量数据SELECTid,content,VEC_COSINE_DISTANCE(embedding,$vec)ASdistFROMknowledge_baseWHEREcategory$categoryORDERBYdistASCLIMIT5;-- 够用就好不要过大五、TiDB 8.5 LTS 新特性支撑大规模 AI 应用5.1 百万表级 SaaS 场景支持TiDB 8.5 在元数据管理上做了重大优化单个 TiDB 集群支持超过100 万张表。这对 SaaS 多租户 AI 应用意义重大SaaS 多租户 AI 应用常见架构 方案A8.5之前所有租户共享表用 tenant_id 隔离 - 向量检索每次都要加 WHERE tenant_id xxx - 数据隔离靠应用层控制风险高 方案B8.5推荐每个租户独立的向量表 - 创建 tenant_A_knowledge_base, tenant_B_knowledge_base... - 真正的数据隔离无需应用层 tenant_id 过滤 - 向量索引针对每个租户独立优化 100 万张表足够支撑100 万个租户每个租户 1 张向量表5.2 分区表全局索引TiDB 8.5 引入了分区表全局索引对 AI 历史数据查询场景影响显著-- 按时间分区的向量表适合需要保留历史向量的场景CREATETABLEtime_series_vectors(idBIGINTNOTNULLAUTO_INCREMENT,contentTEXT,embedding VECTOR(1536),created_dateDATENOTNULL,PRIMARYKEY(id,created_date))PARTITIONBYRANGECOLUMNS(created_date)(PARTITIONp2024q1VALUESLESS THAN(2024-04-01),PARTITIONp2024q2VALUESLESS THAN(2024-07-01),PARTITIONp2024q3VALUESLESS THAN(2024-10-01),PARTITIONp2024q4VALUESLESS THAN(2025-01-01),PARTITIONp2025VALUESLESS THAN(2026-01-01));-- 8.5 新特性全局索引允许跨分区的高效唯一性检索CREATEUNIQUEINDEXuidx_globalONtime_series_vectors(id)GLOBAL;-- 按时间范围的向量搜索只扫描相关分区SELECTid,content,VEC_COSINE_DISTANCE(embedding,$vec)ASdistFROMtime_series_vectorsWHEREcreated_date2024-10-01-- 分区裁剪只扫描 p2024q4 和 p2025ANDcreated_date2025-01-01ORDERBYdistASCLIMIT10;分区裁剪让历史数据查询只扫描相关分区对数据量超过 5000 万的时序向量场景性能提升可达 10 倍以上。六、应用层优化连接池与缓存6.1 连接池配置fromsqlalchemyimportcreate_enginefromsqlalchemy.poolimportQueuePoolimportosdefcreate_tidb_engine(database_url:str): TiDB Cloud Serverless 推荐连接池配置 returncreate_engine(database_url,poolclassQueuePool,# 核心参数pool_size10,# 基础连接数适合中小型应用max_overflow20,# 峰值时额外连接数总计 30pool_timeout30,# 等待连接超时秒pool_recycle3600,# 连接回收时间避免 TiDB 断开闲置连接pool_pre_pingTrue,# 使用前检测连接有效性# TiDB Cloud 特殊配置connect_args{ssl:{ca:os.getenv(TIDB_SSL_CA,/path/to/ca.pem)},connect_timeout:10})# 全局单例一个应用只创建一次enginecreate_tidb_engine(os.getenv(DATABASE_URL))连接池大小经验公式pool_size 并发用户数 × 平均每用户同时持有的连接数 对于 RAG API 服务 - 100 并发用户每个请求持有连接 ~0.1秒 - 每秒 100 个请求 × 0.1秒 10 个并发连接 - pool_size 10 ~ 206.2 查询缓存减少重复向量计算importhashlibimportjsonimportredisfromtypingimportOptional,ListclassCachedVectorSearch: 带 Redis 缓存的向量搜索减少重复查询 def__init__(self,vector_store,redis_client:redis.Redis,cache_ttl:int3600):self.vsvector_store self.cacheredis_client self.ttlcache_ttldef_cache_key(self,query:str,top_k:int,filters:dict)-str:生成缓存 keypayloadjson.dumps({q:query,k:top_k,f:filters},sort_keysTrue)returnfvec_search:{hashlib.md5(payload.encode()).hexdigest()}defsearch(self,query:str,top_k:int5,filters:dictNone)-List[dict]:带缓存的搜索cache_keyself._cache_key(query,top_k,filtersor{})# 尝试从缓存读取cachedself.cache.get(cache_key)ifcached:returnjson.loads(cached)# 缓存未命中执行真实查询resultsself.vs.similarity_search_with_score(query,ktop_k,filterfilters)# 序列化结果并缓存serializable[{content:doc.page_content,metadata:doc.metadata,score:float(score)}fordoc,scoreinresults]self.cache.setex(cache_key,self.ttl,json.dumps(serializable))returnserializable# 使用示例redis_clientredis.Redis(hostlocalhost,port6379,db0)cached_searchCachedVectorSearch(vector_store,redis_client,cache_ttl1800)# 第一次查询实际搜索resultscached_search.search(TiDB 向量索引怎么创建,top_k5)# 第二次相同查询缓存命中不消耗 TiDB RUresultscached_search.search(TiDB 向量索引怎么创建,top_k5)缓存策略建议缓存类型TTL适用场景嵌入向量缓存7天相同文本的向量不变可长期缓存搜索结果缓存1小时热门查询结果Schema 元数据缓存1天表结构不频繁变化6.3 嵌入 API 调用优化fromfunctoolsimportlru_cacheimporthashlibclassEmbeddingService:优化的嵌入服务带内存缓存 批量处理def__init__(self,openai_client,modeltext-embedding-3-small):self.clientopenai_client self.modelmodel self._cache{}# 内存缓存进程级defembed_single(self,text:str)-List[float]:单条嵌入带内存缓存# 对长文本做 hash 作为缓存 keycache_keyhashlib.md5(text.encode()).hexdigest()ifcache_keyinself._cache:returnself._cache[cache_key]respself.client.embeddings.create(modelself.model,inputtext)vecresp.data[0].embedding self._cache[cache_key]vecreturnvecdefembed_batch(self,texts:List[str],batch_size:int100)-List[List[float]]:批量嵌入复用缓存 分批处理result_map{}uncached_texts[]uncached_indices[]# 找出未缓存的fori,textinenumerate(texts):keyhashlib.md5(text.encode()).hexdigest()ifkeyinself._cache:result_map[i]self._cache[key]else:uncached_texts.append(text)uncached_indices.append(i)# 批量处理未缓存的forbatch_startinrange(0,len(uncached_texts),batch_size):batchuncached_texts[batch_start:batch_startbatch_size]respself.client.embeddings.create(modelself.model,inputbatch)forj,iteminenumerate(resp.data):original_idxuncached_indices[batch_startj]result_map[original_idx]item.embedding# 存入缓存keyhashlib.md5(batch[j].encode()).hexdigest()self._cache[key]item.embeddingreturn[result_map[i]foriinrange(len(texts))]七、大规模集群优化7.1 写入热点处理向量数据批量写入时AUTO_INCREMENT 主键容易造成写入热点所有写入集中到最大 Region-- 问题AUTO_INCREMENT 造成写入热点CREATETABLEvectors(idBIGINTPRIMARYKEYAUTO_INCREMENT,-- 写入热点...);-- 解决方案1使用 SHARD_ROW_ID_BITS 打散写入CREATETABLEvectors(idBIGINTPRIMARYKEYAUTO_INCREMENT,embedding VECTOR(1536))SHARD_ROW_ID_BITS4;-- 16 个分片分散写入到多个 Region-- 解决方案2使用 UUID 作为主键写入天然分散CREATETABLEvectors(idCHAR(36)PRIMARYKEYDEFAULT(UUID()),embedding VECTOR(1536));-- 解决方案3业务 ID如果有天然分散的情况CREATETABLEproduct_vectors(product_idBIGINTPRIMARYKEY,-- 业务 ID分散写入embedding VECTOR(1024));7.2 TiFlash 列存引擎HTAP 场景优化-- 为向量表的业务字段创建 TiFlash 副本用于大规模分析ALTERTABLEknowledge_baseSETTIFLASH REPLICA1;-- TiFlash 适合的查询-- 1. 大范围聚合分析统计类查询-- 2. 和向量搜索结合的复杂分析-- TiKV 行存更适合向量搜索点查 Top-K-- TiDB 自动选择最优引擎-- 向量搜索 - TiKV-- 聚合统计 - TiFlash-- 优化器根据成本自动决定八、监控与性能诊断8.1 关键监控指标-- 查看慢查询执行时间 500msSELECTquery,query_time,rows_examined,rows_sentFROMinformation_schema.slow_queryWHEREquery_time0.5ORDERBYquery_timeDESCLIMIT20;-- 查看当前活跃连接SHOWPROCESSLIST;-- 查看表的统计信息影响优化器决策SHOWSTATS_HEALTHYWHERETABLE_NAMEknowledge_base;-- 如果 Healthy 80建议手动更新统计ANALYZETABLEknowledge_base;8.2 应用层监控埋点importtimeimportloggingfromcontextlibimportcontextmanagerfromcollectionsimportdefaultdictclassPerformanceMonitor:向量搜索性能监控metricsdefaultdict(list)classmethodcontextmanagerdeftrack(cls,operation:str):追踪操作耗时starttime.time()try:yieldfinally:elapsed_ms(time.time()-start)*1000cls.metrics[operation].append(elapsed_ms)# 慢操作告警ifelapsed_ms500:logging.warning(f慢操作告警:{operation}耗时{elapsed_ms:.1f}ms)classmethoddefreport(cls):打印性能报告forop,timesincls.metrics.items():avgsum(times)/len(times)p99sorted(times)[int(len(times)*0.99)]print(f{op}: avg{avg:.1f}ms, p99{p99:.1f}ms, count{len(times)})# 使用withPerformanceMonitor.track(vector_search):resultsvector_store.similarity_search(query,k5)withPerformanceMonitor.track(embedding):vecembed_fn.embed_query(text)# 定期打印报告PerformanceMonitor.report()九、成本优化总结表优化措施成本节省实施难度优先级创建向量索引90% RU低一条SQL极高预过滤减少向量计算50-90% RU低改写SQL高避免 SELECT *30-50% 网络费低改写SQL高嵌入结果缓存70-90% API费中加缓存层高降低向量维度50-75% 存储中换模型迁移中分区表时序数据60-90% 历史查询中需重建表中Serverless 替代Dedicated70-90% 基础费低迁移高小团队批量写入替代逐条50% 写入RU低改代码中十、总结TiDB AI 应用的性能和成本优化是一个系统工程需要从索引、SQL、应用层、架构层多个维度综合考量。核心原则向量索引是最高优先级没有索引的向量搜索是成本黑洞预过滤策略先结构化过滤后向量搜索是最简单有效的优化嵌入缓存减少重复 API 调用是 AI 应用成本控制的必备手段TiDB 8.5的百万表支持和分区表全局索引为大规模 SaaS AI 应用提供了更好的架构选项监控驱动优化先发现问题再针对性优化避免过早优化相关资源TiDB Cloud Serverless 计费说明TiDB 8.5 分区表全局索引TiDB 向量索引文档

更多文章