当前位置:首页 > 文章列表 > 文章 > python教程 > Python 内存泄漏排查实战:用 tracemalloc 找到失控引用

Python 内存泄漏排查实战:用 tracemalloc 找到失控引用

来源:Python 博主原创 2026-06-08 18:20:06 0浏览 收藏

Python 服务内存持续上涨时,很多团队第一反应是“GC 没回收”或者“容器限制太小”。实际线上更常见的是:缓存没有上限、全局列表留住对象、闭包或回调链保留引用,导致 RSS 一路爬升。只看进程内存曲线,通常看不到是哪一行代码在增长。

这篇文章按一次生产排障来写:如何判断内存增长是不是 Python 对象泄漏,如何用 tracemalloc 做 snapshot diff,如何结合 gc 和引用链定位缓存/全局变量问题,最后怎样做上线回归。示例适用于 Python 3.12/3.13。

Python 内存泄漏排查思维导图
排查路线:先确认增长趋势,再用 tracemalloc 快照 diff 锁定热点行,最后检查引用和缓存边界。

业务场景:发布后 RSS 每小时涨 300 MB

假设一个 Python API 服务发布后,容器 RSS 从 600 MB 慢慢涨到 2.5 GB。重启能恢复,流量一上来又复现。日志没有异常,CPU 正常,接口延迟在内存接近上限时开始抖动。

这时不要马上改容器内存,也不要只执行 gc.collect()。第一步是确认增长是否可复现,并把排查窗口缩小到“某类请求之后,哪些 Python 分配变多”。

# 典型风险:无上限缓存持续保留对象
user_profile_cache: dict[str, dict] = {}

def get_profile(user_id: str) -> dict:
    if user_id not in user_profile_cache:
        user_profile_cache[user_id] = load_profile(user_id)
    return user_profile_cache[user_id]

这段代码本地压测不一定暴露问题。线上用户 ID 基数大,缓存没有过期策略,字典就会变成进程内常驻对象仓库。

Python 内存增长诊断流程
稳定流程:记录 RSS 曲线、打开 tracemalloc、采集两次快照、对比增长、定位引用、修复后回归。

第一步:尽早打开 tracemalloc

tracemalloc 追踪的是 Python 分配的内存块。它越早启动,看到的分配链路越完整。生产排障时,我通常先在灰度实例或复现环境开启,不直接全量打开。

# app_bootstrap.py
import os
import tracemalloc

if os.getenv("PYTHON_TRACE_MEMORY") == "1":
    tracemalloc.start(25)  # 保留最多 25 层分配调用栈

如果能控制启动参数,也可以用 PYTHONTRACEMALLOC=25-X tracemalloc=25。保留更多 frame 会增加开销,但排查复杂调用链时很有价值。

第二步:用两次快照做 diff

单次快照只能告诉你“现在谁占得多”,不一定说明泄漏。更可靠的是在复现前后各采一张快照,然后比较增量。

# memory_probe.py
import pathlib
import tracemalloc

SNAPSHOT_DIR = pathlib.Path("/tmp/python-memory")
SNAPSHOT_DIR.mkdir(exist_ok=True)

def dump_snapshot(name: str) -> pathlib.Path:
    snapshot = tracemalloc.take_snapshot()
    path = SNAPSHOT_DIR / f"{name}.snap"
    snapshot.dump(path)
    return path

def compare_snapshot(before: pathlib.Path, after: pathlib.Path) -> None:
    snap1 = tracemalloc.Snapshot.load(before)
    snap2 = tracemalloc.Snapshot.load(after)
    for stat in snap2.compare_to(snap1, "lineno")[:20]:
        print(stat)

建议把快照文件写到临时目录,避免在内存紧张时还把大对象保留在进程变量里。分析时优先看 size_diffcount_diff 同时增长的行。

第三步:过滤噪音,盯住业务代码

快照 diff 里经常会出现 import、模板、日志、traceback 缓存等噪音。排查时要过滤掉虚拟环境、标准库和已知框架目录,把注意力放到业务模块。

def business_stats(snapshot):
    return snapshot.filter_traces((
        tracemalloc.Filter(False, ""),
        tracemalloc.Filter(False, "*/site-packages/*"),
        tracemalloc.Filter(False, "*/lib/python*/logging/*"),
    )).statistics("traceback")

如果某个业务文件的某一行持续增长,再切到 statistics("traceback"),看它从哪个入口一路分配过来。只看文件行号容易误判公共工具函数。

Python 内存泄漏风险写法与修复思路
代码审查重点:缓存必须有上限,批处理对象要释放,必要时用弱引用或显式生命周期管理。

第四步:结合 gc 看引用是否还活着

tracemalloc 能告诉你“哪里分配了更多 Python 内存”,但不直接告诉你“谁还引用着对象”。定位到可疑类型后,可以用 gc 做辅助检查。

import gc

def count_live_dicts() -> int:
    gc.collect()
    return sum(1 for obj in gc.get_objects() if isinstance(obj, dict))

def show_referrers(obj) -> None:
    for ref in gc.get_referrers(obj)[:10]:
        print(type(ref), repr(ref)[:200])

gc.get_referrers() 只能在诊断环境谨慎使用,输出里会包含解释器内部对象、当前栈帧和调试器引用。不要把这类函数直接挂到公网接口。

修复:给缓存边界和对象生命周期

如果泄漏来自无上限缓存,优先修缓存策略,而不是定时重启。常见修复包括:functools.lru_cache(maxsize=...)、TTL 缓存、按租户隔离的容量上限、批处理后显式丢弃临时列表。

from functools import lru_cache

@lru_cache(maxsize=512)
def load_profile_cached(user_id: str) -> tuple[str, str]:
    profile = load_profile(user_id)
    return profile["name"], profile["level"]

def process_batch(rows: list[dict]) -> None:
    try:
        for row in rows:
            handle(row)
    finally:
        rows.clear()

不要为了省查询把完整 ORM 对象、请求对象、响应对象直接塞进全局缓存。它们通常带着连接、上下文、回调和大量间接引用。

上线检查清单

  • 确认 RSS 增长可以在灰度或压测环境稳定复现。
  • 在复现前后采集两次 tracemalloc snapshot,并保留分析文件。
  • 使用 compare_to(..., "lineno") 看增量,再用 statistics("traceback") 查调用链。
  • 过滤标准库、site-packages 和已知噪音,优先看业务文件。
  • 检查全局缓存、类变量、模块级列表、闭包、回调注册表和批处理临时对象。
  • 修复后用相同请求集压测,确认 RSS 曲线稳定,快照 diff 不再持续增长。
  • 不要依赖定时重启掩盖泄漏;重启只能作为临时止损。

总结

Python 内存泄漏排查要避免两个误区:只看 RSS 就下结论,以及只调用 gc.collect() 就认为问题解决。真正有效的路径是:复现增长、采集快照、对比增量、定位引用、修复生命周期。

我的经验是,tracemalloc 负责把问题缩小到具体文件和调用链,gc 负责验证对象是否还被引用。把这两件事做扎实,内存泄漏排查就会从猜测变成可验证的工程流程。

版本声明
本文转载于:Python 博主原创 如有侵犯,请联系study_golang@163.com删除
Java 25 Structured Concurrency 实战:别让 CompletableFuture 把超时拖散Java 25 Structured Concurrency 实战:别让 CompletableFuture 把超时拖散
上一篇
Java 25 Structured Concurrency 实战:别让 CompletableFuture 把超时拖散
MySQL 8.4 内部临时表实战:GROUP BY 一慢就先查 TempTable 有没有落盘
下一篇
MySQL 8.4 内部临时表实战:GROUP BY 一慢就先查 TempTable 有没有落盘
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    69次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    227次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    151次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    83次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    63次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码