天天看點

Symfony2Book12:HTTP 緩存

富Web應用程式的特征就是它們是動态的。無論你的應用程式多麼有效率,每個請求總是比服務靜态檔案有着更多的開銷。

對于大多數Web應用程式而言,這是好的。Symfony2非常快,除非你做的是很重量級的事,否則每個請求都會很快被傳回,這不會給你的伺服器太大壓力。

但當你的站點增長之後,開銷就變成了一個問題。對每個請求的正常處理應該隻做一次,這正是緩存的目标所在。

提高應用程式性能最有效的方式就是緩存整個輸出頁,然後完全旁路應用程式随後的請求。當然,對于高度動态的網站而言并不總是可以這樣做,不是嗎?在本章,我們将向你展示Symfony2緩存系統是如何工作的,并且為什麼我們認為這是最好的方式。

Symfony2的緩存系統是不同的,因為它依賴簡單而強大的HTTP緩存,正如它在HTTP規範中定義的那樣。與重塑一個緩存方法不同,Symfony2擁抱标準,該标準定義了Web上的基本通信。一旦你了解HTTP驗證和失效緩存模式的基本原理,你将做好了掌握Symfony2緩存系統的準備。

為了學會如何Symfony2緩存的用途,我們将分四步來讨論:

當與HTTP緩存時,緩存完全從你的應用程式中分離出來,并且位于你應用程式和用戶端之間發送請求。

緩存的工作是從用戶端接受請求,并将其送給應用程式。緩存也從應用程式中接收傳回的響應并将其發送給用戶端。緩存是用戶端和應用程式之間請求-響應通信的中間人。

但網關緩存并不是唯一的緩存類型。實際上,應用程式發送的HTTP緩存頭被消耗和解釋多達三種不同的緩存類型:

浏覽器緩存:每個浏覽器都有它自己的本地緩存,它主要用在你點選“傳回”按鈕或者緩存圖檔和其它資源。浏覽器緩存對于被緩存的資源來說是個私有緩存,它不與任何人共享。

代理緩存:代理是一個共享緩存,因為許多人可以共享一個緩存。它通常被大公司或ISP們安裝,用來減少潛在的和網絡的流量。

網關緩存:就象代理一樣,它也是一個共享緩存,但是在伺服器端的。它通過網絡管理者安裝,使得網站更具可擴充性、可靠性和高性能。

網關緩存有時也被稱為反向代理緩存、代理緩存甚至是HTTP加速器。

私有共享緩存的重要性開始變得更為明顯,因為我們所談論的緩存響應包括的内容是指向特定使用者的(如帳号資訊)。

每個來自你應用程式的響應可能會經過一個或前兩種類型的緩存。這些緩存在你的控制之外,但遵循響應中的HTTP緩存指令集。

Symfony2附帶一個用PHP寫的反向代理(也被稱為網關緩存) 。啟動它之後來自你應用程式、可被緩存的響應将馬上開始被緩存。安裝它非常簡單。每個新Symfony2應用程式都有一個被預配置的緩存核心(AppCache),它包含了預設核心(AppKernel)。緩存核心是個反向代理:

// web/app.php 

require_once __DIR__.'/../app/bootstrap_cache.php.cache'; 

require_once __DIR__.'/../app/AppCache.php'; 

use Symfony\Component\HttpFoundation\Request; 

// wrap the default AppKernel with the AppCache one 

$kernel = new AppCache(new AppKernel('prod', false)); 

$kernel->handle(Request::createFromGlobals())->send(); 

緩存核心會立即生效。作為一個反向代理,它緩存來自應用程式的響應并把它們傳回給用戶端。

緩存核心有一個特殊的getLog()方法,它傳回一個緩存層發生了什麼的字元串說明。在開發環境中,使用它去調試和驗證你的緩存政策:

error_log($kernel->getLog()); 

AppCache對象有一個合理的預設配置,但它也可以通過覆寫getOptions方法設定選項的方式進行微調:

// app/AppCache.php 

class AppCache extends Cache 

    protected function getOptions() 

    { 

        return array( 

            'debug'                  => false, 

            'default_ttl'            => 0, 

            'private_headers'        => array('Authorization', 'Cookie'), 

            'allow_reload'           => false, 

            'allow_revalidate'       => false, 

            'stale_while_revalidate' => 2, 

            'stale_if_error'         => 60, 

        ); 

    } 

除非覆寫getOptions(),否則調試選項将被自動設定為被包含AppKernel的debug值。

這裡有一個主要選項的清單:

default_ttl: 在響應中沒有提供明确的重新整理資訊時一個緩存條目被視為新鮮的秒數。直接覆寫緩存控制或失效頭的值(預設:0);

private_headers: 一組請求頭。在通過緩存控制指令不能明确公共還是私有狀态響應的情況下,觸發“私有”緩存控制行為(預設:授權和Cookie);

allow_reload: 通過在請求中包含一個緩存控制的"no-cache"指令來指定使用者端是否可以強制進行重載緩存,根據RFC2616将其設定為true(預設:false);

allow_revalidate:通過在請求中包含一個緩存控制的"max-age=0"指令來指定使用者端是否可以強制進行緩存重驗證。根據RFC2616将其設定為true(預設:false);

stale_while_revalidate: 指定預設秒數(和響應的TTL精度一樣是秒)。在此期間緩存在背景重新驗證(預設:2)時可以傳回陳舊響應;這個設定被stale-while-revalidate HTTP 緩存控制擴充覆寫(參見RFC 5861);

stale_if_error:指定預設的秒數(精度是秒)。在此期間當發生錯誤(預設:60)時,緩存可以成為陳舊響應。該設定可以通過stale-if-error HTTP 緩存控制擴充來覆寫(參見RFC 5861)。

如果debug為true,Symfony2自動向響應添加 X-Symfony-Cache頭,該響應包含關于緩存點選和錯失的有用資訊。

從一個反向代理改到另一個反向代理

Symfony2反向代理是一個偉大的工具。它用于開發網站,或者在除PHP之外不能安裝其它代碼的共享主機中布署網站。但是因為是用PHP寫的,是以它不如用C寫的代理那麼快。這就是為什麼隻要可能,我們都會高度推薦你在你的生産服務上使用Varnish或Squid。好消息是從一個代理服務切換到另一個是友善和透明的,不需要在你的應用程式上做任何修改。開始時使用友善的Symfony2反向代理,然後在你流量提升之後更新到Varnish。

Symfony2反向代理的性能有賴于應用程式的複雜度。那是因為應用程式核心隻在請求需要轉發給它時才啟動。

要使用緩存層,你的應用程式必須要能夠在可緩存響應和何時/如何緩存變陳舊的規則之間通信。這一切可以通過在響應中設定HTTP緩存頭來實作。

記住,"HTTP"無非是一種語言(一個簡單文本語言),Web用戶端(如浏覽器)和Web伺服器使用它來互相通信。當我們談論HTTP緩存時,我們也正在談論這個語言允許用戶端和服務端交換與緩存有關的資訊。

HTTP指定我們關注的4種響應頭

Cache-Control(緩存控制)

Expires(過程)

ETag(被請求變量的實體值)

Last-Modified(最後修改時間)

最重要和最多模式的是Cache-Control頭,它其實上是不同緩存資訊的集合。

頭中每一部分的細節都在HTTP失效和驗證一節中進行說明。

Cache-Control頭是唯一的,但它所包含的資訊部分不是一個,而是多個。資訊的每一部分都由冒号分開:

Cache-Control: private, max-age=0, must-revalidate Cache-Control: max-age=3600, must-revalidate

Symfony2提供一個Cache-Control頭的抽象, 使之更易管理:

$response = new Response(); 

// mark the response as either public or private 

$response->setPublic(); 

$response->setPrivate(); 

// set the private or shared max age 

$response->setMaxAge(600); 

$response->setSharedMaxAge(600); 

// set a custom Cache-Control directive 

$response->headers->addCacheControlDirective('must-revalidate', true); 

網關和代理這兩個緩存被認為是“共享的”緩存,因為緩存内容被超過一個使用者共享。如果特定使用者的響應錯誤地被共享緩存儲存,那麼它随後可能會傳回給不同的使用者。想像一下,如果你的帳号資訊被緩存,然後傳回給每個詢問其帳号頁的使用者時的情景!

要解決這個問題 ,每個響應都要設定成公共或是私有:

public: 表示該響應可能被私有或共享緩存進行緩存;

private: 表示響應的部分或全部消息是針對單個使用者的,不允許被共享緩存來進行緩存。

Symfony2保守地預設每個響應是私有的。要利用共享緩存(如Symfony2反向代理),響應必須明确設為public。

HTTP緩存隻為“安全”的HTTP方法工作(如GET和HEAD)。安全的意思是當送出請求時你永遠不會改變伺服器上應用程式的狀态(你當然可以記錄資訊、緩存資料等)。這樣有兩個非常合理的結果:

當響應GET或HEAD請求時,你應該不會改變應用程式的狀态。甚至如果你沒有使用網關緩存,代理緩存的表現也意味着任何GET或HEAD請求也許或者不會實際去請求伺服器。

不能希望緩存PUT、POST或DELETE方法。這些方法隻有當應用程式狀态變化時才會被使用(如删除一條博文)。緩存它們将會阻止一些來自點選或改變你應用程式的請求。

HTTP 1.1 允許預設緩存任何東西,除非有一個明顯的Cache-Control頭。在實際中,當請求有一個cookie、授權頭、使用非安全方法(如PUT、POST和DELETE)或者當響應有一個重定向狀态碼時,大多數緩存什麼也不做。

在開發者和下列規則沒有設定時,Symfony2會自動設定一個合理保守的Cache-Control頭:

如果沒有定義緩存頭(Cache-Control、Expires、ETag或 Last-Modified)時,Cache-Control被設定成no-cache,意思是響應将不會被緩存;

如果Cache-Control為空(但其它緩存頭之一被遞交),它的值被設定為private, must-revalidate;

但如果至少有一個Cache-Control指令被設定,并且非public或private被明确添加,Symfony2将自動添加private指令(除了s-maxage被設定)。

HTTP規範定義了兩個緩存模型:

失效模式中,通過包含Cache-Control和/或失效頭,你隻需簡單指定響應多長時間内會被認為是“新鮮”。緩存了解失效,它将不會生成制造的請求,直到緩存的版本到了它失效的時間,并變得“陳舊”。

當頁面是真正地動态時(如它們的表現經常改變),驗證模型通常是必須的。這種模式中緩存儲存響應,但無論緩存的響應是否有效,每次請求都會詢問伺服器。應用程式使用一個唯一的響應辨別(Etage頭)和/或一個時間戳(Last-Modified頭)去檢查在緩存之後頁面是否被更改。

兩種模式的目标是永遠不會兩次生成同一個響應,它們依賴緩存去儲存和傳回“新鮮”的響應。

讀HTTP規範

作為一名Web開發者,我們強烈督促你去讀這個規範。它的清晰和強大是無價的,距建立之日已經超過了十年。不要因為它的外觀而離去,它的内容遠比它的封面美麗。

失效模型是兩種緩存模型中更有效也更直接的,無論何時隻要可能就應該使用。當有着失效期限的響應被緩存時,緩存将儲存響應并無須理會應用程式而直接傳回該響應,直到該響應失效。

失效模型可以使用HTTP頭Expries或Cache-Control兩者中的一個來實作,兩者幾乎相同。

根據HTTP規範,“Expires頭字段給出日期/時間,之後響應被認為陳舊",Expires頭可以通過setExpires()響應方法來設定。它使用DateTime執行個體作為參數:

$date = new DateTime(); 

$date->modify('+600 seconds'); 

$response->setExpires($date); 

HTTP頭最後看上去象這樣:

Expires: Thu, 01 Mar 2011 16:00:00 GMT 

正如規範所要求的那樣,setExpires()方法會自動将日期轉換到GMT時區。

Expires頭有兩個限制。首先,Web伺服器和緩存(如:浏覽器)上的時鐘必須同步。然後,規範指出"HTTP/1.1服務不能發送超過一年的失效日期。"

因為Expires頭的限制,大多數情況下,你應該使用Cache-Control頭來代替。回想一下,Cache-Control頭被用于指定許多不同緩存指令。對于失效,有兩個指令max-age和s-maxage。第一個用于所有緩存,而第二個隻考慮共享緩存:

// Sets the number of seconds after which the response 

// should no longer be considered fresh 

// Same as above but only for shared caches 

Cache-Control頭将使用以下格式(它也許還有附加指令):

Cache-Control: max-age=600, s-maxage=600 

當底層資料一旦改變時資源就需要更新時,失效模型是不足的。使用失效模型,應用程式不會被要求傳回更新的響應,直到緩存最終變成陳舊。

驗證模型解決了這個問題。在這種模型下,緩存仍然儲存響應。不同在于,對于每個請求,緩存都會詢問應用程式被緩存的響應是否有效。如果緩存仍然有效,你的應用程式将傳回304狀态碼,而沒有具體内容。這樣就告訴緩存它是OK的,以便傳回被緩存的内容。

在這種模型下,你主要節省了帶寬,因為無須向同一用戶端發送兩次(用304響應代替)。但如果你應用程式設計仔細的話,你也可以通過發送304響應得到最低限度的資料,也節省CPU(參見下面示例的實作)。

304狀态碼意味着“不用修改”。這是重要的,因為這個狀态碼沒有包含實際被請求的内容。相反,響應隻是簡單一組輕量級的指令,告訴緩存它應該使用它儲存的版本。

就象失效一樣,有兩個HTTP頭可以實作驗證模型:ETag和Last-Modified

ETag頭是一個字元串頭(被稱為“實體标簽”),它完全被應用程式生成和設定,以便你可以看出它是唯一辨別代表目的資源的。舉個例子,被緩存儲存的/about資源是根據應用程式的傳回進行更新。ETag就象是一個指紋,并用來快速比較資源的兩個不同版本是否相等。象指紋一樣,每個ETag必須是唯一代表同一資源的。

讓我們看看做為内容的MD5加密來生成ETag的簡單實作。

public function indexAction() 

    $response = $this->renderView('MyBundle:Main:index.html.twig'); 

    $response->setETag(md5($response->getContent())); 

    $response->isNotModified($this->get('request')); 

    return $response; 

Response::isNotModified()方法比較請求發送的和在響應上設定的ETag,如果兩者比對,方法将自動設定響應狀态碼為304。

算法足夠簡單也非常通用,但你需要在能計算ETag之前建立整個響應。這是次優的,換句話說,它節省帶寬,而不是CPU。

在根據驗證優化你的代碼一節中,我們将展示驗證是如何智能地用于決定緩存驗證,而無須做大量的工作。

Symfony2也支援通過向setETag()方法的第二個參數發送true來調整ETag。

Last-Modified是第二個驗證的方式。根據HTTP規範,“Last-Modified頭表示的日期和時間,使源伺服器相信它表示最後修改的日期和時間"。換句話說,應用程式決定是否更新緩存内容是基于響應被緩存後,該響應是否被更新。

例如,你可以為所有需要計算資源表現的對象使用最後更新的日期做為Last-Modified頭的值:

public function showAction($articleSlug) 

    // ... 

    $articleDate = new \DateTime($article->getUpdatedAt()); 

    $authorDate = new \DateTime($author->getUpdatedAt()); 

    $date = $authorDate > $articleDate ? $authorDate : $articleDate; 

    $response->setLastModified($date); 

Response::isNotModified()方法比較請求發送的If-Modified-Since頭和在響應上設定的Last-Modified頭,如果兩者相等,響應将被設定304的狀态碼。

If-Modified-Since請求頭等于為個别資源發送給用戶端的最後響應的Last-Modified頭。這就是用戶端和服務端互相通信并決定資源被緩存後是否被更新。

緩存政策的主目标是減輕應用程式的負載。換句話說,在應用程式傳回304響應中你做得越少就越好。Response::isNotModified()通過暴露一個簡單而有效的模式來實作這一點:

    // Get the minimum information to compute 

    // the ETag or the Last-Modified value 

    // (based on the Request, data are retrieved from 

    // a database or a key-value store for instance) 

    $article = // ... 

    // create a Response with a ETag and/or a Last-Modified header 

    $response = new Response(); 

    $response->setETag($article->computeETag()); 

    $response->setLastModified($article->getPublishedAt()); 

    // Check that the Response is not modified for the given Request 

    if ($response->isNotModified($this->get('request'))) { 

        // return the 304 Response immediately 

        return $response; 

    } else { 

        // do more work here - like retrieving more data 

        $comments = // ... 

        // or render a template with the $response you've already started 

        return $this->render( 

            'MyBundle:MyController:article.html.twig', 

            array('article' => $article, 'comments' => $comments), 

            $response 

到目前為止,我們已經假設每個URI正好表示一個目的資源。預設狀況下,HTTP緩存通過使用資源的URI做為緩存關鍵詞來實作。如果兩個使用者請求同一可緩存資源的URI,那麼第二個使用者将得到被緩存的版本。

有時這并不夠,相同URI的不同緩存版本需要基于一個或更多請求頭的值。例如,當用戶端支援壓縮頁面時,而你又這樣做了,那麼任何給點URL都有兩種形式:一種是用戶端支援壓縮,一種是用戶端不支援壓縮。這需要通過Accept-Encoding請求頭的值來決定。

在本例中,你需要緩存為特定的URL儲存響應的壓縮和沒壓縮的兩個版本,并且基于請求的Accept-Encoding值來傳回它們。這是通過使用Vary響應頭來實作的,該響應頭使用逗号分隔的頭清單,這些值引發被請求資源的不同表現:

Vary: Accept-Encoding, User-Agent 

這個特殊的Vary頭将緩存每個基于URL資源的不同版本、Accept-Encoding值和User-Agent請求頭。

響應對象提供完整的接口去管理Vary頭

// set one vary header 

$response->setVary('Accept-Encoding'); 

// set multiple vary headers 

$response->setVary(array('Accept-Encoding', 'User-Agent')); 

setVary()方法為響應的Vary頭提供頭名或頭名資料。

你當然可以在同一響應中使用失效和驗證。因為失效高于驗證,是以你可以很輕易地兩全其美。換句話說,通過使用失效和驗證,你可以訓示緩存将被緩存的内容送到伺服器,該内容在一定間隔(失效)後檢查,驗證内容是否仍然有效。

Response類提供了更多關于緩存的方法。這裡是最有用的一些:

// Marks the Response stale 

$response->expire(); 

// Force the response to return a proper 304 response with no content 

$response->setNotModified(); 

另外,大多數緩存相關的HTTP頭可以通過單個setCache()方法設定:

// Set cache settings in one call 

$response->setCache(array( 

    'etag'          => $etag, 

    'last_modified' => $date, 

    'max_age'       => 10, 

    's_maxage'      => 10, 

    'public'        => true, 

    // 'private'    => true, 

)); 

ESI規範描述你可以内嵌到你頁面的标簽,以便與網關緩存通信。在Symfony2中隻實作了一個标簽,include,因為這是在Akamaï上下文之外唯一有用的一個:

<html> 

    <body> 

        Some content 

        <!-- Embed the content of another page here --> 

        <esi:include src="http://..." /> 

        More content 

    </body> 

</html> 

注意例子中的每個ESI标簽都有一個完全合格的URL。一個ESI标簽代表一個頁面片段,可以通過給定URL引入。

當請求被處理時,網關緩存引入從緩存或後端應用程式請求的整個頁面。如果響應包含一個或多個ESI标記,它們都是以相同的方式處理。換句話說,網關緩存可以從緩存中檢索包含的頁面片段,也可以從後端應用程式中再次請求頁面片段。當所有的ESI标記已經解析,網關緩存合并成整個頁面,并将其最終内容發送給用戶端。

所有發生在網關緩存層的一切都是透明的(如應用程式無關)。如你所見,如果你選擇使用ESI标簽,Symfony2可以使包含它們的過程毫不費力。

首先,要使用ESI,需要確定在你的應用程式配置中啟動它:

# app/config/config.yml 

framework: 

    # ... 

    esi: { enabled: true } 

現在,假設我們有一個相對靜态的頁面,除了内容底部的新聞滾動條。通過ESI,我們可以緩存除新聞滾動條之外的頁面其它部分。

    $response = $this->renderView('MyBundle:MyController:index.html.twig'); 

    $response->setSharedMaxAge(600); 

在本例中,我們全頁面緩存十分鐘的生命周期。接下來,讓我們在模闆中通過内嵌一個動作包含新聞滾動條。它可以通過render助手函數實作(參見templating-embedding-controller 以得到更多細節)。

因為内嵌内容來自其它頁(或控制器),Symfony2使用标準的render助手函數來配置ESI标簽:

{% render '...:news' with {}, {'standalone': true} %} 

通過将standalone設定為true,你告訴Symfony2動作應該作為ESI标簽渲染。你也許疑惑為什麼你想使用助手函數代替ESI标簽?那是因為使用助手函數可以使你的應用程式即使在沒安裝網關緩存的情況下正常運作。讓我們看看它是如何工作的。

當standalone為false時(預設值),Symfony2在發送響應到用戶端之前合并被包含的頁面内容到首頁面。但當standalone為真是,如果Symfony2檢測到它正在與一個支援ESI的網關緩存會話時,它會生成一個ESI的include标簽。如果沒有網關緩存或該緩存不支援ESI時,Symfony2将隻是把被包含的頁面内容合并到首頁面中,就象standalone被設定成false一樣。

Symfony2檢測網關緩存是否通過另一個Akamaï規範支援ESI,該規範通過Symfony2反向代理的開箱支援。

被内嵌的動作現在可以指定它自己的緩存規則,完全獨立于首頁面。

public function newsAction() 

  // ... 

  $response->setSharedMaxAge(60); 

通過ESI,整個頁面緩存的有效時間是600秒,而新聞討論區件緩存僅為60秒。

然而,ESI的要求是内嵌動作可以通過URL通路,是以網關代理可以将它從頁面的其它部分中獨立出來。當然一個動作不能通過URL來通路,除非有路由指向它。Symfony2通過路由和控制器可以實作。為了要讓ESI的include标簽正确工作,你必須定義_internal路由:

# app/config/routing.yml 

_internal: 

    resource: "@FrameworkBundle/Resources/config/routing/internal.xml" 

    prefix:   /_internal 

因為這條路由允許所有動作可以通過URL通路,是以你也許想要通過使用Symfony2防火牆功能(通過允許通路你反向代理的IP位址範圍)來保護它。

這種緩存政策最大的好處在于你可以使你的應用程式根據需要在同一時間裡盡可能的動态,命中最可能的少。

一旦開始使用ESI,記住總是要用s-maxage指令去替代max-age。因為浏覽器隻接受彙總的資源,它并不知道子元件,是以它總是聽從max-age指令并緩存整個頁面。而你并不想那樣。

render助手函數支援其它兩個有用的選項:

alt: 作為ESI标簽中的alt屬性,允許你指定另一個URL,在src沒有找到時替代;

ignore_errors: 如果設為真,帶着表明繼續值的onerror屬性會被添加到ESI中。當發生故障時,網關緩存将默默地将ESI标簽删除。

"在計算機學科隻有兩個難題:緩存無效和命名事物" --菲爾 卡爾頓

你永遠不需要去處理無效的緩存資料,因為無效已經被考慮到HTTP緩存模型中。如果你使用驗證模型,你永遠不需要任何被定義無效的事物;如果你使用失效模型,資源無效,這就意味着你設定的失效時間太長了。

又因為沒有無效機制,你可以使用任何反向代理,而無須改動你的應用程式代碼。

實際上,所有反向代理都提供删除緩存資料的方式,但你應該盡可能地避免使用它們。最标準的做法是通過請求指定的URL來删除緩存,該請求帶有特殊PURGE的HTTP方法。

這裡是如何配置Symfony2反向代理支援PURGE的HTTP方法:

    protected function invalidate(Request $request) 

        if ('PURGE' !== $request->getMethod()) { 

            return parent::invalidate($request); 

        } 

        $response = new Response(); 

        if (!$this->store->purge($request->getUri())) { 

            $response->setStatusCode(404, 'Not purged'); 

        } else { 

            $response->setStatusCode(200, 'Purged'); 

無論如何你都必須保護PURGE的HTTP方法,以避免随機使用者删除你的緩存資料。

Symfony2被設計用來遵循被驗證的規則:HTTP。緩存也不例外。掌握Symfon2緩存系統意味着更加熟悉HTTP緩存模型并加以有效利用。這也意味着你已經有機會接近HTTP緩存和網關緩存(如Varnish)相關知識的世界,而不僅僅隻是Symfony2文檔和代碼示例。

本文轉自 firehare 51CTO部落格,原文連結:http://blog.51cto.com/firehare/584557,如需轉載請自行聯系原作者