動態網站的準系統就在於, 沒錯, 它是動態. 使用者每次請求一個頁面時, Web 服務器都要進行全面的計算 -- 從資料庫查詢到渲染商務邏輯 -- 直到產生最終展示的頁面. 從伺服器負載的角度來看,這遠比僅僅從檔案系統讀取一個檔案展示要佔用的系統資源多得多.
對絕大多數網站應用程式程式來說, 這點負載不是大問題.絕大部分網站應用程式不是 washingtonpost.com 或 slashdot.org 這樣繁忙; 它們通常是小規模-中等規模的,沒有特別高的點擊率. 但對大規模高點擊率的網站來說, 就應該儘可能的降低WEB伺服器的負載.
這正是需要緩衝機制的原因.
建立緩衝機制的目的就是在適當的時候省略掉計算步驟(再次訪問時).下面用偽碼來闡述該機制如何運作:
根據 URL 請求, 看看緩衝中有沒有該頁面如果緩衝中有該頁面: 返回該頁面否則: 產生該頁面 將該頁面儲存在緩衝中(以備下次訪問) 返回這具產生的頁面
Django 內建一個高效實用的緩衝系統, 允許你儲存動態網頁面, 這樣就不必在每次請求時都通過計算產生新頁面.為了使用上的方便, Django提供了不同層次的緩衝模式: 你可以僅緩衝指定 views 的輸出, 也可以只緩衝高計算量的的片段, 只要你願意,你也可以緩衝你的整個網站.
Django 與 "upstream" 緩衝可以很好的協作, 就象 Squid(http://www.squid-cache.org/) 及 browser-based 緩衝一樣. 這些緩衝方式, 你不能直接控制但可以通過 HTTP headers 來告知網站的哪些部分需要緩衝及如何緩衝.
設定緩衝
緩衝系統需要進行不太複雜的設定才可以工作. 也就是說你必須告訴它你的快取資料要放在哪兒 -- 或者是資料庫, 或者是檔案系統, 或者乾脆就放在記憶體裡.這個選擇對緩衝的效能有著非常重大的影響.沒錯,某些緩衝類型遠比其它的快!
通過設定你的 settings 檔案的 CACHE_BACKEND 設定來決定 cache 類型. 下面詳細介紹該選項的所有可選值:
Memcached
這是 Django 緩衝系統中最有效率的方式, Memcached 是完全的基於記憶體的緩衝架構, 最初開發它來處理 LiveJournal.com 的高流量後來被 Danga Interactive 公司開源. 它通常用於象 Slashdot 及 Wikipedia 這樣的網站以減少資料庫檢索, 它極大的提高了網站的效能.
Memcached 可以通過 http://danga.com/memcached/ 免費得到. 它以一個後台監視程式的方式運作, 使用分配給它的指定容量的記憶體. 它所做的就是提供一個介面 -- 一個super-lightning-fast 介面 -- 添加,得到及刪除快取資料的介面.所有資料直接儲存在記憶體中,不佔用任何檔案系統及資料庫資源.
在安裝了 Memcached 之後,你需要安裝 Memcached 的 Python 綁定. 它是一個獨立的 Python 模組, memcache.py, 可以通過ftp://ftp.tummy.com/pub/python-memcached/ 得到. 如果該 URL 無法訪問, 就直接到 Memcached 的 Web 網站 (http://www.danga.com/memcached/), 然後通過 "Client APIs" 部分得到它的 Python 綁定.
要在 Django 中使用 Memcached , 設定 CACHE_BACKEND 為 memcached://ip:port/, 這裡的 ip 是 Memcached 精靈的 IP 位址, 而 port 則是 Memcached 進程使用的連接埠.
在下面這個例子裡, Memcached 運行在 localhost (127.0.0.1) 的 11211 連接埠:
CACHE_BACKEND = 'memcached://127.0.0.1:11211/'
Memcached 的一個出色特性是它在多個伺服器間共用快取的能力. 要使用這個進階特性, 在 CACHE_BACKEND 中包含多個伺服器的IP地址,地址間用分號分隔. 下面這個例子共用運行在 172.19.26.240 及 172.19.26.242 的 Memcached 執行個體, 這兩個執行個體均使用 11211 連接埠:
CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11211/'
基於記憶體的緩衝機制有一個缺點: 由於快取資料儲存在記憶體中, 當伺服器崩潰時緩資料將會丟失.誰都知道記憶體不適合用來儲存永久資料, 所以不應該依賴基於記憶體的緩衝來作為你唯一的資料儲存方式.實際上, 沒有一種 Django 緩衝後端適合儲存永久資料 -- 他們之所以存在就是為了緩衝這一個目的, 而不是永遠儲存資料 -- 我們在這裡指出這一點是因為基於記憶體的緩衝它比另外的方式更加臨時化.
資料庫緩衝
要使用資料庫來做為你的緩衝後端, 首先要在你的資料庫中建立一個緩衝表. 運行下面的命令:
python manage.py createcachetable [cache_table_name]
...這裡的 [cache_table_name] 是資料庫中要建立的緩衝表的名字.(你可以取任意的名字,只要不和已有的表重名就行).這個命令建立在資料庫中建立一個適當結構的緩衝表, Django 隨後使用它來緩衝你的資料.
在完成緩衝表的建立之後, 設定 CACHE_BACKEND 為 "db://tablename/", 這裡的 tablename 是緩衝表的名字.下面這個例子裡,緩衝表的名是 my_cache_table:
CACHE_BACKEND = 'db://my_cache_table'
只要你有一個快速的,索引良好的資料庫伺服器, 資料庫緩衝就會極佳的運轉.
檔案系統快取
要在檔案系統中儲存快取資料, 在 CACHE_BACKEND 中使用 "file://" 緩衝類型. 下面這個例子將快取資料儲存在 /var/tmp/django_cache 中, 設定如下:
CACHE_BACKEND = 'file:///var/tmp/django_cache'
注意 file: 之後是三個斜線而不是兩個. 前兩個是 file:// 協議, 第三個是路徑 /var/tmp/django_cache 的第一個字元.
這個目錄路徑必須是絕對路徑 -- 也就是說,它應該從你的檔案系統的根開始算起.至於路徑的最後是否加一個斜線, Django 並不在意.
要確保這個設定的路徑存在並且 Web 服務器可以讀寫.繼續上面的例子,如果你的伺服器以 apache 使用者身份運行, 確保目錄 /var/tmp/django_cache 存在並且可以被使用者 apache 讀寫.
本地記憶體緩衝
如果你想要記憶體緩衝的高效能卻沒有條件運行 Memcached, 可以考虛使用本地記憶體緩衝後端. 這個緩衝後端是多進程的並且安全執行緒. 要使用它,設定 CACHE_BACKEND 為"locmem:///". 舉例來說:
CACHE_BACKEND = 'locmem:///'
簡單緩衝(用於開發)
"simple:///" 是一個簡單的,單進程的記憶體緩衝類型. 它僅僅在進程中儲存快取資料, 這意味著它僅適用於開發或測試環境. 例子:
CACHE_BACKEND = 'simple:///'
虛擬緩衝 (用於開發)
最後, Django還支援一種 "dummy" 緩衝(事實上並未緩衝) -- 僅僅實現了緩衝介面但未實際做任何事.
This is useful if you have a production site that uses heavy-duty caching in various places but a development/test environment on which you don't want to cache. In that case, set CACHE_BACKEND to "dummy:///" in the settings file for your development environment. As a result, your development environment won't use caching and your production environment still will.
CACHE_BACKEND 參數
所有緩衝類型均接受參數. 提供參數的方式類似查詢字串風格. 下面列出了所有合法的參數:
-
timeout
-
預設的緩衝有效時間,以秒計. 預設值是 300 秒(五分鐘).
-
max_entries
-
用於
簡單緩衝 及
資料庫緩衝 後端, 緩衝的最大條目數(超出該數舊的緩衝會被清除,預設值是 300).
-
cull_percentage
-
當達到緩衝的最大條目數時要保留的精選條目比率. 實際被儲存的是 1/cull_percentage, 因此設定 cull_percentage=3 就會儲存精選的 1/3 條目上,其餘的條目則被刪除.
如果將 cull_percentage 設定為 0 則意味著當達到緩衝的最大條目數時整個緩衝都被清除.當快取命中率很低時這會 極大的 提高精選緩衝條目的效率(根本不精選).
這個例子裡, timeout 被設定為 60:
CACHE_BACKEND = "memcached://127.0.0.1:11211/?timeout=60"
這個例子, timeout 設定為 30 而 max_entries 設定為 400:
CACHE_BACKEND = "memcached://127.0.0.1:11211/?timeout=30&max_entries=400"
非法的參數會被忽略.
緩衝整個網站
設定了緩衝類型之後, 最簡單使用緩衝的方式就是緩衝整個網站. 在``MIDDLEWARE_CLASSES`` 設定中添加 django.middleware.cache.CacheMiddleware , 就象下面的例子一樣:
MIDDLEWARE_CLASSES = ( "django.middleware.cache.CacheMiddleware", "django.middleware.common.CommonMiddleware",)
( MIDDLEWARE_CLASSES 順序相關. 參閱下文中的 "Order of MIDDLEWARE_CLASSES")
然後,在 Django 設定檔案中添加以下設定:
- CACHE_MIDDLEWARE_SECONDS -- 每個頁面被緩衝的時間.
- CACHE_MIDDLEWARE_KEY_PREFIX -- 如果緩衝被同一個 Django 安裝的多個網站共用, 在這裡佈建網站名字, 或者某些其它唯一的字串, 以避免 key 衝突.如果你不介意,也可以使用空串.
緩衝中介軟體緩衝沒有 GET/POST 參數的每個頁面.另外, CacheMiddleware 自動在每個 HttpResponse 中設定一些 headers:
- 當請求一個未快取頁面面時,設定 Last-Modified header 到當前的日期時間.
- 設定 Expires header 到當前日期時間加上定義的 CACHE_MIDDLEWARE_SECONDS.
- 設定 Cache-Control header 為該頁面的生存期 -- 該生存期也來自 CACHE_MIDDLEWARE_SECONDS 設定.
參閱 middleware documentation 瞭解中介軟體的更多資訊.
緩衝單個 view
Django 能夠只緩衝特定的頁面. django.views.decorators.cache 定義了一個 cache_page 修飾符, 它能自動緩衝該 view 的響應. 該修飾符的使用極為簡單:
from django.views.decorators.cache import cache_pagedef slashdot_this(request): ...slashdot_this = cache_page(slashdot_this, 60 * 15)
或者, 使用Python 2.4 的修飾符文法:
@cache_page(60 * 15)def slashdot_this(request): ...
cache_page 僅接受一個參數: 緩衝有效期間,以秒計. 在上面的例子裡, slashdot_this() view 將被緩衝 15 分鐘.
底層緩衝 API
某些時候, 緩衝一個完整的頁面不符合你的要求. 比如你認為僅有某些高強度的查詢才有必要緩衝其結果.要達到這種目的,你能使用底層緩衝 API 來在任意層次儲存對象到緩衝系統.
緩衝 API 是簡單的. 從緩衝模組 django.core.cache 匯出一個由 CACHE_BACKEND 設定自動產生的 cache 對象:
>>> from django.core.cache import cache
基本的介面是 set(key, value, timeout_seconds) 和 get(key):
>>> cache.set('my_key', 'hello, world!', 30)>>> cache.get('my_key')'hello, world!'
timeout_seconds 參數是可選的,其預設值等於 CACHE_BACKEND 的 timeout 參數.
如果緩衝中沒有該對象, cache.get() 返回 None:
>>> cache.get('some_other_key')None# Wait 30 seconds for 'my_key' to expire...>>> cache.get('my_key')None
get() 可以接受一個 default 參數:
>>> cache.get('my_key', 'has expired')'has expired'
當然還有一個 get_many() 介面, 它僅僅命中緩衝一次. get_many() 返回一個字典,包括未到期的實際存在的你請求的所有鍵.:
>>> cache.set('a', 1)>>> cache.set('b', 2)>>> cache.set('c', 3)>>> cache.get_many(['a', 'b', 'c']){'a': 1, 'b': 2, 'c': 3}
最後, 你可以使用 delete() 顯式的刪除鍵. 這是一個在緩衝中清除特定對象的簡便的方式:
>>> cache.delete('a')
就是這樣. 緩衝機制限制非常少: 你可以安全的緩衝能被 pickled 的任意對象(key必須是字串).
Upstream caches
到現在為止, 我們的目光僅僅聚焦在 你自己的 資料上. 在 WEB 開發中還有另外一種類型的緩衝: "upstream" 緩衝. 這是使用者請求還未抵達你的網站時由瀏覽器實施的緩衝.
下面是 upstream 緩衝的幾個例子:
- 你的 ISP 會緩衝特定頁面, 當你請求 somedomain.com 的一個頁面時, 你的 ISP 會直接發送給你一個快取頁面.(不訪問 somedomain.com ).
- 你的 Django Web 網站可能建立在一個 Squid (http://www.squid-cache.org/) Web Proxy伺服器之後, 它會快取頁面面以提高效能. 這種情況下,每個請求會先經 Squid 處理, 僅在需要時它會將請求傳遞給你的應用程式.
- 你的瀏覽器也會緩衝一些頁面. 如果一個 WEB 頁發送了正確的 headers, 瀏覽器會用本地(緩衝的)拷貝來回應後發的同一頁面的請求.
Upstream 緩衝是一個非常有效推進, 不過它也有相當不足之處: 很多 WEB 頁基於授權及一堆變數, 而這個緩衝系統盲目的單純依賴 URL 快取頁面面, 這可能會對不適當的使用者泄露敏感資訊.
舉例來說,假設你使用一個 Web e-mail 系統, "inbox" 頁的內容顯然依賴當前登入使用者. 如果一個 ISP 盲目的緩衝了你的網站, 後來的使用者就會看到前一使用者的收件匣, 這可不是一件有趣的事.
幸運的是, HTTP 提供了一個該問題的解決方案: 用一系列 HTTP headers 來構建緩衝機制以區分緩衝內容, 這樣緩衝系統就不會緩衝某些特定頁.
使用 Vary headers
其中一個 header 就是 Vary. 它定義了緩衝機制在建立緩衝 key 時的請求 headers. 舉例來說, 如果一個網頁的內容依賴一個戶的語言設定, 則該網頁被告知 "vary on language."
預設情況, Django 的緩衝系統使用請求路徑建立 緩衝 key -- 比如, "/stories/2005/jun/23/bank_robbed/". 這意味著該 URL 的每個請求使用相同的緩衝版本, 不考慮使用者代理程式的不同.(cookies 及語言特性).
因此我們需要 Vary .
如果你的基於 Django 的頁面根據不同的請求 headers 輸出不同的內容 -- 比如一個 cookie, 或語言, 或使用者代理程式 -- 你會需要使用 Vary header 來告訴緩衝系統這個頁面輸出依賴這些東西.
要在 Django 中做到這一步, 使用 vary_on_headers view 修飾符,就象下面這樣:
from django.views.decorators.vary import vary_on_headers# Python 2.3 syntax.def my_view(request): ...my_view = vary_on_headers(my_view, 'User-Agent')# Python 2.4 decorator syntax.@vary_on_headers('User-Agent')def my_view(request): ...
這樣緩衝系統 (比如 Django 自己的緩衝中介軟體) 會為不同的使用者代理程式緩衝不同的版本的頁面.
使用 vary_on_headers 修飾符的優勢在於(與人工設定 Vary header 相比:使用類似 response['Vary'] = 'user-agent')修飾符會添加到 Vary header (可能已存在) 而不是覆蓋掉它.
你也可以傳遞多個 header 給 vary_on_headers():
@vary_on_headers('User-Agent', 'Cookie')def my_view(request): ...
由於多個 cookie 的情況相當常見, 這裡有一個 vary_on_cookie 修飾符. 下面兩個 views 是等價的:
@vary_on_cookiedef my_view(request): ...@vary_on_headers('Cookie')def my_view(request): ...
需要注意一點傳遞給 vary_on_headers 的參數是大小寫不敏感的. "User-Agent" 與 "user-agent" 完全相同.
你也可以直接使用一個協助函數, django.utils.cache.patch_vary_headers:
from django.utils.cache import patch_vary_headersdef my_view(request): ... response = render_to_response('template_name', context) patch_vary_headers(response, ['Cookie']) return response
patch_vary_headers 接受一個 HttpResponse 執行個體作為它的第一個參數及一個 header 名字的列表或tuple作為第二個參數.
要瞭解 Vary headers 的更多資訊, 參閱 official Vary spec.
控制緩衝: 使用其它 headers
緩衝的另一個問題是資料的私密性及瀑布緩衝模式下資料儲存到哪裡.
使用者經常要面對的有兩種緩衝: 他自己的瀏覽器緩衝(私人緩衝) 及網站提供的緩衝(公開緩衝).一個公開緩衝用於多使用者情況, 其內容由另外的人控制. 這造成了資料的私密性問題: 你當然不想你的銀行帳號儲存在公用緩衝裡. 因此應用程式需要一種方式告訴緩衝系統哪些東西是私密的,哪些則是公開的.
解決方案就是聲明某個頁面的緩衝是 "私密的". 在 Django 中, 使用 cache_control view 修飾符. 例子:
from django.views.decorators.cache import cache_control@cache_control(private=True)def my_view(request): ...
這個修飾符會在幕後謹慎的發送適當的 HTTP header 發避免上面的問題.
還有一些其它的方式控制緩衝參數. 舉例來說,HTTP 允許應用程式做以下事:
- 定義一個頁面被緩衝的最大時間.
- 指定緩衝是否需要總是檢查新版本, 如果沒有變化則僅傳送緩衝版本. (某些緩衝即使伺服器端頁面變化也僅傳遞緩衝版本--僅僅因為緩衝拷貝尚未到期).
在 Django 中, 使用 cache_control view 修飾符指定緩衝參數.在這個例子裡 cache_control 通知緩衝每次檢驗緩衝版本, 直到 3600 秒到期:
from django.views.decorators.cache import cache_control@cache_control(must_revalidate=True, max_age=3600)def my_view(request): ...
所有合法的 Cache-Control HTTP 指令在 cache_control() 中都是合法的. 下面是完整的指令列表:
- public=True
- private=True
- no_cache=True
- no_transform=True
- must_revalidate=True
- proxy_revalidate=True
- max_age=num_seconds
- s_maxage=num_seconds
要瞭解 Cache-Control HTTP 指令的細節,參閱 Cache-Control spec.
(注意緩衝中介軟體已經通過設定中的 CACHE_MIDDLEWARE_SETTINGS 的值設定了緩衝 header 的 max-age. 如果你在 cache_control 修飾符中使用了自訂的 max_age , 修飾符中的設定將被優先使用, header 值會被正確的合并)
其它最佳化
Django 內建了一些中介軟體以協助你提高網站效能:
- django.middleware.http.ConditionalGetMiddleware 添加了有條件GET的支援. 它利用了 ETag 及 Last-Modified headers.
- django.middleware.gzip.GZipMiddleware 為支援 Gzip 的瀏覽器對發送內容進行壓縮(所有流行瀏覽器均支援).
MIDDLEWARE_CLASSES 順序
如果你使用了 CacheMiddleware, 在 MIDDLEWARE_CLASSES 設定中使用正確順序非常重要. 由於緩衝中介軟體需要知道哪些 headers 由哪些緩衝儲存.中介軟體總是在可能的情況下添加某些東西到 Vary 響應 header.
將 CacheMiddleware 放到其它可能添加某些東西到 Vary Header的中介軟體之後,下面的中介軟體會添加東西到 Vary header:
- SessionMiddleware 添加了 Cookie
- GZipMiddleware 添加了 Accept-Encoding