You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:15:42