一次Python本地cache不当使用导致的内存泄露

这篇具有很好参考价值的文章主要介绍了一次Python本地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的先进先出淘汰机制。
在最开始评估其占用内存时考虑了以下因素:

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

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

LocalCache占用内存的正确评估

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

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

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

后续处理

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

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

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

总结

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

转载请注明出处,原文地址: https://www.cnblogs.com/AcAc-t/p/python_local_cache_usage.html

参考

https://docs.python.org/3.8/library/tracemalloc.html
https://www.cnblogs.com/AcAc-t/p/python_weakref_study.html
https://docs.python.org/3.8/library/collections.html#collections.deque
https://www.cnblogs.com/AcAc-t/p/python_local_cache_usage.html
https://docs.python.org/3.8/library/sys.html?highlight=getsizeof文章来源地址https://www.toymoban.com/news/detail-680343.html

到了这里,关于一次Python本地cache不当使用导致的内存泄露的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Go坑:time.After可能导致的内存泄露问题分析

    Go 中 time.After 可能导致的内存泄露 go v1.20.4 time 包中有 3 个比较常用的定时函数:NewTicker,NewTimer 和 time.After: NewTimer : 表示在一段时间后才执行,默认情况下执行一次。如果想再次执行,需要调用 time.Reset() 方法,这时类似于 NewTicker 定时器了。可以调用 stop 方法停止执行。

    2024年02月02日
    浏览(51)
  • 注意避坑!Java 内部类持有外部类会导致内存泄露。。。

    本文介绍 Java 内部类持有外部类导致内存泄露的原因以及其解决方案。 为什么内部类持有外部类会导致内存泄露 非静态内部类会持有外部类,如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类(即使外部类已经没有其他地方在使

    2024年02月09日
    浏览(50)
  • 得物-Golang-记一次线上服务的内存泄露排查

    在风和日丽的一天,本人正看着需求、敲着代码,展望美好的未来。突然收到一条内存使用率过高的告警。 告警的这个项目,老代码是python的,最近一直在go化。随着go化率不断上升,发现内存的RSS使用率越飙越高。最终达到容器内存限制后,进程会自动重启。RSS如下图所示

    2024年02月04日
    浏览(58)
  • .NET 6 在 Win7 系统证书链错误导致 HttpWebRequest 内存泄露

    本文记录我将应用迁移到 dotnet 6 之后,在 Win7 系统上,因为使用 HttpWebRequest 访问一个本地服务,此本地服务开启 https 且证书链在此 Win7 系统上错误,导致应用内存泄露问题。本文记录此问题的原因以及调查过程 核心原因是在 CRYPT32.dll 上的 CertGetCertificateChain 方法存在内存泄

    2024年02月06日
    浏览(47)
  • NumPy使用不当引起的内存泄漏

    以下是一段会引起内存逐步累积的代码: 代码大意 :处理n个user的数据,将每个user的数据按照时间排序,取最早的10条的前三列保存 为了监控性能,我们使用python自带的分析工具tracemalloc跟踪内存分配,使用方法见: Python-tracemalloc-跟踪内存分配_Rnan-prince的博客-CSDN博客 运行

    2024年02月08日
    浏览(34)
  • rocketmq客户端本地日志文件过大调整配置(导致pod缓存cache过高)

            在使用rocketmq时,发现本地项目中文件越来越大,查找发现在/home/root/logs/rocketmqlog目录下存在大量rocketmq_client.log日志文件。 开启slf4j日志模式,在项目启动项中增加-Drocketmq.client.logUseSlf4j=true 因为配置使用的是System.getProperty获取,所以只能使用系统环境配置。 调整日

    2024年02月15日
    浏览(40)
  • 忆一次因SQLServer内存占用飙高导致的工厂停工

           现在回想起来,整件事还挺离谱的......        中午午休,正在公司总部(重庆)附近和同事们一起享受午餐;        突然接到上司电话,要求我立即出发去广州一趟,今天中午有个工厂因为我们的程序出问题导致停工了!!!        我立即反馈,由于我们的程

    2024年02月14日
    浏览(35)
  • python中使用selenium进行爬虫时,导致(内存已缓存)备用内存占用过大导致崩溃问题,3个解决方案

    在使用python进行爬虫的时候,使用selenium进行爬取的时候经常会出现已缓存过大的情况,如果缓存出现过大之后再次执行的话就会计算机拒绝,但是这个时候我们的内存又有很多空间可以使用,一开始我以为是占用文件过多然后点360的那个进行文件整理和清理垃圾,结果效果

    2023年04月08日
    浏览(50)
  • new ArrayList 不当导致 CPU 飙升。。

    来源:juejin.cn/post/7139202066362138654 昨天线上容器突然cpu飙升,也是第一次排查这种问题所以记录一下~ 首先问题是这样的,周五正在写文档,突然收到了线上报警,发现cpu占用达到了90多,上平台监控系统查看容器,在jvm监控中发现有一个pod在两个小时内产生了61次youngGc一次

    2024年02月13日
    浏览(31)
  • Zabbix Timeout 设置不当导致的问题

    哈喽大家好,我是咸鱼 今天跟大家分享一个关于 zabbix Timeout 值设置不当导致的问题,这个问题不知道大家有没有碰到过 事情经过是这样的: 把某一台 zabbix agent 的模板由原来的 Template OS Windows by Zabbix agent 换成了 Template OS Windows by Zabbix agent active Template OS Windows by Zabbix agent

    2024年02月10日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包