最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

一次Python本地cache不當(dāng)使用導(dǎo)致的內(nèi)存泄露

2023-08-30 10:23 作者:程序員-王堅(jiān)  | 我要投稿

背景

近期一個(gè)大版本上線后,Python編寫(xiě)的api主服務(wù)使用內(nèi)存有較明顯上升,服務(wù)重啟后數(shù)小時(shí)就會(huì)觸發(fā)機(jī)器的90%內(nèi)存占用告警,分析后發(fā)現(xiàn)了本地cache不當(dāng)使用導(dǎo)致的一個(gè)內(nèi)存泄露問(wèn)題,這里記錄一下分析過(guò)程。

問(wèn)題分析

LocalCache實(shí)現(xiàn)分析

該cache大概實(shí)現(xiàn)代碼如下:

class LocalCache(): ? ?notFound = object() # 定義cache未命中時(shí)返回的唯一對(duì)象 ? ?# list dict等本身不支持弱引用,但其子類(lèi)支持,這里包裝下 ? ?class Dict(dict): ? ? ? ?def __del__(self): ? ? ? ? ? ?pass ? ?def __init__(self, maxlen=10): # maxlen指定最多緩存的對(duì)象個(gè)數(shù) ? ? ? ?self.weak = weakref.WeakValueDictionary() # 存儲(chǔ)緩存對(duì)象弱引用的dict ? ? ? ?self.strong = collections.deque(maxlen=maxlen) # 存儲(chǔ)緩存對(duì)象強(qiáng)引用的deque ? ?# 從緩存dict中查找對(duì)應(yīng)key的對(duì)象,若已過(guò)期或不存在則返回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 ? ?# 設(shè)置kv到緩存dict中,并設(shè)置其過(guò)期時(shí)間 ? ?def set_ex(self, key, value, expire): ? ? ? ?self.weak[key] = strongRef = LocalCache.Dict({'result': value, 'expire': self.nowTime()+expire}) ? ? ? ?self.strong.append(strongRef)

如上述代碼,該LocalCache核心在于一個(gè)存儲(chǔ)弱引用的weakref.WeakValueDictionary對(duì)象與存儲(chǔ)強(qiáng)引用的deque對(duì)象(Python中弱引用與強(qiáng)引用介紹可以參見(jiàn)這篇文章--Python中的弱引用與基礎(chǔ)類(lèi)型支持情況探究?),LocalCache實(shí)例化時(shí)可以指定最大緩存的對(duì)象個(gè)數(shù)。使用set_ex方法可以設(shè)置新的緩存kv,get_ex則獲取指定key的緩存對(duì)象,如果key不存在或者已過(guò)期則返回notFound。
該LocalCache通過(guò)deque在達(dá)到maxlen時(shí)按先進(jìn)先出的順序移除隊(duì)列元素,而一旦對(duì)象的所有強(qiáng)引用被移除后,WeakValueDictionary的特性則保證了對(duì)應(yīng)對(duì)象的弱引用也會(huì)直接從dict中被移除出去,如此即實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的支持過(guò)期時(shí)間和最大緩存對(duì)象數(shù)量限制的本地cache。

LocalCache使用占用內(nèi)存的錯(cuò)誤評(píng)估

按照上面的LocalCache原則,理論上只要設(shè)置合理的過(guò)期時(shí)間與maxlen值應(yīng)該可以保證其合理內(nèi)存的合理使用,而這次新版本發(fā)布新增了類(lèi)似如下兩個(gè)個(gè)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)

如上定義了兩個(gè)50w大小的cache,其緩存的是業(yè)務(wù)內(nèi)部使用的user_id到用戶app上可見(jiàn)的display_id的映射關(guān)系,該映射關(guān)系在用戶創(chuàng)建時(shí)即生成固定不變,可以設(shè)置較長(zhǎng)期時(shí)間,如果同時(shí)有效的對(duì)象數(shù)超過(guò)的maxlen,這個(gè)LocalCache直接就等價(jià)于一個(gè)LRU了,對(duì)象釋放可以完全依賴(lài)deque的先進(jìn)先出淘汰機(jī)制。
在最開(kāi)始評(píng)估其占用內(nèi)存時(shí)考慮了以下因素:

  1. 單個(gè)k、v對(duì) user_id最多20字節(jié),display_id最多8字節(jié),加上要存入的過(guò)期時(shí)間float字段8字節(jié),總大小20+8+8=36,加上一些額外花銷(xiāo)最多100字節(jié)

  2. 最大50w限制內(nèi)存占用: 500000 * 100/1024 = 47.6MB

  3. 線上api服務(wù)為uWSGI框架提供的多進(jìn)程運(yùn)行方式,單機(jī)4個(gè)worker進(jìn)程,總占用內(nèi)存: 47.6 * 4 = 190MB

  4. 兩個(gè)LcoalCache占用內(nèi)存: 190MB * 2 = 380MB

按照這個(gè)計(jì)算一臺(tái)主機(jī)即便每個(gè)進(jìn)程都緩存滿了50w對(duì)象,也就增加不到400MB內(nèi)存占用,何況按照估算同時(shí)處于有效期內(nèi)的緩存對(duì)象應(yīng)該遠(yuǎn)小于50w,所以剩余內(nèi)存應(yīng)當(dāng)完全是綽綽有余的,然而這個(gè)評(píng)估值其實(shí)遠(yuǎn)小于實(shí)際值。

LocalCache占用內(nèi)存的正確評(píng)估

線上出現(xiàn)內(nèi)存問(wèn)題后,嘗試使用tracemalloc分析了線上服務(wù)的內(nèi)存分配情況,發(fā)現(xiàn)很多內(nèi)存都集中于LocalCache這塊,于是結(jié)合實(shí)際重新評(píng)估這個(gè)內(nèi)存占用,發(fā)現(xiàn)了以下問(wèn)題:

  1. str與float的內(nèi)存占用評(píng)估錯(cuò)誤,即便str本身len只有10個(gè)字符,其占用內(nèi)存其實(shí)是遠(yuǎn)大于10的,而float并不是占用8字節(jié)而是24字節(jié),如下代碼可驗(yàn)證:

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

  1. 即便是一個(gè)空dict其占用內(nèi)存也有64字節(jié),而如果存入kv后則更是急速膨脹為至少232:

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

  1. 無(wú)論過(guò)期時(shí)間設(shè)置長(zhǎng)短,由于存入該cache的對(duì)象資源回收完全是依賴(lài)于deque對(duì)其存入強(qiáng)引用的移除進(jìn)行--即便對(duì)象按照時(shí)間已經(jīng)過(guò)期了,但是只要deque中還存有該對(duì)象,對(duì)象就不會(huì)被回收--所以最終cache中緩存的對(duì)象一定會(huì)達(dá)到設(shè)置的maxlen,占用其理論上可占用的最大內(nèi)存。

綜合以上幾點(diǎn),雖然開(kāi)始設(shè)置的過(guò)期時(shí)間較短,LocalCache中同時(shí)有效的對(duì)象數(shù)遠(yuǎn)小于50w,但最終LocalCache還是會(huì)存滿50w的對(duì)象,同時(shí)實(shí)測(cè)LocalCache中存入一個(gè)對(duì)象的平均內(nèi)存大小在700~800字節(jié),這樣一評(píng)估,最終這兩個(gè)cache單主機(jī)上需要占用的最大且肯定會(huì)達(dá)到的內(nèi)存大小變成了: 700 * 500000 * 4 * 2 / 1024/1024 = 2.67GB,是之前錯(cuò)誤評(píng)估值的6倍==!這樣一算主機(jī)上的內(nèi)存就不夠用了。

后續(xù)處理

結(jié)合實(shí)際正確評(píng)估內(nèi)存占用后,總結(jié)以下LocalCache使用原則:

  1. maxlen的設(shè)置需根據(jù)實(shí)際數(shù)據(jù)情況設(shè)置為合理值--如最大可能同時(shí)有效對(duì)象數(shù)的1.1 ~ 2.0倍,防止大量過(guò)期對(duì)象長(zhǎng)期占用內(nèi)存而不釋放的情況,check后確認(rèn)線上代碼就有好幾處maxlen大于其最大有效對(duì)象數(shù)5~10倍的LocalCache使用。

  2. 拆分大對(duì)象與小對(duì)象同時(shí)使用的cache,因?yàn)檎加脦装僮止?jié)的小對(duì)象的maxlen設(shè)置為1千、1萬(wàn)甚至10w都合理,但是對(duì)于占用幾MB設(shè)置十幾MB的對(duì)象,maxlen設(shè)置>100就已經(jīng)可能占用掉大量?jī)?nèi)存了。

針對(duì)api服務(wù)使用的多處LocalCache按照以上原則進(jìn)行優(yōu)化后,其占用的總內(nèi)存量下降了超過(guò)3GB。

總結(jié)

在初版評(píng)估cache內(nèi)存占用時(shí),用了想當(dāng)然評(píng)估法,而沒(méi)有實(shí)測(cè)每個(gè)類(lèi)型、對(duì)象的實(shí)際占用大小,導(dǎo)致評(píng)估值遠(yuǎn)小于實(shí)際值。
對(duì)于LocalCache的對(duì)象回收原理未深度理解,一直想當(dāng)然認(rèn)為只要過(guò)了有效時(shí)間其對(duì)象即會(huì)被回收掉,沒(méi)有認(rèn)識(shí)到其回收完全依賴(lài)于deque。
又一次想當(dāng)然造成的問(wèn)題。


一次Python本地cache不當(dāng)使用導(dǎo)致的內(nèi)存泄露的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
彭水| 东丽区| 谢通门县| 开封市| 玛曲县| 阿尔山市| 平远县| 通渭县| 漳平市| 乐昌市| 固镇县| 正安县| 喀什市| 竹溪县| 柯坪县| 当雄县| 湘潭县| 青浦区| 江华| 呼玛县| 阜康市| 千阳县| 高青县| 榆中县| 申扎县| 台南市| 万宁市| 文山县| 新巴尔虎左旗| 南平市| 文山县| 灵璧县| 桂平市| 兰西县| 石家庄市| 浪卡子县| 福清市| 沂水县| 厦门市| 工布江达县| 长白|