我先说个昨天晚上的事儿哈。
昨天晚上十一点多,我在公司楼下抽烟,手机那边钉钉就“叮”一下,我们组那个小李问我: “东哥,Python 这服务 CPU 又打满了,要不要干脆用 Rust 重写算了?”
我当时脑子一抽就回他一句:“别瞎重写啊,用 Rust 把 Python 扛不动的那一块儿扛一下,让 Python 再伟大一回就行了。”
你看,这标题不就来了嘛:Rust 让 Python 再次伟大。
先把丑话说前头:Python 到底慢在哪儿
你们是不是也有这种体验:写业务、写脚本、写 Web,Python 那叫一个顺手; 但是一旦遇到那种很吃 CPU 的玩意儿,比如:
- 一堆循环算加密 / 压缩
- 批量图片处理
- 大量 JSON 解析 + 校验
- 数据清洗,动不动几十万行一刷
top 一看,Python 进程 300% CPU,风扇起飞,对吧。
根子其实就俩词:
- 解释执行 + GIL(别纠结细节,就当是“写起来爽,算起来累”)
- 很多库是 Python 写的 for 循环,一层套一层
但你说让大家都去写 C 扩展、写 C++,一堆指针、内存、段错误,谁爱写谁写去。 Rust 就不一样,它刚好踩在一个很尴尬但很舒服的位置上:性能顶、内存安全、工具链顺手。
所以思路其实很简单一句话:Python 负责“说话”,Rust 负责“干活”。
先来个最小可用例子:Python 调一个 Rust 加速函数
那个场景是这样的: 我们有个接口要把一堆数字求和,每次都是几百万个数。 原来小李写的是标准 Python:
# sum_slow.py
def sum_numbers(nums):
total = 0
for n in nums:
total += n
return total
if __name__ == "__main__":
data = list(range(10_000_000))
print(sum_numbers(data))
逻辑一点毛病没有,就是慢。 我跟他说,要不你把这个内层循环丢给 Rust 算,Python 只管把数组丢进去、结果拿出来。
整件事儿的核心其实是一个词:pyo3。 你可以简单理解成“帮你把 Rust 函数变成 Python 模块的那层胶水”。
Rust 这边大概长这样(示意,你不用一行一行背):
// src/lib.rs
use pyo3::prelude::*;
#[pyfunction]
fn sum_numbers_py(nums: Vec<i64>) -> PyResult<i64> {
let mut total: i64 = 0;
for n in nums {
total += n;
}
Ok(total)
}
#[pymodule]
fn fast_sum(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_numbers_py, m)?)?;
Ok(())
}
编译完以后(一般用 maturin 这种工具打个包就行),Python 这边的代码就能摇身一变:
# new_sum.py
from fast_sum import sum_numbers_py
def sum_numbers(nums):
# 这里直接调用 Rust 实现
return sum_numbers_py(nums)
if __name__ == "__main__":
data = list(range(10_000_000))
print(sum_numbers(data))
对小李来说,他日常写的 Python 代码几乎不用改, 就是 from 的地方换了下,把真正干体力活的那层换成 Rust 写的。
跑出来啥效果呢?具体数字我就不报了,反正肉眼可见的快, 原来接口动不动 2、3 秒,现在几百毫秒搞定。 你说这算不算是 Rust 在背后让 Python“再次伟大”了一把。
为啥不是“全都用 Rust 写算了”
这个问题你们肯定会问,小李也问了。 我当时跟他说,我说你想象一下你们组现在的技术栈:
- 监控组件、ORM、框架、SDK,全是围着 Python 生态转的
- 一堆脚本、运维、数据流程也绑在 Python 这套库上
- 团队里真正在项目里写过 Rust 的,可能就你一个
如果你一拍脑袋说:我们要“全面 Rust 化”,那接下来半年你就不用做业务了,天天在填坑: 部署方式改、监控方案改、日志改、SDK 重写一套,老脚本也得重来。
大部分团队其实只需要这么几件事:
- 95% 的业务逻辑依旧 Python,写得开心、上线快
- 那 5% 真正的性能瓶颈用 Rust 封一层
- 外面暴露出来还是 import 某个 Python 模块,别人感知不到 Rust 的存在
这不就好比啥? 饭店厨房里请了个硬核大厨(Rust),但前厅、收银、菜单(Python)一切照旧, 顾客只会觉得“这菜怎么忽然好吃、上得又快了”,不会关心后厨是谁在炒。
第二种组合拳:Rust 做服务,Python 当“前台”
上面那个是以内嵌扩展的模式(在一个进程里玩)。 有时候你们团队里已经有一帮写 Rust 的同学,他们更习惯搞个独立服务,比如:
- 用 Rust 写一个超快的校验 / 计算 / 推荐引擎
- 对外只提供 HTTP / gRPC 接口
那 Python 要做的就更简单了,跟调别的微服务没啥区别。 举个最土的例子,就当 Rust 那头有个 http://rust-core:8080/eval 的接口好了:
import requests
def eval_score(user_id: int, features: dict) -> float:
payload = {
"user_id": user_id,
"features": features,
}
resp = requests.post("http://rust-core:8080/eval", json=payload, timeout=0.2)
resp.raise_for_status()
data = resp.json()
return data["score"]
if __name__ == "__main__":
score = eval_score(123, {"age": 29, "vip": True})
print("score =", score)
这就完全当普通 HTTP 服务在用了。 差别只是: 平时你调用的是 Python 写的微服务,这次换成 Rust 写的而已。
优点是啥?
- 语言隔离,崩了也不会直接带走你的 Python Web 进程
- Rust 那边可以随便用多线程、多核,完全躲开 GIL 的限制
- 部署上直接当一个新的服务,滚动发布也简单
缺点也有:网络开销、序列化开销,但很多重逻辑场景这点成本是可以接受的。
还有一种更“懒”的玩法:直接薅生态的羊毛
这个其实你们已经天天在干,只是没注意它是 Rust 写的。 现在好多 Python 圈的“快库”,其实背后都是 Rust:
- polars:一个特别快的数据分析库,很多人拿它当 pandas 替代
- 一堆 JSON / 校验 / 解析相关的库,底层也在慢慢往 Rust 挪
- 静态检查、打包、依赖解析那一票工具里,Rust 也越混越熟
比如你要做个很简单的分析,小李原来写 pandas 是这样:
import pandas as pd
df = pd.read_csv("users.csv")
active_df = df[df["active"] == 1]
print(active_df.groupby("city")["id"].count())
后来他尝试了一下 polars,写法有点不一样,但思路挺像:
import polars as pl
df = pl.read_csv("users.csv")
active_df = df.filter(pl.col("active") == 1)
result = active_df.groupby("city").agg(pl.count())
print(result)
在数据量比较大的时候,你会发现 polars 就明显快很多, 但是对 Python 选手来说,学习成本并不离谱, 你要记住的事其实只有一句:“这是一个 Rust 写的高性能库,我拿 Python 来调它。”
你看,同样是利用 Rust 的性能,一种是你自己写扩展; 另一种是直接用别人已经帮你封装好的轮子。
来个稍微像样点的小实战:用 Rust 加速 Python 的简单打分逻辑
说个我们线上真干过的活,只不过我给你简化了一下。
背景: 有个实时接口,需要根据用户的一长串行为记录算一个“活跃度分数”, 大概类似:
def calc_score(events):
score = 0.0
for e in events:
if e["type"] == "login":
score += 1.0
elif e["type"] == "pay":
score += 5.0 * e.get("amount", 0)
elif e["type"] == "comment":
score += 0.5
# ... 乱七八糟很多规则
return score
问题不在逻辑,而在量:一个请求里 events 可能就上千条, 每秒几百个请求打进来,这点小循环撑不住。
思路就是把这段循环搬到 Rust,Python 负责准备好“干净”的结构。 比如 Python 这边改成这样(假设 Rust 那头导出了一个 calc_score_batch):
from fast_score import calc_score_batch # Rust 导出的函数
def calc_score(events):
# 先把复杂的 dict 结构,转换成比较规整的列表
types = []
amounts = []
for e in events:
t = e.get("type")
a = float(e.get("amount", 0) or 0)
types.append(t)
amounts.append(a)
# 交给 Rust 批量算
return calc_score_batch(types, amounts)
Rust 那头大概就是绕着两个 Vec 做循环, 这里我就不写完整了,反正结构跟前面 sum 那个类似, 只是多了一点枚举匹配的逻辑而已。
这事儿做完以后,最大的变化其实不是“快了多少倍”, 而是整个 Python 项目再也没有人为那个算分函数“微优化”了。 你知道那种感觉吧:以前大家会各种:
- 换成 list comprehension
- 想办法把 if-else 拆成字典查表
- 加缓存、加短路、改数据结构
做完 Rust 扩展之后,就没人想这些歪门邪道了, 接口慢了?优先考虑是不是别的地方出了问题, 这个模块就“稳定快速”,不用动它。
说点落地的:真要上手,大概要搞些什么
这块我简单念一嘴,你回头真要干,可以按这个方向查文档:
- 想在一个进程里玩(Python import Rust 模块那种)
- 查 pyo3 + maturin
- Rust 这边写 #[pyfunction]、#[pymodule] 那套
- Python 这边就当普通模块用
- Rust 那边随便选个 Web 框架(actix / axum 之类)
- Python 用 requests / httpx 去调
- 配合现有微服务体系就行
- 直接搜“xxx rust python binding”
- 看有没有社区已经写好的库,比如 polars 这种
重点是别一上来就搞得太大,挑一个最痛的热点先动一刀, 尝到甜头之后,再慢慢把其他瓶颈挪过去。
顺嘴说一句坑
Rust 虽然好,但也不是“银弹”,有几个坑你提早心里有数:
- 调试难度比纯 Python 大一点,尤其是跨语言调用时
- 编译时间会比你 pip 装个纯 Python 包要长
- 团队里总得有一两个真写得动 Rust 的,不然出了 bug 会很尴尬
所以我一般跟同事说: “别把 Rust 当目标,把性能当目标。Rust 只是当前看起来比较顺眼的那个方案。”
行了我不唠了,再唠下去该饿了,我还得去热个夜宵。 你要是真打算搞一把 Rust + Python 的组合,回头可以把你们的业务场景丢给我,我帮你一起想想先砍哪一刀比较划算。