記一次django內(nèi)存異常排查及解決方法
起因
Django 作為 Python著名的Web框架,相信很多人都在用,自己工作中也有項(xiàng)目項(xiàng)目在用,而在最近幾天的使用中發(fā)現(xiàn),部署Django程序的服務(wù)器出現(xiàn)了內(nèi)存問題,現(xiàn)象就是運(yùn)行一段時(shí)間之后,內(nèi)存占用非常高,最終會把服務(wù)器的內(nèi)存耗盡,對于Python項(xiàng)目出現(xiàn)內(nèi)存問題,自己之前處理過一次,所以并沒有第一次解決時(shí)的慌張,自己之前把解決方法也整理了:http://www.dbjr.com.cn/article/151604.htm
但是事情似乎并沒有我想的那么簡單,自己嘗試用之前的的方法tracemalloc庫進(jìn)行問題的排查,但是問題來了實(shí)際的項(xiàng)目中有快一百多個(gè)接口,怎么排查?難道一個(gè)一個(gè)接口進(jìn)行測試排查,但是時(shí)間又比較緊急,可能又來不及了。對比上次自己解決是因?yàn)樯洗蔚捻?xiàng)目比較簡單,相對來說定位問題比較容易,那么這次怎么處理呢?
處理過程
一般Python項(xiàng)目其實(shí)是很少出現(xiàn)內(nèi)存問題的,一般都是自己代碼寫的有問題導(dǎo)致的,而對于這次出現(xiàn)的問題,自己的排查思路(對于web 接口類型的項(xiàng)目):
- 先排查調(diào)用比較頻繁的接口
- 然后排查數(shù)據(jù)匯總接口(查詢比較復(fù)雜)
- 如果上述還沒有查出來,再排查剩余的接口
在這次的問題排查中,自己大致也是按照這個(gè)思路進(jìn)行的,在對調(diào)用頻繁的接口進(jìn)行排查時(shí),并沒有發(fā)現(xiàn)內(nèi)存的異常,而出現(xiàn)內(nèi)存的問題則是在數(shù)據(jù)匯總的相關(guān)接口上。
其實(shí)這種接口對于初級開發(fā)可能是容易出問題的地方,首先這種接口查詢的數(shù)據(jù)相對其他接口會比較復(fù)雜,如果編碼基礎(chǔ)又不是特別好,可能就會在這些接口上出現(xiàn)bug.
而在這次的排查中,最終確定是在一個(gè)匯總數(shù)據(jù)的接口上,定位到問題處在了Django ORM 使用不當(dāng)導(dǎo)致的。自己通過一個(gè)簡單代碼實(shí)例來說明:
class Student(models.Model): name = models.CharField(max_length=20) name2 = models.CharField(max_length=20) name3 = models.CharField(max_length=20) name4 = models.CharField(max_length=20) name5 = models.CharField(max_length=20) name6 = models.CharField(max_length=20) name7 = models.CharField(max_length=20) name8 = models.CharField(max_length=20) name9 = models.CharField(max_length=20) name10 = models.CharField(max_length=20) name11 = models.CharField(max_length=20) name12 = models.CharField(max_length=20) name13 = models.CharField(max_length=20) name14 = models.CharField(max_length=20) name15 = models.CharField(max_length=20) age = models.IntegerField(default=0)
正常情況,我們的表字段會比較多,這里就通過多個(gè)name來模擬,出現(xiàn)題的代碼就出在關(guān)于這個(gè)表的接口上:
def index(request): studets = Student.objects.filter(age__gt=20) if studets: pass return HttpResponse("test memory")
為了讓內(nèi)存問題容易復(fù)現(xiàn),我通過腳本向Student中插入了20000條數(shù)據(jù),當(dāng)然這里數(shù)據(jù)越多,問題越明顯
通過一個(gè)測試腳本并發(fā)請求這個(gè)接口,觀察內(nèi)存情況,你會發(fā)現(xiàn),內(nèi)存會出現(xiàn)瞬間上漲的情況,并且如果你的數(shù)據(jù)越多,請求越多,你的內(nèi)存可能會在一段時(shí)間居高不下,并且逐漸上漲。問題出在哪里了?
其實(shí)很簡單,問題出在了代碼中的if 判斷那里,我們通過filter 查詢返回的是QuerySet 類型的數(shù)據(jù),而我們過濾之后的數(shù)據(jù)可能會存在非常多的時(shí)候,這個(gè)時(shí)候我們通過if 直接判斷,自己的理解這個(gè)地方會將整個(gè)QuerySet加載到內(nèi)存中,從而出現(xiàn)內(nèi)存占用過高的問題,而如果并且這個(gè)時(shí)候這個(gè)接口的響應(yīng)速度也是非常會變慢,而這個(gè)QuerySet 中的數(shù)據(jù)越多,內(nèi)存占用越明顯。
在Django的文檔中其實(shí)做了說明
exists()¶
Returns True if the QuerySet contains any results, and False if not. This tries to perform the query in the simplest and fastest way possible, but it does execute nearly the same query as a normal QuerySet query.
exists() is useful for searches relating to both object membership in a QuerySet and to the existence of any objects in a QuerySet, particularly in the context of a large QuerySet.
The most efficient method of finding whether a model with a unique field (e.g. primary_key) is a member of a QuerySet is:
entry = Entry.objects.get(pk=123) if some_queryset.filter(pk=entry.pk).exists(): print("Entry contained in queryset")
Which will be faster than the following which requires evaluating and iterating through the entire queryset:
if entry in some_queryset: print("Entry contained in QuerySet")
And to find whether a queryset contains any items:
if some_queryset.exists(): print("There is at least one object in some_queryset")
Which will be faster than:
if some_queryset: print("There is at least one object in some_queryset")
… but not by a large degree (hence needing a large queryset for efficiency gains).
Additionally, if a some_queryset has not yet been evaluated, but you know that it will be at some point, then using some_queryset.exists() will do more overall work (one query for the existence check plus an extra one to later retrieve the results) than using bool(some_queryset), which retrieves the results and then checks if any were returned.
所以對于我們的代碼我們只需要把if 判斷地方改成if not studets.exists() 就可以解決問題。
這是一個(gè)很小的知識點(diǎn),但是如果使用不對,可能就會造成非常嚴(yán)重的內(nèi)存問題。
總結(jié)
除了單元測試,還需要做大數(shù)據(jù)量測試,這次的問題如果在測試的時(shí)候做過一定數(shù)據(jù)量的測試,可能很早就能及時(shí)發(fā)現(xiàn)
問題
對于基礎(chǔ)的庫的使用要更加熟悉
排查問題的思路要明確,不然可能會無從下手
延伸閱讀
- https://docs.djangoproject.com/en/3.0/ref/models/querysets/
- http://www.dbjr.com.cn/article/151604.htm
到此這篇關(guān)于django內(nèi)存異常排查及解決方法的文章就介紹到這了,更多相關(guān)django內(nèi)存異常排查內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決springboot yml配置 logging.level 報(bào)錯(cuò)問題
今天小編就為大家分享一篇解決springboot yml配置 logging.level 報(bào)錯(cuò)問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02python獲取微信企業(yè)號打卡數(shù)據(jù)并生成windows計(jì)劃任務(wù)
由于公司的系統(tǒng)用的是Java版本,開通了企業(yè)號打卡之后又沒有預(yù)算讓供應(yīng)商做數(shù)據(jù)對接,所以只能自己搗鼓這個(gè),以下是個(gè)人設(shè)置的一些內(nèi)容,僅供大家參考2019-04-04matplotlib實(shí)現(xiàn)數(shù)據(jù)實(shí)時(shí)刷新的示例代碼
這篇文章主要介紹了matplotlib實(shí)現(xiàn)數(shù)據(jù)實(shí)時(shí)刷新的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Python學(xué)習(xí)之文件的創(chuàng)建與寫入詳解
本文主要介紹了Python中關(guān)于文件的處理,即如何創(chuàng)建、讀寫一個(gè)文件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-03-03python中pandas nlargest()的詳細(xì)用法小結(jié)
df.nlargest()是一個(gè)DataFrame的方法,用于返回DataFrame中最大的n個(gè)值所在的行,通過調(diào)用nlargest()方法,我們返回了分?jǐn)?shù)最高的三個(gè)行,并按照降序排列,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2023-10-10Python多線程編程(三):threading.Thread類的重要函數(shù)和方法
這篇文章主要介紹了Python多線程編程(三):threading.Thread類的重要函數(shù)和方法,本文講解了線程名稱、join方法、setDaemon方法等內(nèi)容,需要的朋友可以參考下2015-04-04python實(shí)現(xiàn)selenium截圖的兩種方法
本文主要介紹了python實(shí)現(xiàn)selenium截圖的兩種方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04