1. 程式人生 > >一次django記憶體異常排查

一次django記憶體異常排查

## 起因 `Django` 作為 `Python`著名的`Web`框架,相信很多人都在用,自己工作中也有專案專案在用,而在最近幾天的使用中發現,部署`Django`程式的伺服器出現了記憶體問題,現象就是執行一段時間之後,記憶體佔用非常高,最終會把伺服器的記憶體耗盡,對於`Python`專案出現記憶體問題,自己之前處理過一次,所以並沒有第一次解決時的慌張,自己之前把解決方法也整理了部落格:https://www.cnblogs.com/zhaof/p/10031945.html 但是事情似乎並沒有我想的那麼簡單,自己嘗試用之前的的方法`tracemalloc`庫進行問題的排查,但是問題來了實際的專案中有快一百多個介面,怎麼排查?難道一個一個介面進行測試排查,但是時間又比較緊急,可能又來不及了。對比上次自己解決是因為上次的專案比較簡單,相對來說定位問題比較容易,那麼這次怎麼處理呢? ## 處理過程 一般`Python`專案其實是很少出現記憶體問題的,一般都是自己程式碼寫的有問題導致的,而對於這次出現的問題,自己的排查思路(對於web 介面型別的專案): 1. 先排查呼叫比較頻繁的介面 2. 然後排查資料彙總介面(查詢比較複雜) 3. 如果上述還沒有查出來,再排查剩餘的介面 在這次的問題排查中,自己大致也是按照這個思路進行的,在對呼叫頻繁的介面進行排查時,並沒有發現記憶體的異常,而出現記憶體的問題則是在資料彙總的相關介面上。 其實這種介面對於初級開發可能是容易出問題的地方,首先這種介面查詢的資料相對其他介面會比較複雜,如果編碼基礎又不是特別好,可能就會在這些介面上出現bug. 而在這次的排查中,最終確定是在一個彙總資料的介面上,定位到問題處在了`Django ORM` 使用不當導致的。自己通過一個簡單程式碼例項來說明: ```python 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) ``` 正常情況,我們的表字段會比較多,這裡就通過多個name來模擬,出現題的程式碼就出在關於這個表的介面上: ```python def index(request): studets = Student.objects.filter(age__gt=20) if studets: pass return HttpResponse("test memory") ``` 為了讓記憶體問題容易復現,我通過指令碼向Student中插入了20000條資料,當然這裡資料越多,問題越明顯 通過一個測試指令碼併發請求這個介面,觀察記憶體情況,你會發現,記憶體會出現瞬間上漲的情況,並且如果你的資料越多,請求越多,你的記憶體可能會在一段時間居高不下,並且逐漸上漲。問題出在哪裡了? 其實很簡單,問題出在了程式碼中的if 判斷那裡,我們通過filter 查詢返回的是QuerySet 型別的資料,而我們過濾之後的資料可能會存在非常多的時候,這個時候我們通過if 直接判斷,自己的理解這個地方會將整個QuerySet載入到記憶體中,從而出現記憶體佔用過高的問題,而如果並且這個時候這個介面的響應速度也是非常會變慢,而這個QuerySet 中的資料越多,記憶體佔用越明顯。 在`Django`的文件中其實做了說明 > - `exists`()[¶](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet.exists) > > > > Returns `True` if the [`QuerySet`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.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`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet) query. > > [`exists()`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet.exists) is useful for searches relating to both object membership in a [`QuerySet`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet) and to the existence of any objects in a [`QuerySet`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet), particularly in the context of a large [`QuerySet`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.QuerySet). > > The most efficient method of finding whether a model with a unique field (e.g. `primary_key`) is a member of a [`QuerySet`](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.query.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()` 就可以解決問題。 這是一個很小的知識點,但是如果使用不對,可能就會造成非常嚴重的記憶體問題。 ## 總結 - 除了單元測試,還需要做大資料量測試,這次的問題如果在測試的時候做過一定資料量的測試,可能很早就能及時發現問題 - 對於基礎的庫的使用要更加熟悉 - 排查問題的思路要明確,不然可能會無從下手 ### 延伸閱讀 - https://docs.djangoproject.com/en/3.0/ref/models/querysets/ - https://www.cnblogs.com/zhaof/p/100319