找回密码
立即注册
搜索
发回帖 发新帖

5420

积分

0

好友

698

主题
发表于 昨天 21:19 | 查看: 9| 回复: 0

线上有个接口突然报警。
不是 QPS 飙了,也不是数据库慢,而是 CPU 直接打满。

我看监控第一眼有点懵:这个接口平时挺轻,主要就是查数据然后返回 JSON。没有复杂计算,没有大循环,也没有大 SQL。

结果火焰图一展开,最热的位置居然是:

json.dumps()

我当时第一反应:“JSON 序列化还能把 CPU 吃满?”

后来才发现,问题不是 JSON 慢,而是我们太相信 JSON 了。

很多 Python 服务里,JSON 就像空气一样存在。接口返回它,日志打印它,消息传它,缓存存它。小数据时没感觉,数据一上来,序列化成本就藏不住了。


先看一个很常见的写法。业务接口查出来一批数据,然后直接返回:

import json

def build_response(rows):
    result = []
    for row in rows:
        result.append({
            "id": row["id"],
            "name": row["name"],
            "detail": row["detail"]
        })
    return json.dumps(result)

代码没毛病,很多项目里就是这么写的。但线上数据量一上来,这里会发生几件事:

第一,Python 对象创建。
第二,字符串编码转换。
第三,JSON 格式拼接。
第四,大量内存复制。

假设返回的数据有 20 万条,你以为只是“转一下格式”,实际上 CPU 在疯狂做字符串处理。


我遇到过一个类似场景:一个数据导出接口,用户点击下载报表。后端逻辑大致是:

  1. MySQL 查询 20 万条数据
  2. Python 组装 dict
  3. json.dumps 序列化
  4. 把结果返回前端

接口耗时从几秒慢慢涨到几十秒。监控看到:

CPU: 95%
Memory: 正常
DB: 正常
Network: 正常

这种情况我一般不会先看数据库。因为数据库正常、网络正常,CPU 高,优先怀疑的就是应用层计算。直接抓了一下 profile,结果:

json/encoder.py
耗时占比 48%

问题就出来了:不是查数据慢,而是返回数据之前把 CPU 烧掉了。


后来把代码拆开看,原来还有一个隐藏操作。为了方便排查问题,开发加了日志:

logger.info(
    "export data=%s",
    json.dumps(data)
)

这一行更坑。很多人以为日志级别 INFO 关闭了,这个序列化不会执行。实际上不是这样。

Python logging 的参数处理和字符串格式化方式有关。如果提前拼接:

logger.info(
    "export data=" + json.dumps(data)
)

不管日志开不开,json.dumps 都会执行。数据量大的时候,日志没输出,CPU 先干活。

我线上看到过类似代码:

def save_order(order):
    content = json.dumps(order)
    logger.debug(content)
    save_to_db(order)

debug 日志关闭了,但序列化已经发生了。


这种地方我一般会改成延迟处理。比如:

import logging

logger = logging.getLogger(__name__)

def save_order(order):
    logger.debug(
        "order snapshot=%s",
        order
    )
    save_to_db(order)

让 logging 自己决定是否处理,不要提前把大对象转字符串。


还有一个坑在接口返回里更明显。很多人喜欢这么写:

return json.dumps(
    {
        "code": 0,
        "data": big_list
    }
)

小数据没问题,大数据直接炸。

我的处理习惯是先看数据规模。比如:

def check_payload(items):
    size = len(items)
    if size > 50000:
        raise ValueError(
            f"payload too large:{size}"
        )

不是说 50000 一定不行,而是接口边界要明确。一个接口返回几十万条数据,本身设计就值得怀疑。


那 JSON 有没有更快的方式?有。Python 默认的 json 库追求兼容性,性能场景可以考虑更快的序列化库,比如 orjson。简单测试代码:

import json
import orjson
import time

records = [
    {
        "uid": i,
        "name": "user_" + str(i),
        "active": True
    }
    for i in range(100000)
]

start = time.time()
json.dumps(records)
print(
    "json cost:",
    time.time() - start
)

start = time.time()
orjson.dumps(records)
print(
    "orjson cost:",
    time.time() - start
)

实际效果和数据结构有关,但大对象序列化这种场景,orjson 通常会明显降低 CPU 消耗。

不过别看到性能库就上。我排查性能问题的时候有个习惯:先确认是不是方向错了。很多优化都是换 JSON 库、换缓存、加机器,最后发现真正的问题是一开始就不该返回那么大的数据。


还有一个容易忽略的问题:JSON 很浪费。比如:

{
    "user_id": 10001,
    "user_name": "dongge",
    "create_time": "2026-01-01 10:20:30"
}

字段名本身也占空间。如果内部服务之间通信,没必要一定用 JSON。比如大量数据同步,可以考虑二进制协议。简单一点:

import pickle

data = {
    "id": 1001,
    "name": "test"
}

binary = pickle.dumps(data)

print(len(binary))

当然,pickle 不适合跨服务开放接口,更不能接收不可信数据。这里只是想说明一个问题:JSON 最大的优势是可读,不是性能。


还有一个场景特别容易踩坑——缓存。很多项目喜欢:

redis.set(
    "user:list",
    json.dumps(users)
)

然后每次读取:

users = json.loads(
    redis.get("user:list")
)

数据量小没问题。数据大了以后,Redis 很快,慢的是 Python。因为每次取缓存,都重新反序列化。

监控里可能看到:

Redis GET: 3ms
接口耗时: 800ms

然后大家开始怀疑 Redis。其实 Redis 已经把数据给你了,CPU 花在 Python 自己身上。


后来我处理这类问题,会先看三个地方:

第一,看返回大小。

curl -w "%{size_download}"

确认接口到底返回多少数据。

第二,看 CPU 火焰图,确认是不是 serializer 占时间。

第三,看代码里有没有隐藏序列化。尤其是这几个地方:

  • 日志
  • 缓存
  • 消息发送
  • 接口包装

这些位置最容易偷偷消耗 CPU。


JSON 没错,它解决的是数据交换问题。只是很多系统发展到一定阶段后,会发现“能传”和“高效传”是两回事。刚开始几十 KB 的接口,用 JSON 很舒服。等数据到了几十 MB、甚至几百 MB,还抱着 JSON 不放,CPU 迟早会告诉你:这个选择当初挺方便,现在开始收费了。

类似的性能排查经验,在云栈社区上也经常有人分享,排查思路值得参考。




上一篇:黄仁勋的AI工厂缺「粮仓」:迅策TokenRouters如何打通企业Token交换
下一篇:Google Genie 4交互式世界模型发布,实时生成开放世界开启游戏影游新范式
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-30 01:15 , Processed in 0.082157 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表