一次django記憶體異常排查
阿新 • • 發佈:2020-07-30
## 起因
`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