Skip to content

Robyn 性能调优:从默认值到真实压测 ​

Robyn 的宣传点一直是"快",但在真实业务里,框架本身通常不是瓶颈。这篇文章讲怎么把 Robyn 的默认值调到合适、以及哪些 Python 层的写法会把 Rust 运行时带来的优势吃掉。

一、先建立基准,别凭感觉调优 ​

调优前先有一条可复现的基线。用 wrk 压一个最简接口:

bash
# 4 线程 100 连接 持续 30 秒
wrk -t4 -c100 -d30s http://127.0.0.1:8080/ping
python
from robyn import Robyn

app = Robyn(__file__)

@app.get("/ping")
def ping():
    return "pong"

把此时的 QPS、P99 延迟记下来,然后每次只改一个变量再压一遍:先调 --processes,再调 --workers,最后才动代码。同时改三样东西得到的数字没有任何参考价值。

顺便提醒:压测客户端和被测服务不要跑在同一台机器上抢 CPU,否则你测的是机器调度能力。

二、进程数与线程数怎么取 ​

  • --processes:建议从 CPU 物理核数起步
  • --workers:每进程内的事件循环线程数,默认先用 1
bash
python app.py --processes=4 --workers=1

加 --workers 只在单个请求有大量 IO 等待时才有明显收益;如果你的请求本身是几条 SQL 然后返回 JSON,加线程带来的收益远小于增加进程。

用 --processes 的关键是记住:每个进程都会完整加载一份应用。内存占用随进程数线性上升,如果进程里有大对象(如加载到本地的 ML 模型),内存会迅速吃紧。这种情况反而应该少进程 + 多 workers,或者把模型放独立服务。

三、最常踩的坑:在 async 处理器里做阻塞调用 ​

事件循环只有一根管子,requests.get()、time.sleep()、pandas 大表计算这类同步阻塞调用会卡住整个事件循环上的所有请求。

错的例子:

python
import requests

@app.get("/proxy")
async def proxy(request):   # 看起来是协程,实际完全阻塞
    resp = requests.get("https://example.com/api")
    return {"data": resp.json()}

正确的写法是把阻塞调用挪出事件循环:

python
import asyncio
import httpx

@app.get("/proxy")
async def proxy(request):
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://example.com/api")
    return {"data": resp.json()}

# 实在只有同步库时,丢到线程池执行
@app.get("/legacy")
async def legacy(request):
    result = await asyncio.to_thread(blocking_sdk_call)
    return {"data": result}

同样的场景替换清单:

阻塞写法替换方案
requestshttpx.AsyncClient / aiohttp
time.sleep(n)await asyncio.sleep(n)
同步数据库驱动asyncpg / asyncmy / SQLAlchemy 异步会话
CPU 密集计算asyncio.to_thread() 或 ProcessPoolExecutor
本地大文件读写aiofiles 或丢给后端对象存储

注意这也意味着:print() 和写文件日志在高并发下会拖慢响应。把日志改成异步队列输出,或者直接交给 stdout + 采集器处理。

四、数据库连接池必须复用 ​

每请求新建连接是最常见的性能杀手,Robyn 也不例外:

python
# ❌ 每次请求都新建连接
@app.get("/users/:id")
async def user(request):
    conn = await asyncpg.connect(DATABASE_URL)
    return await conn.fetchrow("SELECT * FROM users WHERE id = $1", request.params["id"])

# ✅ 启动时建池,全局复用
POOL = None

async def init_db():
    global POOL
    POOL = await asyncpg.create_pool(DATABASE_URL, min_size=4, max_size=16)

app.startup_handler(init_db)

@app.get("/users/:id")
async def user(request):
    async with POOL.acquire() as conn:
        return await conn.fetchrow("SELECT * FROM users WHERE id = $1", request.params["id"])

连接池大小与 --processes 相乘才是总连接数,别超过数据库的 max_connections。

五、静态资源别压到 Python 进程上 ​

Robyn 的静态文件服务由 Rust 的 actix-files 驱动,性能很好(见 静态文件),但生产环境更推荐让 CDN 或 Nginx 承担,理由是可做缓存、压缩和 Zero Downtime:

python
import os
from robyn import Robyn

app = Robyn(__file__)
app.serve_directory(
    route="/static",
    directory_path=os.path.join(os.path.dirname(__file__), "build"),
    index_file="index.html",
)

六、别忘了限流与负载边界 ​

  • ROBYN_MAX_PAYLOAD_SIZE 控制最大 body(默认约 1MB),有上传需求时按需上调,但不要设成无限大,否则一次超大请求就能打爆内存(详见 Robyn 环境文件)
  • 生产环境关闭 --dev:开发模式带文件监听与热重载开销
  • 上游挂 Nginx 或 CDN 做连接数限制

七、调优后的检查顺序 ​

  1. 空接口 QPS 是否随 --processes 线性提升?不提升说明卡在 CPU 以外的资源
  2. P99 明显高于平均值?多半有阻塞调用没挪走
  3. 数据库 CPU 是否先于应用打满?先优化查询再谈框架
  4. 内存是否在多次压测后持续上涨?检查是否有模块级缓存无限增长

小结 ​

Robyn 的 Rust 运行时帮你省掉了一层 Python 开销,但它救不了 mistakes in handler code。真正的性能提升通常来自:挪走阻塞调用、复用连接池、让静态资源走 CDN,而不是继续加进程数。

基于 MIT 许可发布 · 隐私政策