Flask-SQLAlchemy内存泄露问题

Flask-SQLAlchemy内存泄露问题

现象及问题

Flask应用异步子线程跑批时,每次循环到400次左右时就会有1个workergunicorn启动4个worker)不明原因地发生重启,supervisor和应用无异常日志,CPU、内存占用在运行过程中持续升高,分别涨到110%和90%时worker重启,才回落。如果批次数据不大,跑完之后虽然worker不会重启,但是内存却一直没有释放,占用70%(正常20%)。

初步怀疑是内存泄露,检查循环的代码块,重点语句依次注释后压测发现问题是由于循环中的数据库更新操作导致的。问题代码块中每个循环内会执行一次数据库单条记录的update操作,并commit()

示例代码:

for i in range(1000):
    try:
        rt = Test.query.filter(Test.a == str(i)).update({Test.b: str(i + 1)})
        db.session.commit()
    except Exception as e:
        db.session.rollback()

解决方案

解决SQLAlchemy内存释放问题,可以使用手工释放的方式,执行db.session.close()方法(Flask-SQLAlchemy执行close不会断开连接,只是把连接放回连接池)。

for i in range(1000):
    try:
        rt = Test.query.filter(Test.a == str(i)).update({Test.b: str(i + 1)})
        db.session.commit()
        db.session.close()
    except Exception as e:
        db.session.rollback()

重新跑批时,cpu占用从有问题的110%降到平稳的个位数,内存占用从有问题的90%降到平稳的20%,再也不担心worker挂掉了。

原理解析

web应用在每次接受到请求时会通过中心化的工厂创建当前线程的scoped_session(线程安全的session),请求过程中使用该session完成与数据库之间的交互,请求结束时自动销毁session(释放内存)。注意,session可以包含多个事务。

对session的生命周期有了一定了解之后,回到问题,commit和close对内存处理的差别在哪?在commit()的源码中,也可以看到调用了session.close()为什么还是会导致内存占用。为了对比二者的区别,接下来从官网和源码逐个分析比较。

Session.commit()

官网介绍

Flush pending changes and commit the current transaction.

By default, the Session also expires all database loaded state on all ORM-managed attributes after transaction commit. This so that subsequent operations load the most recent data from the database. This behavior can be disabled using the expire_on_commit=False option to sessionmaker or the Session constructor.

源码:


总结:commit会将数据库的操作同步到数据库中执行,并且在expire_on_commit为True时,使当前session所有ORM管理的变量过期。问题就是这个过期,并不会实际全部清理掉session中的内存。

Session.close()

Close out the transactional resources and ORM objects used by this Session.

This expunges all ORM objects associated with this Session, ends any transaction in progress and releases any Connection objects which this Session itself has checked out from associated Engine objects. The operation then leaves the Session in a state which it may be used again.

源码:
在这里插入图片描述

总结:session的close方法主要是调用expunge_all()方法并且清理事务,expunge_all()方法会清理全部该session使用过的ORM对象,实际地释放session的内存。close()方法不会阻止该session再次被使用。

总结

总而言之,方法很简单,但是很好用。session的生命周期与web请求的生命周期基本相同。对于长生命周期的session对象,在循环中或者大批量的查询之后提交session事务并不能实际释放内存,commit只是将ORM对象过期,要想实际地释放内存,需要显式调用session.close()方法或者expunge_all()方法。

我们日常开发中容易误以为session提交之后,会理所当然地以为SQLAlchemy帮我们释放这部分的内存,可实际上它只是让它失效,并没有实际地释放,在循环中导致内存和CPU上涨,执行效率变慢。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值