当前位置:首页 > 文章列表 > 文章 > 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推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    6923次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    7334次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    7146次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    9078次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    7805次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码