FDE Training · Module 16

16

性能与成本优化 · 让系统更快、更省

← 返回 FDE 培养总览

模块 16 · 性能与成本优化

本章目标:客户系统上线后,"慢"和"贵"是两个最常被投诉的问题。FDE 要能定位瓶颈、给出量化优化方案、算清楚省了多少钱。不讲理论公式,讲 FDE 在现场用的三板斧:Profile 找瓶颈、缓存减重复、LLM 降本。


慢查询/高延迟 用户投诉 Profile 定位瓶颈 cProfile/py-spy/p99 三板斧优化 缓存/异步/降本 量化验证 压测前后对比
优化闭环:发现问题 → Profile 定位 → 动手优化 → 压测量化验证

16.1 先量后改:没有数据不要瞎优化

FDE 最容易犯的错是"凭感觉优化"——觉得某个地方慢就去改,结果改完没提升。正确姿势是:

  1. 先建基线:现在接口 p99 延迟是多少?数据库 QPS 多少?LLM 单次调用多少 Token?
  2. 用工具找瓶颈:不是猜,是量出来。
  3. 改一处测一处:别一次改十处,出了问题不知道是哪个改坏的。

80/20 法则:20% 的代码导致了 80% 的耗时。优化要找到那 20%,而不是平均优化所有代码。


16.2 后端性能:Python 服务怎么 Profile

16.2.1 接口慢:先看慢在哪一层

一个接口慢,可能是:数据库查询慢、外部 API 调用慢、业务逻辑计算慢、序列化慢。用中间件打时间戳:

import time
from functools import wraps

def timing(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        result = fn(*args, **kwargs)
        t1 = time.perf_counter()
        print(f"{fn.__name__}: {((t1-t0)*1000):.1f}ms")
        return result
    return wrapper

# 或者用 cProfile 跑一次完整请求
# python -m cProfile -s cumulative myapp.py

16.2.2 数据库慢查询

90% 的后端性能问题最终是 SQL 慢:

-- 开启慢查询日志(PostgreSQL)
-- 查最耗时的 SQL
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;

-- EXPLAIN ANALYZE 看执行计划
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';
-- 如果看到 Seq Scan(全表扫描),就要加索引
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

16.2.3 常见优化清单

问题症状解法
N+1 查询列表页循环里一条条查关联数据JOIN 或批量 IN 查询
缺索引大表全表扫描WHERE/JOIN 字段加复合索引
内存泄漏进程越跑越占内存查对象引用、加对象池
同步阻塞调外部 API 时 worker 卡住改异步或加超时

16.3 缓存:把重复计算挡在数据库前面

缓存是性价比最高的优化。原则:读多写少的数据,优先加缓存。

import redis
import json

r = redis.Redis(host="localhost", port=6379, db=0)

def get_user_profile(user_id):
    cache_key = f"user:{user_id}:profile"
    # 1. 先查缓存
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    # 2. 缓存没有,查数据库
    user = User.query.get(user_id)
    data = {"id": user.id, "name": user.name, "role": user.role}
    # 3. 写缓存,设过期时间
    r.setex(cache_key, 300, json.dumps(data))   # 5 分钟
    return data

def update_user_profile(user_id, **fields):
    User.query.filter_by(id=user.id).update(fields)
    db.session.commit()
    # 更新数据库后,删掉旧缓存(别写脏缓存)
    r.delete(f"user:{user_id}:profile")

缓存的三个坑:

  1. 缓存穿透:查不存在的 key 每次都打到 DB。解决:空值也缓存(短过期),或布隆过滤器。
  2. 缓存雪崩:大量 key 同时过期,瞬间全打到 DB。解决:过期时间加随机偏移。
  3. 缓存一致性:改了 DB 忘删缓存。解决:更新 DB 后立即删缓存,别写缓存。

16.4 LLM 成本优化:Token 就是钱

大模型是客户账单里的大头。优化空间非常大:

16.4.1 选对模型:不是所有任务都要用最贵的

任务推荐模型理由
简单分类/提取小模型(gpt-4o-mini / 本地 7B)便宜 10 倍,效果够用
RAG 问答中等模型给了资料,不需要最强推理
复杂推理/写代码大模型(gpt-4o / Claude)难任务才用贵的
批量处理批量 API(Batch)半价,异步可接受

16.4.2 Prompt 优化:减少冗余 Token

# 反例:每次都塞一堆重复的系统提示 + 超长上下文
prompt = f"""你是一个专业助手。{long_system_prompt_1000_tokens}
以下是历史对话:{huge_chat_history_5000_tokens}
以下是检索到的文档:{huge_documents_8000_tokens}
用户问题:{user_question}
"""

# 优化:
# 1. 历史对话只保留最近 5 轮,更早的做摘要
# 2. RAG 只取 top-3 最相关片段,别塞 10 段
# 3. 系统提示精简,别写废话
# 4. 明确 max_tokens 限制输出长度

16.4.3 缓存重复请求

import hashlib

def cached_llm_call(messages):
    # 对消息内容做 hash 当 key
    key = hashlib.md5(json.dumps(messages, sort_keys=True).encode()).hexdigest()
    cached = r.get(f"llm:{key}")
    if cached:
        return cached
    resp = call_llm(messages)
    r.setex(f"llm:{key}", 3600, resp)   # 缓存 1 小时
    return resp

FAQ、常见问题这种重复率高的场景,缓存命中率能到 50% 以上,直接省一半钱。


16.5 推理性能:本地部署怎么提速

客户要求私有化部署大模型时,推理速度直接影响体验。常用手段:

  • vLLM 连续批处理(continuous batching):比原生 transformers 快 5-10 倍。
  • 量化:FP16 → INT8/INT4,显存减半,速度提升 30-50%,精度损失可接受。
  • KV Cache 复用:多轮对话时不用重新计算历史 token。
  • 限流+排队:并发太高时排队比 OOM 崩了好。
# vLLM 启动示例
python -m vllm.entrypoints.openai.api_server \
  --model /models/qwen2.5-72b \
  --quantization awq \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.9 \
  --port 8000

16.6 动手练习(可折叠答案)

练习 1:一个列表接口要 3 秒,你怎么定位瓶颈?

答案思路:

  1. 加 timing 中间件,打印数据库查询时间、外部调用时间、业务处理时间。
  2. 如果 DB 占了 2.5 秒 → 查 pg_stat_statements 找慢 SQL,看有没有缺索引、N+1。
  3. 如果外部 API 占了 2 秒 → 看是调了几次、能不能并行、能不能加缓存。
  4. 如果业务逻辑占了 2 秒 → cProfile 跑一下,看哪个函数耗时。
练习 2:客户月度 LLM 账单从 $200 涨到 $2000,你怎么降本?

答案思路:

  1. 查用量分布:按功能/用户/模型拆,看哪部分涨得最多。
  2. 简单任务换小模型:分类、提取这种,gpt-4o-mini 比 gpt-4o 便宜 15 倍。
  3. 优化 Prompt:RAG 只留 top-3 片段,历史对话做摘要,减少输入 token。
  4. 加结果缓存:FAQ 类问题命中缓存直接返回。
  5. 批量任务走 Batch API:半价。
  6. 设月度预算告警,超了自动降级。
练习 3:缓存和数据库一致性怎么保证?

答案思路:

  1. 读路径:先查缓存,miss 再查 DB,然后回填缓存。
  2. 写路径:先更新 DB,再删除缓存(不是更新缓存)。
  3. 为什么删而不是更:并发写时更新缓存容易写脏;删了下次读自然从 DB 拉新的。
  4. 极端情况:删缓存失败怎么办?加重试,或设短过期时间兜底。

16.7 本章面试题

  1. "你怎么判断一个系统慢在哪?"→ 答:先建基线(p99 延迟、QPS),然后分层打时间戳(DB/外部调用/业务逻辑),用 cProfile / pg_stat_statements / 慢查询日志定位具体瓶颈,不要凭感觉改。
  2. "缓存穿透、雪崩、击穿分别是什么?怎么解?"→ 答:穿透=查不存在的 key 每次打 DB(空值缓存/布隆过滤器);雪崩=大量 key 同时过期(过期时间加随机);击穿=热点 key 过期瞬间打 DB(互斥锁重建)。
  3. "LLM 成本优化有哪些手段?"→ 答:任务分级选模型(简单用小模型)、精简 Prompt 减少 token、结果缓存、批量 API 半价、设预算熔断。
  4. "为什么先 Profile 再优化?"→ 答:80% 的耗时集中在 20% 的代码上。瞎优化花了精力还没效果,甚至引入 bug。量出来再改,改完压测验证。

16.8 小结

  • 性能优化先量后改:建基线 → Profile 定位 → 改一处测一处。
  • 后端慢 90% 是 SQL 问题:慢查询日志 + 索引 + 消除 N+1。
  • 缓存是性价比最高的优化,但要处理穿透/雪崩/一致性。
  • LLM 降本三板斧:选对模型、精简 Prompt、结果缓存。
  • 优化完必须压测对比,用数据说话——快了多少、省了多少。
本页由 FDE 培养课程文档生成,完整课程见 FDE 培养 · 课程总览。