压测一跑,CPU 100%,机器风扇跟要起飞一样,代码一行没改,吞吐却能从每秒几百条蹿到几万。第一次看到这组数据,我第一反应不是“Python 变快了”,而是这里八成有人把账算错了。
后来翻 perf、看火焰图、再把启动命令扒开,才发现慢的根本不是业务逻辑本身,是解释器在硬扛。很多 Python 服务慢,不是慢在你那几行 if else,也不是慢在 for 循环写得土,慢在默认跑法太老实。默认解释器能跑,但不一定适合线上吞吞吐吐的活。
比如下面这段代码,逻辑很普通,做日志清洗、字段归一化,再聚合统计。很多内部脚本、数据处理接口,本质上都长这样:
import json
from collections import defaultdict
def handle(lines):
counter = defaultdict(int)
for raw in lines:
item = json.loads(raw)
uid = item.get("user_id")
event = item.get("event")
if not uid or not event:
continue
key = f"{uid}:{event}"
counter[key] += 1
return counter
这段代码改不改,当然还能继续抠。比如少建点对象,少拼点字符串,换个更快的 json 库。但有些场景里,你还没开始改业务,先把运行时换掉,收益就已经很离谱了。
我这边更常见的一套做法,是直接把 CPython 换成 PyPy 跑一轮基准。尤其是这种长时间运行、纯 Python 逻辑占比高、对象创建频繁的任务,效果经常比改几天代码还直接。
命令不用花活:
pypy3 worker.py
或者你原来是这么起:
python3 app.py
现在改成:
pypy3 app.py
就这一刀。
为什么会这样?因为 PyPy 不是单纯换个壳,它有 JIT,会把热点代码在运行时编译优化。那种反复执行的 Python 路径,CPython 是老老实实一句句解释,PyPy 会盯住热点循环狠狠干。请求一多、任务一长,差距就出来了。
我之前拿一个内部清洗脚本做过对比,逻辑和上面差不多,输入是几百万行 JSON 日志。CPython 跑完要几十秒,PyPy 下去直接压到 1 秒级,倍数看着夸张,但真不是玄学。你要是业务里正好堆着大量字符串处理、字典操作、循环判断,这种收益并不稀奇。
简单压测脚本也能自己跑:
import time
import json
payload = json.dumps({
"user_id": 1024,
"event": "pay",
"amount": 99,
"ts": 1710000000
})
def work(n=2_000_000):
hit = 0
for _ in range(n):
obj = json.loads(payload)
if obj["amount"] > 10:
hit += 1
return hit
start = time.time()
total = work()
print(total, round(time.time() - start, 3))
同一台机器,别先看“理论上”,直接分别用 python3 和 pypy3 跑,结果通常很诚实。
不过这事也不是见谁都上。第一眼我一般先看三个东西。
先看热点是不是在 Python 层。要是你服务 80% 时间都耗在 MySQL、Redis、HTTP 调用上,那你换解释器,收益很有限,瓶颈根本不在本地执行。
再看有没有 C 扩展重依赖。像 numpy、pandas 这类库,很多场景本来就跑在底层 C 上,PyPy 不一定占便宜,有些包兼容性还得单独验证。
最后看任务是不是“热得起来”。PyPy 的 JIT 需要预热,短命脚本、跑一下就退出的命令行工具,未必划算。常驻进程、批处理任务、消费程序,这种更适合。
线上判断也不复杂,别一上来全量切。先拿一个 CPU 高、外部依赖少的 worker 灰度。盯三组指标就够了:单机吞吐、CPU 使用率、P99 延迟。数据好看再放量,数据不好立刻回切,成本其实不高。
还有个细节很多人会漏:别拿“冷启动第一分钟”的数据下结论。PyPy 预热前没那么猛,你要看稳定运行后的曲线。这个跟 JVM 有点像,别刚起服务就拍桌子说没提升,那多半是看早了。
所以这事真正有意思的地方,不是 Python 忽然变神了,而是很多人默认把“性能优化”等同于“改代码”。其实有时候不用碰业务逻辑,先把运行时、启动方式、执行环境这几件事看一遍,收益可能更大。
代码当然还能继续抠,但在动刀之前,我更愿意先确认一件事:你现在这份代码,到底是写得慢,还是跑得冤。