那是一个再普通不过的周五下午监控突然报警服务内存持续上涨已经逼近容器上限。我第一反应是“又有人写死循环了”但日志一切正常QPS平稳接口响应也没变慢。唯独内存像漏水的桶只涨不跌。重启服务内存回落。可跑了两小时又涨回去了。这不是偶发是必然。排查从tracemalloc到objgraph我先用tracemalloc抓快照对比两个时间点的内存分配。结果指向一个全局字典_cache它的大小在持续增长。这个字典是我半年前写的“本地缓存”用来存用户会话对象。python复制下载_cache {} def get_user(user_id): if user_id not in _cache: _cache[user_id] User(user_id) return _cache[user_id]问题很明显只增不减。但奇怪的是我明明设置了过期时间后台线程每10分钟清理一次。为什么没生效我打开清理逻辑python复制下载def clean_cache(): now time.time() for uid, user in _cache.items(): if now user.last_active 600: del _cache[uid]逻辑没错。但为什么user.last_active一直没更新我翻到User类python复制下载class User: def __init__(self, uid): self.uid uid self.last_active time.time() self.session Session(self) # 关键User持有一个Session而Session又反过来持有Userpython复制下载class Session: def __init__(self, user): self.user user循环引用但这还不是最致命的。真正的问题是Session在别处被另一个全局列表_active_sessions引用了而那个列表从未清理。于是User被Session引用Session被全局列表引用整个对象链永远无法回收。del _cache[uid]只是删掉了字典里的引用但_active_sessions里的引用还在引用计数不为零GC也不会回收因为它们是可达的。解决弱引用与显式清理我做了两件事。第一把_active_sessions改成weakref.WeakSetpython复制下载import weakref _active_sessions weakref.WeakSet()这样Session被弱引用当_cache中删除User后User不再被强引用Session也随之被回收。第二在clean_cache里显式断开循环python复制下载user.session.user None del _cache[uid]双管齐下内存曲线终于平了。彻悟Python引用的三层真相这次排查让我彻底理解了Python引用机制。第一层引用计数是主力。每个对象记录被引用次数归零即回收。del只是删除一个引用不是删除对象。如果还有其他引用对象依然存活。第二层循环引用需要GC。引用计数无法处理互相引用的对象。Python的垃圾回收器会定期扫描标记并清除不可达的循环。但GC不是实时的也不是万能的。如果循环对象被全局变量引用它们始终“可达”GC也救不了。第三层弱引用是解药。weakref不增加引用计数让对象可以被回收。缓存、观察者模式、父子引用中弱引用能有效避免内存泄漏。以前我总觉得“Python自动管理内存不用操心”。现在才明白自动管理的前提是你得理解它的规则。引用在哪里对象就在哪里。引用不断内存不还。那次之后我养成了习惯写全局缓存必用弱引用写循环引用必显式断开上线前必用objgraph看一眼对象增长。内存泄漏不可怕可怕的是不懂引用还怪Python。