Flask-SQLAlchemy内存泄漏排查:无限循环生成题库致内存耗尽
首先明确:这不是无限循环本身的问题,无限循环只是让内存泄漏的症状被放大——因为程序长期运行,内存缓慢累积直到触发上限崩溃。从你给出的内存追踪数据来看,每次循环内存净增长约5000字节,根源是ORM会话的对象缓存和一些代码细节导致的内存无法被回收。
核心问题原因
SQLAlchemy会话的对象累积
你使用的ORM(看起来是SQLAlchemy)的db.session会默认缓存所有通过query加载的对象(存在identity map中),哪怕你执行了db.session.commit(),这些对象依然会被会话持有引用,无法被Python的垃圾回收(GC)机制回收。每次循环都会加载新的Problem、Skill对象,日积月累就会导致内存持续上涨。冗余查询与不必要的对象加载
- 你多次重复查询同一个固定对象(比如
Skill.query.filter_by(id=1).first()、Skill.query.filter_by(skillName="generated").first()),每次查询都会把对象加入会话缓存,完全没必要重复查询。 - 检查问题是否存在时,你用了
Problem.query.filter_by(question=prob).first() == None,这会把整个Problem对象加载到内存里,而实际上只需要判断存在性,不需要加载完整对象。
- 你多次重复查询同一个固定对象(比如
tracemalloc的追踪数据累积
你全程开启了tracemalloc.start(),它会记录所有内存分配的快照数据,长期运行这些数据本身也会占用大量内存,如果只是调试用,调试完成后应该关闭或者定期清理。异常处理的小bug
代码里myfile.write(sys.exc_value)是错误的,sys.exc_value在Python3中已经被废弃,应该用str(e),而且这个错误可能会导致异常信息无法正确写入,甚至引发新的异常。
具体修复步骤
1. 定期清理ORM会话
在每次generate_problem函数执行完成后,调用db.session.remove()来清理会话的identity map,释放对象引用,让GC可以回收内存:
def generate_problem(gen_id): # ... 原有代码 ... db.session.commit() print("2: " + str(tracemalloc.get_traced_memory()[0])) # 新增:清理会话 db.session.remove() return prob, ans, generator_name else: # 即使问题已存在,也要清理会话 db.session.remove() return "Problem already exists", "N/A", "N/A"
2. 优化查询逻辑,减少不必要的对象加载
- 提前缓存固定的Skill对象,避免重复查询:
# 在auto_generate函数外或者程序启动时提前查询一次 skill_id1 = Skill.query.filter_by(id=1).first() skill_generated = Skill.query.filter_by(skillName="generated").first() def generate_problem(gen_id): poster = User.query.filter_by().first() gen_list = mathgen.getGenList() prob, ans = mathgen.genById(gen_id) print("1: " + str(tracemalloc.get_traced_memory()[0])) generator_name = gen_list[gen_id][1] # 用exists()替代first() == None,只判断存在性,不加载完整对象 if not Problem.query.filter_by(question=prob).exists(): Problem.create( question=prob, answer=ans, poster_id=poster.id, correctnessRating=1, sortRating=1, difficultyLevel=None, expectedTime=None, hasSolution=True, ) thisProb = Problem.query.filter_by(question=prob).first() # 使用提前缓存的对象 thisProb.skills.append(skill_id1) # 检查generator_name对应的Skill是否存在 generator_skill = Skill.query.filter_by(skillName=generator_name).first() if not generator_skill: generator_skill = Skill.create(skillName=generator_name) thisProb.skills.append(generator_skill) thisProb.skills.append(skill_generated) db.session.commit() print("2: " + str(tracemalloc.get_traced_memory()[0])) db.session.remove() return prob, ans, generator_name else: db.session.remove() return "Problem already exists", "N/A", "N/A"
3. 调整tracemalloc的使用
如果只是调试内存,调试完成后记得关闭追踪:
def auto_generate(): tracemalloc.start() try: while True: # 用while True替代while 0==0,更易读 # ... 原有循环代码 ... finally: tracemalloc.stop() # 调试完成后关闭追踪
如果需要长期追踪,可以定期清理追踪数据:
# 每N次循环清理一次,比如每100次 loop_count = 0 while True: loop_count +=1 # ... 原有代码 ... if loop_count % 100 == 0: tracemalloc.clear_traces()
4. 修复异常处理的bug
import datetime # 需要导入datetime模块 # ... 原有代码 ... except Exception as e: with open("gen_errors.txt", "a") as myfile: # 写入时间戳和异常信息,方便排查 myfile.write(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: {str(e)}\n")
验证修复效果
修复后,你可以重新用tracemalloc追踪内存变化,应该会看到内存不再持续上涨,而是在一个稳定的区间内波动(因为GC会回收不再使用的对象)。如果还有小幅度增长,可以进一步检查是否有其他未释放的资源,比如文件句柄、第三方库的缓存等,但以上修复应该能解决大部分问题。
内容的提问来源于stack exchange,提问作者lukew3

