高并发 API 压测与调优:locust + 火焰图 + 瓶颈定位

文章目录

    • [一、压测工具选型:locust vs wrk2 vs k6](#一、压测工具选型:locust vs wrk2 vs k6)
    • [二、Locust 实战:从脚本撰写到分布式压测](#二、Locust 实战:从脚本撰写到分布式压测)
      • [2.1 单文件压测脚本](#2.1 单文件压测脚本)
      • [2.2 分布式压测部署](#2.2 分布式压测部署)
    • 三、三段式压测方案设计
    • [四、Locust Web UI 指标解读](#四、Locust Web UI 指标解读)
    • [五、Python Profiling 冷启动:cProfile + pstats](#五、Python Profiling 冷启动:cProfile + pstats)
      • [5.1 在代码中嵌入 cProfile](#5.1 在代码中嵌入 cProfile)
      • [5.2 对于生产环境:py-spy 无侵入采样](#5.2 对于生产环境:py-spy 无侵入采样)
      • [5.3 火焰图读图方法](#5.3 火焰图读图方法)
    • 六、内存泄漏定位:tracemalloc
    • 七、数据库慢查询定位
      • [7.1 SQLAlchemy 端:打印所有 SQL](#7.1 SQLAlchemy 端:打印所有 SQL)
      • [7.2 PostgreSQL 端:pg_stat_statements](#7.2 PostgreSQL 端:pg_stat_statements)
      • [7.3 EXPLAIN ANALYZE 解读执行计划](#7.3 EXPLAIN ANALYZE 解读执行计划)
    • 八、调优三板斧
      • [8.1 连接池扩容](#8.1 连接池扩容)
      • [8.2 消除 N+1 查询](#8.2 消除 N+1 查询)
      • [8.3 批量写入](#8.3 批量写入)
    • 九、完整调优闭环:从压测到验证
    • 小结

上线后流量翻倍服务直接挂掉------这种事故在技术团队中并不罕见。但凭借上线前的压测数据和性能分析,它是完全可以避免的。在上线前用 locust 将服务压到极限吞吐量,用火焰图定位耗时最长的函数调用路径,再对瓶颈做针对性调优,最后以数据验证优化效果。整个闭环不依赖猜测,不靠"感觉能扛住"的直觉判断。

本文不孤立地讲解压测工具或 Profiling 工具,而是将它们组合成一个完整的"压测 → 定位 → 调优 → 验证"闭环,以真实可运行的代码串联每个环节。


一、压测工具选型:locust vs wrk2 vs k6

工具选型取决于压测场景的复杂度。以下是三款主流工具的工程能力对比:

维度 locust wrk2 k6
脚本语言 Python LuaJIT JavaScript (ES6)
分布式压测 原生支持(Master-Worker) 不支持 付费版支持(k6 Cloud)
恒定吞吐量 需自定义 Shape 原生支持(-R 参数) 通过 constant-arrival-rate 执行器
场景编程能力 强:支持条件分支、动态参数化、前置脚本 弱:仅请求序列 中:支持生命周期钩子、check/group
实时 Web UI 原生内置 通过 Grafana + Prometheus 集成
学习曲线 低(Python 生态) 中(需了解 k6 的执行模型)

wrk2 的恒定吞吐量压测能力在精确控制 QPS 场景中无可替代,适合做极限吞吐量的基准测试。k6 与 Grafana 的原生集成在需要精美报告和团队协作的场景中优势明显。locust 的核心长板在于场景可编程性和分布式压测无额外成本------这一点在需要模拟电商"浏览 → 加入购物车 → 下单 → 支付"这类多步骤用户行为的场景中,locust 的优势会被充分放大。


二、Locust 实战:从脚本撰写到分布式压测

2.1 单文件压测脚本

python 复制代码
from locust import HttpUser, task, between

class BookStoreUser(HttpUser):
    # 模拟真实用户的操作间隔:1~3 秒随机延迟
    wait_time = between(1, 3)

    def on_start(self):
        """每个虚拟用户启动时执行一次"""
        self.headers = {"Authorization": "Bearer test-token-123"}

    @task(weight=3)   # 浏览图书,权重 3
    def browse_books(self):
        self.client.get("/api/books", params={"page": 1, "size": 20}, headers=self.headers)

    @task(weight=2)   # 搜索图书,权重 2
    def search_books(self):
        self.client.get("/api/books", params={"q": "python", "page": 1}, headers=self.headers)

    @task(weight=1)   # 查看详情,权重 1
    def get_book_detail(self):
        book_id = random.randint(1, 1000)
        self.client.get(f"/api/books/{book_id}", headers=self.headers)

    @task(weight=1)   # 创建订单,权重 1
    def create_order(self):
        payload = {"book_id": random.randint(1, 500), "quantity": 1}
        self.client.post("/api/orders", json=payload, headers=self.headers)

@taskweight 参数直接决定了各接口被调用的频率分布。在上例中,浏览操占总请求的 3/7(约 43%),搜索占 2/7(约 29%),详情和下单各占 1/7(约 14%)。这种权重分配需要根据线上真实流量的接口调用比例来设定------直接抓取生产环境的 Nginx 日志分析 URI 分布,比拍脑袋设置的权重更有说服力。

2.2 分布式压测部署

单机 locust 受限于本地 CPU 和网络带宽,通常只能支持约 2000~3000 并发用户。当压测目标超过这一量级时,需要启用 Master-Worker 模式:

bash 复制代码
# Master 节点(仅负责聚合统计和 Web UI)
locust -f stress_test.py --master --expect-workers=3

# Worker 节点 1(只负责产生请求)
locust -f stress_test.py --worker --master-host=10.0.1.10

# Worker 节点 2
locust -f stress_test.py --worker --master-host=10.0.1.10

# Worker 节点 3
locust -f stress_test.py --worker --master-host=10.0.1.10

Master 节点本身不产生负载,虚拟用户实际由各 Worker 节点分担。正确的扩容方式是增加 Worker 节点数量,而非在单台机器上调高并发用户数。


三、三段式压测方案设计

将一次完整的压测分成三个连续阶段,每一阶段有明确的持续时间、用户变化曲线和观测目标。
#mermaid-svg-joSDqmbqpqfttKdS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-joSDqmbqpqfttKdS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-joSDqmbqpqfttKdS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-joSDqmbqpqfttKdS .error-icon{fill:#552222;}#mermaid-svg-joSDqmbqpqfttKdS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-joSDqmbqpqfttKdS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-joSDqmbqpqfttKdS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-joSDqmbqpqfttKdS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-joSDqmbqpqfttKdS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-joSDqmbqpqfttKdS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-joSDqmbqpqfttKdS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-joSDqmbqpqfttKdS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-joSDqmbqpqfttKdS .marker.cross{stroke:#333333;}#mermaid-svg-joSDqmbqpqfttKdS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-joSDqmbqpqfttKdS p{margin:0;}#mermaid-svg-joSDqmbqpqfttKdS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-joSDqmbqpqfttKdS .cluster-label text{fill:#333;}#mermaid-svg-joSDqmbqpqfttKdS .cluster-label span{color:#333;}#mermaid-svg-joSDqmbqpqfttKdS .cluster-label span p{background-color:transparent;}#mermaid-svg-joSDqmbqpqfttKdS .label text,#mermaid-svg-joSDqmbqpqfttKdS span{fill:#333;color:#333;}#mermaid-svg-joSDqmbqpqfttKdS .node rect,#mermaid-svg-joSDqmbqpqfttKdS .node circle,#mermaid-svg-joSDqmbqpqfttKdS .node ellipse,#mermaid-svg-joSDqmbqpqfttKdS .node polygon,#mermaid-svg-joSDqmbqpqfttKdS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-joSDqmbqpqfttKdS .rough-node .label text,#mermaid-svg-joSDqmbqpqfttKdS .node .label text,#mermaid-svg-joSDqmbqpqfttKdS .image-shape .label,#mermaid-svg-joSDqmbqpqfttKdS .icon-shape .label{text-anchor:middle;}#mermaid-svg-joSDqmbqpqfttKdS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-joSDqmbqpqfttKdS .rough-node .label,#mermaid-svg-joSDqmbqpqfttKdS .node .label,#mermaid-svg-joSDqmbqpqfttKdS .image-shape .label,#mermaid-svg-joSDqmbqpqfttKdS .icon-shape .label{text-align:center;}#mermaid-svg-joSDqmbqpqfttKdS .node.clickable{cursor:pointer;}#mermaid-svg-joSDqmbqpqfttKdS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-joSDqmbqpqfttKdS .arrowheadPath{fill:#333333;}#mermaid-svg-joSDqmbqpqfttKdS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-joSDqmbqpqfttKdS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-joSDqmbqpqfttKdS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-joSDqmbqpqfttKdS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-joSDqmbqpqfttKdS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-joSDqmbqpqfttKdS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-joSDqmbqpqfttKdS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-joSDqmbqpqfttKdS .cluster text{fill:#333;}#mermaid-svg-joSDqmbqpqfttKdS .cluster span{color:#333;}#mermaid-svg-joSDqmbqpqfttKdS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-joSDqmbqpqfttKdS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-joSDqmbqpqfttKdS rect.text{fill:none;stroke-width:0;}#mermaid-svg-joSDqmbqpqfttKdS .icon-shape,#mermaid-svg-joSDqmbqpqfttKdS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-joSDqmbqpqfttKdS .icon-shape p,#mermaid-svg-joSDqmbqpqfttKdS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-joSDqmbqpqfttKdS .icon-shape .label rect,#mermaid-svg-joSDqmbqpqfttKdS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-joSDqmbqpqfttKdS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-joSDqmbqpqfttKdS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-joSDqmbqpqfttKdS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段三: Spike

1 分钟
用户数: 1000 → 5000

瞬间冲击
目标: 验证限流/降级

观察失败率和恢复能力
阶段二: Steady

10 分钟
用户数: 恒定 1000
目标: 观测稳态延迟分布

P50/P95/P99
阶段一: Ramp-Up

5 分钟
用户数: 10 → 1000

线性递增
目标: 找到吞吐量饱和点

观察 RPS 增长是否线性

阶段一(Ramp-Up) 的关键观察指标是 RPS 的增长曲线。在理想情况下,RPS 应与用户数成正比增长。当 RPS 的增长开始放缓(即使增加用户数,RPS 也不再上升),说明系统已达到吞吐量极限,此时的 RPS 即为最大可承载吞吐量。

阶段二(Steady) 的核心指标是百分位延迟(Percentile Latency)。P50(中位数延迟)反映正常用户体验,P99 则是极端情况------P99 超过 1 秒意味着每 100 次请求中就有 1 次用户等待超过 1 秒,在电商场景中直接影响转化率。

阶段三(Spike) 用于验证系统的过载保护机制是否生效。若没有限流或熔断,5 倍流量冲击可能瞬间耗尽数据库连接池,导致所有请求返回 500 错误。


四、Locust Web UI 指标解读

在 Locust Web UI(通常监听在 http://localhost:8089)中,需要重点关注的指标如下:

  • RPS(Requests Per Second):每秒完成的请求数。这是衡量吞吐量的最直接指标,RPS 不再随用户数增长时即为系统极限。
  • 响应时间分布:关注 Median(中位数)、95%ile 和 99%ile。如果 99%ile 是 Median 的 10 倍以上,通常意味着存在长尾延迟,可能源于数据库慢查询或连接池等待。
  • 失败率:超过 1% 即属于严重问题。失败包括 HTTP 5xx、连接超时和连接拒绝。需要区分"可预期的失败"(如限流返回的 429)和"不可预期的崩溃"(如数据库连接耗尽返回的 500)。

五、Python Profiling 冷启动:cProfile + pstats

压测数据指向了性能瓶颈的存在(例如 P99 过高),但它无法告诉瓶颈的具体位置。下一步需要调用 Python 内置的 Profiling 工具。

cProfile 是 Python 标准库中最常用的确定性 Profiler,能精确记录每个函数的调用次数和耗时。

5.1 在代码中嵌入 cProfile

python 复制代码
import cProfile
import pstats
from io import StringIO

profiler = cProfile.Profile()

# 对 API 处理函数进行 Profiling
@app.get("/api/books/{book_id}")
def get_book(book_id: int):
    profiler.enable()
    result = _query_book_detail(book_id)
    profiler.disable()
    return result

# 输出 Top 20 耗时函数
s = StringIO()
ps = pstats.Stats(profiler, stream=s).sort_stats("cumulative")
ps.print_stats(20)
print(s.getvalue())

5.2 对于生产环境:py-spy 无侵入采样

cProfile 需要修改代码,且会对每个函数调用产生额外开销。在运行中的生产服务上,更推荐使用 py-spy------它是一个基于 process_vm_readv 系统调用的采样型 Profiler,不需要修改任何代码、不需要重启进程。

bash 复制代码
# 持续采样 30 秒,输出 SVG 火焰图
py-spy record -o profile.svg --pid $(pgrep -f "uvicorn") --duration 30

# 实时查看 Top 20 函数
py-spy top --pid $(pgrep -f "uvicorn") --duration 10

py-spy 的采样原理是周期性地读取目标 Python 进程的调用栈(默认每秒 200 次),然后统计每个函数在采样中出现的频次。某函数出现频次越高,意味着 CPU 在该函数上花费的时间越多。

5.3 火焰图读图方法

火焰图(Flame Graph)将调用栈从下到上堆叠,每一层的宽度代表该函数占总 CPU 时间的比例。以下是通过 py-spy 生成、在 speedscope.app 中可视化后的典型模式:

复制代码
┌──────────────────────────────────────────────┐
│         │ query_db() ██████████████████      │  ← 宽平台:数据库查询占用最大
│         │              parse_response()      │  ← 中间层:序列化
│  asgi_app()  ██████   ████████████          │  ← 底层:ASGI 框架开销
└──────────────────────────────────────────────┘

读图规律:

  • 宽函数 = CPU 耗时高。优先优化这些函数。
  • 平顶函数(上方没有更多调用栈)= 不存在深层递归,热点集中在该函数本身的 CPU 计算上。常见场景包括 JSON 反序列化、正则匹配、字符串拼接等纯 CPU 密集操作。
  • 塔形函数(上方有很深的调用栈)= CPU 时间分散在多层调用链中,需要逐层向下追溯找到真正的热点。

六、内存泄漏定位:tracemalloc

单次请求的慢是一个问题,随运行时间增长逐渐变慢是另一个更隐蔽的问题。后者通常源自内存泄漏------某些对象引用未被释放,导致 GC 频率和耗时不断增加。

Python 3.4 内置的 tracemalloc 模块提供了内存分配的追踪能力:

python 复制代码
import tracemalloc

tracemalloc.start()

# 快照 1:服务启动后拍摄
snapshot1 = tracemalloc.take_snapshot()

# ... 服务运行 1 小时后 ...

# 快照 2:运行一段时间后拍摄
snapshot2 = tracemalloc.take_snapshot()

# 对比两个快照,找出增长最多的内存分配
top_stats = snapshot2.compare_to(snapshot1, "lineno", limit=10)
for stat in top_stats:
    print(f"{stat.count} 个对象,+{stat.size_diff / 1024:.1f} KB")
    for frame in stat.traceback.format():
        print(f"    {frame}")

compare_to 返回的差分分析会列出内存增长最多的代码行。常见的内存泄漏模式包括:

  • 闭包捕获大对象:内层函数引用了外层的大字典或列表,导致这些对象始终无法被回收。
  • 全局缓存无限增长 :用 dict 做缓存但未设置 maxsize,在长时间运行后内存持续增长直至 OOM。
  • 事件循环中的回调未清理asyncio.create_task 创建的后台任务引用了请求级对象,导致每个请求结束后对象无法被回收。

七、数据库慢查询定位

Python 服务的性能瓶颈中,数据库查询占据了相当大的比例。定位慢查询需要从框架层和数据库层双管齐下。

7.1 SQLAlchemy 端:打印所有 SQL

python 复制代码
from sqlalchemy import create_engine, event

engine = create_engine(
    "postgresql://user:pass@host:5432/db",
    echo=False,
)

# 自定义 SQL 日志,添加耗时信息
@event.listens_for(engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info["query_start"] = time.monotonic()

@event.listens_for(engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    elapsed = time.monotonic() - conn.info["query_start"]
    if elapsed > 0.1:  # 仅记录超过 100ms 的慢查询
        logger.warning(f"SLOW SQL [{elapsed:.3f}s]: {statement}")

7.2 PostgreSQL 端:pg_stat_statements

sql 复制代码
-- 安装扩展(一次性)
CREATE EXTENSION pg_stat_statements;

-- 查询执行最慢的 SQL Top 10
SELECT
    queryid,
    calls,
    mean_exec_time::numeric(10,2) AS avg_ms,
    max_exec_time::numeric(10,2) AS max_ms,
    LEFT(query, 200) AS query_preview
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;

7.3 EXPLAIN ANALYZE 解读执行计划

pg_stat_statements 定位到慢查询后,通过 EXPLAIN ANALYZE 深入分析查询计划:

sql 复制代码
EXPLAIN ANALYZE
SELECT * FROM books WHERE author = 'Martin' ORDER BY created_at DESC LIMIT 20;

关键信息在输出的 Plan 行中:Seq Scan(全表扫描)是性能杀手,Index ScanBitmap Index Scan 是期望的结果。如果计划显示 Seq Scan on booksrows=500000,则需要为该查询添加索引:

sql 复制代码
CREATE INDEX idx_books_author_created ON books (author, created_at DESC);

八、调优三板斧

根据前面的定位结果,针对不同类型的瓶颈采用不同的优化策略。

8.1 连接池扩容

数据库连接池是并发性能的直接瓶颈。SQLAlchemy 默认的 pool_size=5 在 100 并发下会直接阻塞等待连接释放。

python 复制代码
engine = create_engine(
    "postgresql://user:pass@host:5432/db",
    pool_size=20,           # 从 5 提升到 20
    max_overflow=10,        # 允许额外 10 个临时连接
    pool_recycle=3600,      # 连接存活 1 小时后重建
    pool_pre_ping=True,     # 每次取出连接前检测有效性
)

测试数据表明,将 pool_size 从 5 提升到 20 后,并发场景下的 QPS 从 320 提升到 650,翻了一倍。但连接池不是越大越好------PostgreSQL 每个连接约占用 5~10MB 内存,100 个连接就是 1GB。需要根据数据库服务器的内存容量来设定上限。

8.2 消除 N+1 查询

N+1 查询是 ORM 环境中最常见的性能陷阱。以下代码在获取 100 本图书及其作者信息时,会触发 1 + 100 = 101 次数据库查询:

python 复制代码
# 问题代码:N+1
books = session.query(Book).all()  # 1 次查询
for book in books:
    author = book.author  # 每次循环触发 1 次查询

解决方案是使用 joinedload 预加载关联数据:

python 复制代码
from sqlalchemy.orm import joinedload

books = session.query(Book).options(
    joinedload(Book.author),
    joinedload(Book.category),
).all()  # 1 次查询,LEFT JOIN 加载所有关联数据

消除 N+1 后,数据库查询数从 101 降到 1,端到端响应时间从 3200ms 降至 45ms,降幅超过 98%。

8.3 批量写入

在数据导入、批量更新等场景中,逐条 INSERT 对数据库而言是性能灾难。即便每条 INSERT 仅耗时 1ms,插入 10,000 条记录就是 10 秒。

python 复制代码
# 逐条插入:10000 次数据库往返
for item in items:
    session.add(Book(**item))
session.commit()

# 批量插入:1 次数据库往返
session.execute(
    insert(Book).values(items)
)
session.commit()

对于 PostgreSQL,还可以配合 executemany 模式进一步优化:

python 复制代码
from sqlalchemy.dialects.postgresql import insert as pg_insert

stmt = pg_insert(Book).values(items)
# ON CONFLICT DO UPDATE:幂等写入
stmt = stmt.on_conflict_do_update(
    index_elements=[Book.isbn],
    set_=dict(price=stmt.excluded.price, updated_at=func.now()),
)
session.execute(stmt)

九、完整调优闭环:从压测到验证

下面是将所有技术串联起来的完整流程图:
#mermaid-svg-U1LpvqkU2kwSdVnt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U1LpvqkU2kwSdVnt .error-icon{fill:#552222;}#mermaid-svg-U1LpvqkU2kwSdVnt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U1LpvqkU2kwSdVnt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U1LpvqkU2kwSdVnt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U1LpvqkU2kwSdVnt .marker.cross{stroke:#333333;}#mermaid-svg-U1LpvqkU2kwSdVnt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U1LpvqkU2kwSdVnt p{margin:0;}#mermaid-svg-U1LpvqkU2kwSdVnt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster-label text{fill:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster-label span{color:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster-label span p{background-color:transparent;}#mermaid-svg-U1LpvqkU2kwSdVnt .label text,#mermaid-svg-U1LpvqkU2kwSdVnt span{fill:#333;color:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt .node rect,#mermaid-svg-U1LpvqkU2kwSdVnt .node circle,#mermaid-svg-U1LpvqkU2kwSdVnt .node ellipse,#mermaid-svg-U1LpvqkU2kwSdVnt .node polygon,#mermaid-svg-U1LpvqkU2kwSdVnt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-U1LpvqkU2kwSdVnt .rough-node .label text,#mermaid-svg-U1LpvqkU2kwSdVnt .node .label text,#mermaid-svg-U1LpvqkU2kwSdVnt .image-shape .label,#mermaid-svg-U1LpvqkU2kwSdVnt .icon-shape .label{text-anchor:middle;}#mermaid-svg-U1LpvqkU2kwSdVnt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U1LpvqkU2kwSdVnt .rough-node .label,#mermaid-svg-U1LpvqkU2kwSdVnt .node .label,#mermaid-svg-U1LpvqkU2kwSdVnt .image-shape .label,#mermaid-svg-U1LpvqkU2kwSdVnt .icon-shape .label{text-align:center;}#mermaid-svg-U1LpvqkU2kwSdVnt .node.clickable{cursor:pointer;}#mermaid-svg-U1LpvqkU2kwSdVnt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-U1LpvqkU2kwSdVnt .arrowheadPath{fill:#333333;}#mermaid-svg-U1LpvqkU2kwSdVnt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-U1LpvqkU2kwSdVnt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-U1LpvqkU2kwSdVnt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U1LpvqkU2kwSdVnt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-U1LpvqkU2kwSdVnt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U1LpvqkU2kwSdVnt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster text{fill:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt .cluster span{color:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U1LpvqkU2kwSdVnt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U1LpvqkU2kwSdVnt rect.text{fill:none;stroke-width:0;}#mermaid-svg-U1LpvqkU2kwSdVnt .icon-shape,#mermaid-svg-U1LpvqkU2kwSdVnt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-U1LpvqkU2kwSdVnt .icon-shape p,#mermaid-svg-U1LpvqkU2kwSdVnt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-U1LpvqkU2kwSdVnt .icon-shape .label rect,#mermaid-svg-U1LpvqkU2kwSdVnt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-U1LpvqkU2kwSdVnt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U1LpvqkU2kwSdVnt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U1LpvqkU2kwSdVnt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
CPU 密集
数据库





图书管理 API

上线前压测
Locust 三段式压测

Ramp-Up → Steady → Spike
P99 延迟

超过 500ms?
第一阶段定位

cProfile 输出 Top 20 函数
第二阶段定位

py-spy 生成火焰图
瓶颈类型判断
代码级优化

算法/缓存/预计算
数据库慢查询?
pg_stat_statements

定位 Top 慢查询
EXPLAIN ANALYZE

确认索引缺失
添加复合索引
检查连接池配置
调优后重测
存在 N+1 查询?
joinedload 预加载
批量写入优化
调优完成

输出对比报告

下面是一个完整的调优前后对比示例:

指标 优化前 优化后 提升
最大 QPS 320 req/s 1200 req/s +275%
P50 延迟 120ms 18ms -85%
P99 延迟 2400ms 95ms -96%
数据库查询数(每次请求) 6~101(N+1) 1~3(预加载) -85%
CPU 使用率(100 并发) 92% 38% -59%
连接池等待 频繁 ---

小结

性能调优不是玄学,它是由"压测定上限 → 火焰图定热点 → SQL 分析定根因 → 针对性优化 → 再压测验证"五个步骤组成的工程闭环。每一条优化都应该有压测数据做证伪,避免陷入"加个缓存试试"的试错循环。

locust 的可编程场景能力让压测脚本能模拟真实用户行为;cProfilepy-spy 的组合覆盖了"精确统计"和"无侵入采样"两种 profiling 模式;pg_stat_statementsEXPLAIN ANALYZE 则直接从数据库层面提供了最客观的查询成本分析。这些工具叠加使用,可以在半小时内定位到大多数性能问题的根因。

如果本文的压测与调优方法论对工作有所帮助,欢迎点赞、收藏与关注。关于 Python 服务在 K8s 部署、消息队列集成和缓存策略方面的工程实践,可以回顾本专栏此前的相关文章,构建从前端请求到后端存储的完整性能优化体系。

相关推荐
myenjoy_11 小时前
开源!Go+Wails+Vue3 手搓一个 PLC 实时监控桌面工具
开发语言·golang·开源
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年6月4日
人工智能·python·ai·信息可视化·自然语言处理·ai编程·灵砚智能
Flash.kkl1 小时前
C++基于websocketpp的多用户网页五子棋项目
开发语言·网络·数据库·c++·websocket·mysql
酉鬼女又兒1 小时前
零基础入门计算机网络物理层:核心概念、传输媒体、传输方式、编码调制与信道极限容量完整知识点总结
开发语言·网络·计算机网络·考研·职场和发展·php·信息与通信
kong@react1 小时前
milvus(向量数据库)docker容器(升级1.0)
数据库·docker·milvus
开发者联盟league1 小时前
docker登录失败解决方法。http: server gave HTTP response to HTTPS client
http·docker·https
用户67573181940251 小时前
两个Bot不能聊天,我让它们自己建了一条高速公路
python
quqi991 小时前
为什么电脑不亮灯(by quqi99)
docker·samba
qq_452396231 小时前
第十八篇:《Docker 监控与性能优化》
docker·容器·性能优化