天天看點

Django性能優化大全!Django性能優化大全!

Django性能優化大全!

性能優化名額

在對一個Web項目進行性能優化時,我們通常需要評價多個名額:

  • 響應時間
  • 最大并發連接配接數
  • 代碼的行數
  • 函數調用次數
  • 記憶體占用情況
  • CPU占比

其中響應時間(伺服器從接收使用者請求,處理該請求并傳回結果所需的總的時間)通常是最重要的名額,因為過長的響應時間會讓使用者厭倦等待,轉投其它網站或APP。當你的使用者數量變得非常龐大,如何提高最大并發連接配接數,減少記憶體消耗也将變得非常重要。

在開發環境中,我們一般建議使用django-debug-toolbar和django-silk來進行性能監測分析。它們提供了每次使用者請求的響應時間,并告訴你程式執行過程哪個環節(比如SQL查詢)最消耗時間。

對于中大型網站或Web APP而言,最影響網站性能的就是資料庫查詢部分了。一是反複從資料庫讀寫資料很消耗時間和計算資源,二是當傳回的查詢資料集queryset非常大時還會占據很多記憶體。我們先從這部分優化做起。

資料庫查詢優化

  1. 利用Queryset的惰性和緩存,避免重複查詢

充分利用Django的QuerySet的惰性和自帶緩存特性,可以幫助我們減少資料庫查詢次數。比如下例中例1比例2要好。因為在你列印文章标題後,Django不僅執行了資料庫查詢,還把查詢到的article_list放在了緩存裡,下次可以在其它地方複用,而例2就不行了。

# 例1: 利用了緩存特性 - Good
 article_list = Article.objects.filter(title__contains="django")
 for article in article_list:
     print(article.title)
 
 # 例2: Bad
 for article in Article.objects.filter(title__contains="django"):
     print(article.title)
           

但有時我們隻希望了解查詢的結果是否存在或查詢結果的數量,這時可以使用exists()和count()方法,如下所示。這樣就不會浪費資源查詢一個用不到的資料集,還可以節省記憶體。

# 例3: Good
 article_list = Article.objects.filter(title__contains="django")
 if article_list.exists():
     print("Records found.")
 else:
     print("No records")
     
 # 例4: Good
 count = Article.objects.filter(title__contains="django").count()
           

\2. 一次查詢所有需要的關聯模型資料

假設我們有一個文章(Article)模型,其與類别(Category)是單對多的關系(ForeignKey), 與标簽(Tag)是多對多的關系(ManyToMany)。我們需要編寫一個article_list的函數視圖,以清單形式顯示文章清單及每篇文章的類别和标簽,你的模闆檔案可能如下所示:

{% for article in articles %}
     <li>{{ article.title }} </li>
     <li>{{ article.category.name }}</li>
     <li>
         {% for tag in article.tags.all %}
            {{ tag.name }},
         {% endfor %}
     </li>
 {% endfor %}
           

在模闆裡每進行一次for循環擷取關聯對象category和tag的資訊,Django就要單獨進行一次資料庫查詢,造成了極大資源浪費。我們完全可以使用select_related方法和prefetch_related方法一次性從資料庫擷取單對多和多對多關聯模型資料,這樣在模闆中周遊時Django也不會執行資料庫查詢了。

# 僅擷取文章資料 - Bad
 def article_list(request):
     articles = Article.objects.all()
     return render(request, 'blog/article_list.html',{'articles': articles, })
 
 # 一次性提取關聯模型資料 - Good
 def article_list(request):
     articles = Article.objects.all().select_related('category').prefecth_related('tags')
     return render(request, 'blog/article_list.html', {'articles': articles, })
           

\3. 僅查詢需要用到的資料

預設情況下Django會從資料庫中提取所有字段,但是當資料表有很多列很多行的時候,告訴Django提取哪些特定的字段就非常有意義了。假如我們資料庫中有100萬篇文章,需要循環列印每篇文章的标題。如果按例4操作,我們會将每篇文章對象的全部資訊都提取出來載入到記憶體中,不僅花費更多時間查詢,還會大量占用記憶體,而最後隻用了title這一個字段,這是完全沒有必要的。我們完全可以使用values和value_list方法按需提取資料,比如隻擷取文章的id和title,節省查詢時間和記憶體(例6-例8)。

# 例子5: Bad
 article_list = Article.objects.all()
 if article_list:
     print(article.title)
 
 # 例子6: Good - 字典格式資料
 article_list = Article.objects.values('id', 'title')
 if article_list:
     print(article.title)
 
 # 例子7: Good - 元組格式資料
 article_list = Article.objects.values_list('id', 'title')
 if article_list:
     print(article.title)
     
 # 例子8: Good - 清單格式資料
 article_list = Article.objects.values_list('id', 'title', flat=True)
 if article_list:
     print(article.title)
           

除此以外,Django項目還可以使用defer和only這兩個查詢方法來實作這一點。第一個用于指定哪些字段不要加載,第二個用于指定隻加載哪些字段。

\4. 使用分頁,限制最大頁數

事實前面代碼可以進一步優化,比如使用分頁僅展示使用者所需要的資料,而不是一下子查詢所有資料。同時使用分頁時也最好控制最大頁數。比如當你的資料庫有100萬篇文章時,每頁即使展示100篇,也需要1萬頁展示給你的使用者,這是完全沒有必要的。你可以完全隻展示前200頁的資料,如下所示:

LIMIT = 100 * 200
 
 data = Articles.objects.all()[:(LIMIT + 1)]
 if len(data) > LIMIT:
     raise ExceededLimit(LIMIT)
 
 return data
           

資料庫設定優化

如果你使用單個資料庫,你可以采用如下手段進行優化:

  • 建立模型時能用CharField确定長度的字段盡量不用不用TextField, 可節省存儲空間;
  • 可以給搜尋頻率高的字段屬性,在定義模型時使用索引(db_index=True);
  • 持久化資料庫連接配接。

沒有持久化連接配接,Django每個請求都會與資料庫建立一個連接配接,直到請求結束,關閉連接配接。如果資料庫不在本地,每次建立和關閉連接配接也需要花費一些時間。設定持久化連接配接時間,僅需要添加CONN_MAX_AGE參數到你的資料庫設定中,如下所示:

DATABASES = {
     ‘default’: {
         ‘ENGINE’: ‘django.db.backends.postgresql_psycopg2’,
         ‘NAME’: ‘postgres’,
         ‘CONN_MAX_AGE’: 60, # 60秒
     }
 }
           

當然CONN_MAX_AGE也不宜設定過大,因為每個資料庫并發連接配接數有上限的(比如mysql預設的最大并發連接配接數是100個)。如果CONN_MAX_AGE設定過大,會導緻mysql 資料庫連接配接數飙升很快達到上限。當并發請求數量很高時,CONN_MAX_AGE應該設低點,比如30s, 10s或5s。當并發請求數不高時,這個值可以設得長一點,比如60s或5分鐘。

當你的使用者非常多、資料量非常大時,你可以考慮讀寫分離、主從複制、分表分庫的多資料庫伺服器架構。這種架構上的布局是對所有web開發語言适用的,并不僅僅局限于Django,這裡不做進一步展開了。

緩存

緩存是一類可以更快的讀取資料的媒體統稱,也指其它可以加快資料讀取的存儲方式。一般用來存儲臨時資料,常用媒體的是讀取速度很快的記憶體。一般來說從資料庫多次把所需要的資料提取出來,要比從記憶體或者硬碟等一次讀出來付出的成本大很多。對于中大型網站而言,使用緩存減少對資料庫的通路次數是提升網站性能的關鍵之一。

  1. 視圖緩存
from django.views.decorators.cache import cache_page
 
 @cache_page(60 * 15)
 def my_view(request):
     ...
           

\2. 使用@cached_property裝飾器緩存計算屬性

對于不經常變動的計算屬性,可以使用@cached_property裝飾器緩存結果。

\3. 緩存臨時性資料比如sessions

Django的sessions預設是存在資料庫中的,這樣的話每一個請求Django都要使用sql查詢會話資料,然後獲得使用者對象的資訊。對于臨時性的資料比如sessions和messages,最好将它們放到緩存裡,也可以減少SQL查詢次數。

SESSION_ENGINE = 'django.contrib.sessions.backends.cache'