通過小細節大幅改善 Django Rest 框架序列化性能
當開發人員選擇 Python、Django 或 Django Rest 框架時,通常並不是因爲它們的性能非常快。Python 一直是 “舒適” 的選擇,當你更關心人體工程學而不是略去某些過程的幾微秒時,你就會選擇 Python。
人體工程學沒有什麼問題。大多數項目並不真正需要那微秒級別的性能提升,但是它們確實需要快速交付高質量的代碼。
所有這些並不意味着性能不重要。正如這個故事告訴我們的那樣,只需稍加註意並進行一些小的改變,就可以顯著提高性能。
模型序列化器性能
不久前,我們注意到一個主要 API 端點的性能非常差。該端點從一個非常大的表中獲取數據,因此我們自然而然地假設問題一定在數據庫中。
當我們注意到即使是很小的數據集也會有很差的性能時,我們開始查看應用程序的其他部分。這個旅程最終將我們帶向了 Django Rest 框架(DRF)序列化器。
版本
在基準測試中,我們使用 Python 3.7、Django 2.1.1 和 Django Rest 框架 3.9.4。
簡單的函數
序列化器用於將數據轉換爲對象,以及將對象轉換爲數據。這是一個簡單的函數,因此我們來編寫一個接受一個 User 實例並返回一個字典的函數:
創建一個用戶以便在基準測試中使用:
對於我們的基準測試,我們將使用 cProfile。爲了消除數據庫等外部影響,我們提前獲取一個用戶,並對其進行 5000 次序列化:
這個簡單的函數花了 0.034 秒來序列化一個用戶對象 5000 次。
ModelSerializer
Django Rest 框架 (DRF) 附帶了一些實用程序類,即 ModelSerializer。
內置 User 模型的一個 ModelSerializer 可能是這樣的:
和之前一樣運行相同的基準測試:
DRF 序列化一個用戶 5000 次需要 12.8 秒,或者說,僅序列化一個用戶需要 390 毫秒。這比普通的函數慢 377 倍。
我們可以看到在 functional.py 中花費了大量的時間。ModelSerializer 使用了 django.utils.functional 中的 lazy 函數來評估驗證情況。Django 的 verbose name 等也使用到了 lazy,DRF 也對它進行了評估。這個函數似乎在拖累序列化器。
只讀 ModelSerializer
ModelSerializer 僅爲可寫字段添加字段驗證。爲了度量驗證的效果,我們創建了一個 ModelSerializer,並將所有字段標記爲只讀:
當所有字段是隻讀時,則不能使用序列化器創建新的實例。
我們來運行這個只讀序列化器的基準測試:
只有 7.4 秒。與可寫的 ModelSerializer 相比,提升了 40%。
在基準測試的輸出中,我們可以看到在 field_mapping.py 和 fields.py 中花費了大量時間。這些都與 ModelSerializer 的內部工作方式有關。在序列化和初始化過程中,ModelSerializer 使用大量元數據來構造和驗證序列化器字段,當然這是有代價的。
“一般”Serializer
在下一個基準測試中,我們希望準確地測量 ModelSerializer“花費” 了我們多少時間。我們先爲 User 模型創建一個 “一般”Serializer:
對這個 "一般" 序列化器運行同樣的基準測試:
這就是我們期待已久的飛躍!
“一般” 序列化器只花了 2.1 秒。這比只讀的 ModelSerializer 快 60%,比可寫的 ModelSerializer 驚人地快 85%。
此時,我們可以很明顯地看到 ModelSerializer 並不 “便宜”!
只讀 “一般”Serializer
在可寫的 ModelSerializer 中,驗證過程花費了大量的時間。通過將所有字段標記爲只讀,我們可以使它更快。“一般” 序列化器並不定義任何的驗證,因此將字段標記爲只讀並不會使它更快。我們要確保:
並對一個用戶實例運行基準測試:
和預期的一樣,與 “一般” 序列化器相比,將字段標記爲只讀並沒有帶來太大區別。這就再一次肯定了時間主要花在從模型的字段定義派生的驗證部分上。
結果摘要
以下是迄今爲止的運行結果的摘要:
之前的工作
目前,人們寫了很多關於 Python 中的序列化性能的文章。正如預期的那樣,大多數文章都關注於使用 select_related 和 prefetch_related 等技術來改進 DB 訪問。雖然這兩種方法都可以有效地提高 API 請求的總體響應時間,但它們並沒有解決序列化本身的問題。我懷疑這是因爲沒有人想到序列化會很慢。
其他只關注序列化的文章通常會避免修復 DRF,而是去激發新的序列化框架,如 marshmallow 和 serpy。甚至有一個站點專門比較 Python 中的序列化格式。爲了節省你的點擊,DRF 總是排在最後。
2013 年年末,Django Rest 框架的創建者 Tom Christie 寫了一篇文章,討論了 DRF 的一些缺點。在他的基準測試中,序列化過程佔處理單個請求總時間的 12%。在總結中,Tom 建議不要總是使用序列化:
4. 你不需要總是使用序列化器。
對於性能關鍵的視圖,你可以考慮完全刪除序列化器,並在數據庫查詢中簡單地使用. values()。
正如我們在前面看到的,這是一個可靠的建議。
爲什麼會這樣?
在第一個使用 ModelSerializer 的基準測試中,我們看到大量的時間花費在 functional.py 中,更具體地說是在 lazy 函數中。
修復 Django 中的 lazy
Django 在內部使用 lazy 函數來處理許多事情,比如 verbose name(冗長的名稱)、模板等。其源代碼中將 lazy 描述如下:
對一個函數調用進行封裝,並將其作爲一個在該函數的結果上進行調用的方法的代理。在調用結果上的一個方法之前,不會對函數進行計算。
lazy 函數通過創建一個結果類的代理來實現它的魔力。要創建這個代理,lazy 函數會遍歷這個結果類 (及其超類) 的所有屬性和函數,並創建一個包裝器類,該類僅在實際使用函數結果時纔會對函數進行計算。
對於大型結果類,創建代理可能需要一些時間。因此,爲了加快速度,lazy 會緩存該代理。但事實證明,代碼中的一個小疏忽會完全破壞這個緩存機制,使得 lazy 函數非常非常慢。
爲了瞭解在沒有適當緩存的情況下,lazy 函數有多慢,讓我們使用一個簡單的函數,它返回一個 str (結果類),比如 upper。我們選擇 str 是因爲它有很多方法,所以爲它設置一個代理需要一段時間。
爲了建立一個基線,我們直接使用 str.upper 進行基準測試,不使用 lazy 函數:
現在就是驚人的部分,完全相同的函數,但這次使用 lazy 進行了包裝:
沒有任何錯誤! 使用 lazy 時,將 5000 個字符串轉換爲大寫需要 1.139 秒,而直接使用相同的函數只需要 0.034 秒。快將近 33.5 倍。
這顯然是一個疏忽。開發人員清楚地意識到緩存代理的重要性。因此,他們發佈了一個 PR,並在不久後進行了合併 (有關不同之處請看這裏)。一旦發佈,這個補丁將使 Django 的整體性能更好。
修復 Django Rest 框架
DRF 對驗證和字段冗長名稱使用了 lazy 函數。當所有這些惰性評估結果放在一起時,你會明顯感覺運行要慢。
Django 中對 lazy 的修復在進行微小修復後本來也可以解決 DRF 的這個問題,但儘管如此,開發人員還是對 DRF 進行了一個單獨的修復,用更有效的東西替代 lazy。
要查看更改的效果,請安裝 Django 和 DRF 的最新版本:
在應用了這兩個補丁之後,我們再一次運行同樣的基準測試。這些是並列的結果:
我們來總結一下 Django 和 DRF 的變化結果:
-
可寫 ModelSerializer 的序列化時間被降低了一半。
-
只讀 ModelSerializer 的序列化時間被降低了三分之一。
-
和預期的一樣,在其它的序列化方法中沒有明顯的差異。
結論
我們從這個實驗中得出的結論是:
1. 一旦這些補丁正式發佈,就升級 DRF 和 Django。
兩個 PR 的補丁都已合併,但尚未發佈。
2. 在性能關鍵的端點中,使用 “一般” 序列化器,或者根本不使用。
我們有幾個地方的客戶端正在使用 API 來獲取大量數據。API 只用於從服務器讀取數據,因此我們決定根本不使用 Serializer,而是使用內聯序列化進行替代。
3. 不用於寫入或驗證的 Serializer 字段應該是隻讀的。
正如我們在基準測試中所看到的,驗證的實現方式使它們變得昂貴,而將字段標記爲只讀可以消除不必要的額外成本。
福利: 強制形成好習慣
爲了確保開發人員不會忘記設置只讀字段,我們添加了一個 Django 檢查,以確保所有的 ModelSerializer 都設置了 read_only_fields:
有了這個檢查,當開發人員添加一個序列化器時,她還必須設置 read_only_field。如果這個序列化器是可寫的,read_only_fields 可以設置爲一個空元組。如果開發人員忘記設置 read_only_fields,她將得到以下錯誤:
我們經常使用 Django 檢查,以確保沒有遺漏任何內容。
來源:https://hakibenita.com/django-rest-framework-slow
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/lG41YA8p_0uEQuo3i05Cnw