在Heroku上使用线程出现内存泄漏,如何解决?
解决Flask+Heroku线程导致的内存超限问题
针对你遇到的R14内存超限错误,核心问题在于全局数据未及时清理和线程资源未妥善回收,以下是具体优化方案:
1. 清理全局字典中的用户残留数据
你的global_dict会存储每个用户的运行结果,多次请求后旧数据会持续占用内存。在/result路由处理完成后,必须主动删除对应用户的条目:
@app.route('/result') def result(): userId = int(session['user_id']) try: # 先获取数据再清理,避免渲染时找不到数据 user_data = global_dict[userId] # 等待线程结束 user_data['t1'].join() user_data['t2'].join() # 渲染模板 return render_template("built.html", num_small=user_data['num_small'], plot_json_weights_min_vol_long=user_data['plot_json_weights_min_vol_long'], # 其他参数按原逻辑传递... leftover_cvar=user_data['leftover_cvar']) except Exception as e: try: user_data['t1'].join() user_data['t2'].join() return_error = str(user_data['error']) flash(return_error) return redirect("/build") except: user_data['t1'].join() user_data['t2'].join() return redirect("/build") finally: # 无论成功失败,都清理全局字典中的用户数据 if userId in global_dict: del global_dict[userId]
2. 避免使用全局线程变量
当前用global t1、global t2存储线程对象,每次新请求会覆盖旧对象,但旧线程的引用可能仍未被GC回收。建议把线程对象存在global_dict的用户条目里:
@app.route("/build",methods=["GET", "POST"]) @login_required def build(): if request.method == "POST": user_id = int(session['user_id']) # 初始化用户的全局条目,包含线程和状态 global_dict[user_id] = {'finished': 'False'} @copy_current_request_context def operation(session): # 你的业务逻辑... # 完成后标记状态 global_dict[user_id]['finished'] = 'True' t1 = Thread(target=operation, args=[session], name = f"{user_id}_operation_thread") t1.start() global_dict[user_id]['t1'] = t1 @copy_current_request_context def enter_sql_data(nasdaq_exchange_info, tickers): # 你的SQL逻辑... t2 = Thread(target=enter_sql_data, args=[nasdaq_exchange_info, tickers], name=f"{user_id}_sql_thread") t2.start() global_dict[user_id]['t2'] = t2 return render_template("loading.html")
3. 线程内主动释放大内存对象
检查operation和enter_sql_data函数,如果你在里面创建了大的数据集(比如Pandas DataFrame)、绘图对象或缓存数据,处理完成后手动删除这些变量,让GC及时回收内存:
def operation(session): # 示例:创建大对象 large_df = pd.read_csv('big_data.csv') # 业务处理逻辑... # 处理完成后删除大对象 del large_df # 标记任务完成 global_dict[int(session['user_id'])]['finished'] = 'True'
4. 改用线程池复用线程
频繁创建销毁线程会带来额外内存开销,用concurrent.futures.ThreadPoolExecutor复用线程,控制并发数:
from concurrent.futures import ThreadPoolExecutor # 初始化线程池,根据Heroku dyno规格设置线程数(建议2-4) executor = ThreadPoolExecutor(max_workers=2) @app.route("/build",methods=["GET", "POST"]) @login_required def build(): if request.method == "POST": user_id = int(session['user_id']) global_dict[user_id] = {'finished': 'False'} @copy_current_request_context def operation(session): # 业务逻辑... global_dict[user_id]['finished'] = 'True' # 提交任务到线程池 future1 = executor.submit(operation, session) global_dict[user_id]['future1'] = future1 @copy_current_request_context def enter_sql_data(nasdaq_exchange_info, tickers): # SQL逻辑... future2 = executor.submit(enter_sql_data, nasdaq_exchange_info, tickers) global_dict[user_id]['future2'] = future2 return render_template("loading.html") # 在/result路由中等待任务完成 @app.route('/result') def result(): userId = int(session['user_id']) try: user_data = global_dict[userId] # 等待线程池任务完成 user_data['future1'].result() user_data['future2'].result() # 渲染模板... except Exception as e: try: user_data['future1'].result() user_data['future2'].result() return_error = str(user_data['error']) flash(return_error) return redirect("/build") except: user_data['future1'].result() user_data['future2'].result() return redirect("/build") finally: del global_dict[userId]
5. 可选:优化Heroku资源配置
如果代码优化后仍内存不足,可以考虑升级Heroku的dyno规格(比如从Hobby升级到Standard),但这是付费方案,优先通过代码优化解决。
内容的提问来源于stack exchange,提问作者originn
相关产品推荐
相关产品推荐

