Python本地cache不当使用导致内存泄露的问题分析与解决

 更新时间:2023年08月30日 10:40:12   作者:及时  
最近在项目开发中遇到了本地cache不当使用导致的一个内存泄露问题,所以本文主要分析了问题出现的原因已经解决方法,需要的小伙伴可以参考下

背景

近期一个大版本上线后,Python编写的api主服务使用内存有较明显上升,服务重启后数小时就会触发机器的90%内存占用告警,分析后发现了本地cache不当使用导致的一个内存泄露问题,这里记录一下分析过程。

问题分析

LocalCache实现分析

该cache大概实现代码如下:

class LocalCache():
    notFound = object() # 定义cache未命中时返回的唯一对象
    # list dict等本身不支持弱引用,但其子类支持,这里包装下
    class Dict(dict):
        def __del__(self):
            pass

    def __init__(self, maxlen=10): # maxlen指定最多缓存的对象个数
        self.weak = weakref.WeakValueDictionary() # 存储缓存对象弱引用的dict
        self.strong = collections.deque(maxlen=maxlen) # 存储缓存对象强引用的deque

    # 从缓存dict中查找对应key的对象,若已过期或不存在则返回notFound
    def get_ex(self, key):
        value = self.weak.get(key, self.notFound)
        if value is not self.notFound:
            expire = value['expire']
            if self.nowTime() > expire:
                return self.notFound
            else:
                return value['result']
        return self.notFound

    # 设置kv到缓存dict中,并设置其过期时间
    def set_ex(self, key, value, expire):
        self.weak[key] = strongRef = LocalCache.Dict({'result': value, 'expire': self.nowTime()+expire})
        self.strong.append(strongRef)

如上述代码,该LocalCache核心在于一个存储弱引用的weakref.WeakValueDictionary对象与存储强引用的deque对象(Python中弱引用与强引用介绍可以参见这篇文章--Python中的弱引用与基础类型支持情况探究 ),LocalCache实例化时可以指定最大缓存的对象个数。使用set_ex方法可以设置新的缓存kv,get_ex则获取指定key的缓存对象,如果key不存在或者已过期则返回notFound。
该LocalCache通过deque在达到maxlen时按先进先出的顺序移除队列元素,而一旦对象的所有强引用被移除后,WeakValueDictionary的特性则保证了对应对象的弱引用也会直接从dict中被移除出去,如此即实现了一个简单的支持过期时间和最大缓存对象数量限制的本地cache。

LocalCache使用占用内存的错误评估

按照上面的LocalCache原则,理论上只要设置合理的过期时间与maxlen值应该可以保证其合理内存的合理使用,而这次新版本发布新增了类似如下两个个LocalCache:

id_local_cache0 = LocalCache(500000)
id_local_cache1 = LocalCache(500000)
id_local_cache0.set_ex('user_id_012345678901', 'display_id_ABCDEFGH', 1800)
id_local_cache1.set_ex('display_id_ABCDEFGH', 'user_id_012345678901', 1800)

如上定义了两个50w大小的cache,其缓存的是业务内部使用的user_id到用户app上可见的display_id的映射关系,该映射关系在用户创建时即生成固定不变,可以设置较长期时间,如果同时有效的对象数超过的maxlen,这个LocalCache直接就等价于一个LRU了,对象释放可以完全依赖deque的先进先出淘汰机制。
在最开始评估其占用内存时考虑了以下因素:

  • 单个k、v对 user_id最多20字节,display_id最多8字节,加上要存入的过期时间float字段8字节,总大小20+8+8=36,加上一些额外花销最多100字节
  • 最大50w限制内存占用: 500000 * 100/1024 = 47.6MB
  • 线上api服务为uWSGI框架提供的多进程运行方式,单机4个worker进程,总占用内存: 47.6 * 4 = 190MB
  • 两个LcoalCache占用内存: 190MB * 2 = 380MB

按照这个计算一台主机即便每个进程都缓存满了50w对象,也就增加不到400MB内存占用,何况按照估算同时处于有效期内的缓存对象应该远小于50w,所以剩余内存应当完全是绰绰有余的,然而这个评估值其实远小于实际值。

LocalCache占用内存的正确评估

线上出现内存问题后,尝试使用tracemalloc分析了线上服务的内存分配情况,发现很多内存都集中于LocalCache这块,于是结合实际重新评估这个内存占用,发现了以下问题:

str与float的内存占用评估错误,即便str本身len只有10个字符,其占用内存其实是远大于10的,而float并不是占用8字节而是24字节,如下代码可验证:

In [20]: len('0123456789')
Out[20]: 10
In [21]: sys.getsizeof('0123456789')
Out[21]: 59
In [23]: sys.getsizeof(time.time())
Out[23]: 24

即便是一个空dict其占用内存也有64字节,而如果存入kv后则更是急速膨胀为至少232:

In [24]: sys.getsizeof({})
Out[24]: 64
In [26]: sys.getsizeof({'result': {'user_id_012345678901': 'display_id_ABCDEFGH'}, 'expire': time.time()})
Out[26]: 232

无论过期时间设置长短,由于存入该cache的对象资源回收完全是依赖于deque对其存入强引用的移除进行--即便对象按照时间已经过期了,但是只要deque中还存有该对象,对象就不会被回收--所以最终cache中缓存的对象一定会达到设置的maxlen,占用其理论上可占用的最大内存。

综合以上几点,虽然开始设置的过期时间较短,LocalCache中同时有效的对象数远小于50w,但最终LocalCache还是会存满50w的对象,同时实测LocalCache中存入一个对象的平均内存大小在700~800字节,这样一评估,最终这两个cache单主机上需要占用的最大且肯定会达到的内存大小变成了: 700 * 500000 * 4 * 2 / 1024/1024 = 2.67GB,是之前错误评估值的6倍==!这样一算主机上的内存就不够用了。

后续处理

结合实际正确评估内存占用后,总结以下LocalCache使用原则:

  • maxlen的设置需根据实际数据情况设置为合理值--如最大可能同时有效对象数的1.1 ~ 2.0倍,防止大量过期对象长期占用内存而不释放的情况,check后确认线上代码就有好几处maxlen大于其最大有效对象数5~10倍的LocalCache使用。
  • 拆分大对象与小对象同时使用的cache,因为占用几百字节的小对象的maxlen设置为1千、1万甚至10w都合理,但是对于占用几MB设置十几MB的对象,maxlen设置>100就已经可能占用掉大量内存了。

针对api服务使用的多处LocalCache按照以上原则进行优化后,其占用的总内存量下降了超过3GB。

总结

在初版评估cache内存占用时,用了想当然评估法,而没有实测每个类型、对象的实际占用大小,导致评估值远小于实际值。
对于LocalCache的对象回收原理未深度理解,一直想当然认为只要过了有效时间其对象即会被回收掉,没有认识到其回收完全依赖于deque。
又一次想当然造成的问题。

到此这篇关于Python本地cache不当使用导致内存泄露的问题分析与解决的文章就介绍到这了,更多相关Python内存泄露内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • python实现对指定输入的字符串逆序输出的6种方法

    python实现对指定输入的字符串逆序输出的6种方法

    这篇文章主要介绍了python实现对指定输入的字符串逆序输出的6种方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • VSCode中Python venv环境加载失败的5大原因及解决方案

    VSCode中Python venv环境加载失败的5大原因及解决方案

    本文详细探讨了VSCode中Python虚拟环境加载失败的常见原因及解决方法,包括解释器选择、环境配置、路径设置、解释器优先级和配置文件管理等方面,通过实际案例和配置示例,提供了从零创建可被VSCode识别的venv环境的实践步骤,需要的朋友可以参考下
    2026-03-03
  • OpenCV形状检测的示例详解

    OpenCV形状检测的示例详解

    本文主要介绍了OpenCV中的形状检测,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • 在Pandas中导入CSV数据时去除默认索引的方法汇总

    在Pandas中导入CSV数据时去除默认索引的方法汇总

    在Pandas中读取CSV数据时,会默认将第一列设为索引列index,但有时候我们并不需要索引,或者希望指定自己的索引列,本文将介绍几种在Pandas中导入CSV数据时去除默认索引的方法,需要的朋友可以参考下
    2023-05-05
  • Python可视化函数plt.scatter详解

    Python可视化函数plt.scatter详解

    这篇文章主要介绍了Python可视化函数plt.scatter详解, 关于matplotlib的scatter函数有许多活动参数,如果不专门注解,是无法掌握精髓的,本文专门针对scatter的参数和调用说起,并配有若干案例,需要的朋友可以参考下
    2023-04-04
  • 浅谈Keras中shuffle和validation_split的顺序

    浅谈Keras中shuffle和validation_split的顺序

    这篇文章主要介绍了浅谈Keras中shuffle和validation_split的顺序,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-06-06
  • pycharm使用技巧之自动调整代码格式总结

    pycharm使用技巧之自动调整代码格式总结

    这篇文章主要给大家介绍了关于pycharm使用技巧之自动调整代码格式总结的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • python3如何将docx转换成pdf文件

    python3如何将docx转换成pdf文件

    这篇文章主要为大家详细介绍了python3如何将docx转换成pdf文件的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • Python中deepcopy的用法及注意事项详细解释

    Python中deepcopy的用法及注意事项详细解释

    这篇文章主要介绍了Python中deepcopy用法及注意事项的相关资料,deepcopy是Python中用于创建对象深拷贝的函数,与浅拷贝不同,它会递归地复制对象及其所有子对象,确保新对象是独立的副本,需要的朋友可以参考下
    2025-04-04
  • 深度学习小工程练习之tensorflow垃圾分类详解

    深度学习小工程练习之tensorflow垃圾分类详解

    这篇文章主要介绍了练习深度学习的一个小工程,代码简单明确,用来作为学习深度学习的练习很适合,对于有需要的朋友可以参考下,希望大家可以体验到深度学习带来的收获
    2021-04-04

最新评论