Windows Azure 快取策略
我用於緩存的兩步過程最初是在網路快速發展時期創建的。的確,我當時必須在用戶端或記憶體中的各個位置緩存一些資料,以便能夠對我在此之前構建的應用程式更輕鬆或更快捷地執行操作。但直到互聯網(尤其是互聯網商務)呈現爆炸式增長後,我的一些關於我在 Web 和桌面等應用程式中採用的緩存策略的想法才真正開始發生變化。
在本專欄中,我會將各種 Windows Azure 緩存功能映射到輸出、記憶體中資料和檔資源的緩存策略,並且我還將嘗試在新資料需求與最佳性能需求之間取得平衡。最後,我將簡要介紹一下進行智慧緩存的間接方法。
資源緩存
我所說的資源緩存是指已序列化為在端點使用的檔案格式的任何內容,其中包括從序列化物件(例如 XML 和 JSON)到圖像和視頻的所有內容。您可以嘗試使用標頭和 META 標記來影響流覽器的緩存行為,但這些建議往往不能得到很好的遵循,並且服務介面將忽略標頭,這幾乎是必然的結局。因此,不要指望我們可以在 Web 用戶端成功緩存緩慢變化的資源內容 - 至少為了保證負載下的相應性能和行為 - 我們必須後退一步。但是,對於大多數資源,我們可以使用內容交付網路,而不是將其返回到 Web 伺服器。
考慮從用戶端返回的路徑,我們可以利用前端 Web 伺服器和用戶端之間(特別是在廣泛的地理位置中)的分類路標將內容放到離使用者更近的位置。不僅在這些位置緩存該內容,而且更重要的是,它離最終使用者更近。用於分發的伺服器統稱為內容交付/分發網路。在互聯網迅速發展的早期,針對 Web 進行分散式資源緩存的想法和實施方式相當新穎,並且諸如 Akami Technologies 之類的公司已發現銷售可説明擴大網站規模的服務的大好時機。一晃十年過去了,Web 將分散在世界各地的人們聯繫在一起,該策略比以往任何時候都更加重要。Microsoft 針對 Windows Azure 提供了 Windows Azure 內容交付網路 (CDN)。儘管 CDN 是一種緩存內容和將內容移動到離使用者更近位置的有效策略,但現實是,通常情況下它更多地由具備大規模資源和/或大量或大型資源的網站使用。可在 Steve Marx(他在 Windows Azure 團隊工作)的博客 (bit.ly/fvapd7) 上找到有關使用 Windows Azure CDN 的精彩文章。
在部署網站的大多數情況下,需要將檔放在網站伺服器上,這一點似乎很明顯。在 Windows Azure Web 角色中,網站內容部署在套裝程式中 - 看,我已完成了。等等,市場行銷部門的最新映射未隨套裝程式一起推送;需要重新部署。更新該內容目前實際上意味著重新部署套裝程式。當然,可以按階段部署套裝程式和轉換套裝程式,但使用者端會出現延遲,或者可能出現停頓。
提供可更新的前端 Web 內容緩存的直接方法是,將大部分內容存儲到 Windows Azure 存儲中,並將所有 URI 指向 Windows Azure 存儲容器。但是,出於各種原因,通過 Web 角色保存內容或許是最好方式。確保可刷新 Web 角色內容或可添加新內容的一種方法是,將檔保存在 Windows Azure 存儲中,然後在需要時將它們移到 Web 角色上的本地資源存儲容器中。該主題有幾種不同的形式,我在 2010 年 3 月發佈的一篇博客文章 (bit.ly/u08DkV) 中討論了其中一種形式。
記憶體中緩存
上面的緩存討論實際上重點關注移動基於檔的資源,下麵我將重點介紹網站的所有資料和動態呈現內容。我已針對網站的性能和背後的資料庫進行了大量性能測試和優化。毫無例外,具有涵蓋輸出緩存(不必再次呈現、可直接發送到用戶端的已呈現 HTML)和資料(通常為緩存端樣式)的可靠緩存計畫和實現方式能夠使您顯著擴大規模和改進性能,前提是資料庫實現本身未中斷。
在網站中實現緩存策略的難點在於,確定在每次請求時哪些內容需要緩存、哪些內容仍動態呈現以及緩存內容的刷新頻率。除 Microsoft .NET Framework 為輸出緩存和 System.Web.Caching 提供的標準功能外,Windows Azure 還提供了名為 Windows Azure AppFabric 緩存(簡稱 AppFabric 緩存)的分散式緩存。
分散式緩存
分散式緩存可説明解決多個問題。例如,雖然始終建議通過緩存來保證網站性能,但即使會話狀態可提供上下文緩存,通常也禁止使用它。原因是,獲得會話狀態要求用戶端綁定到伺服器(這會對可伸縮性產生不利影響),或者會話狀態會在伺服器場中的伺服器之間進行同步,而通常有充分的理由認為這會導致出現問題和局限性。使用功能強大且可靠的分散式緩存對會話狀態進行備份可解決會話狀態問題。這使得伺服器能夠在無需連續訪問資料庫以獲取資料的情況下便可擁有這些資料,同時提供了一種寫入資料並在緩存用戶端中無縫傳播該資料的機制。這樣可向開發人員提供充足的上下文緩存,同時維護 Web 場的規模品質。
有關 AppFabric 緩存的最好消息是,當涉及會話狀態時,您只需更改一些配置設置便可使用它,並且它有一個可用於程式設計的簡單易用的 API。有關使用該緩存的一些有用的詳細資訊,請閱讀 Karandeep Anand 和 Wade Wegner 在 2011 年 4 月的期刊中發表的文章 (msdn.microsoft.com/magazine/gg983488)。
遺憾的是,如果您要處理直接在代碼中調用 System.Web.Caching 的現有網站,則引入 AppFabric 緩存會比較麻煩。這有兩個原因:
- API 之間存在差異(參見圖 1)
- 緩存內容和緩存位置的策略
圖 1 按緩存 API 添加內容
添加到 AppFabric 緩存 | 添加到 System.Web.Caching 緩存 |
DataCacheFactory cacheFactory= new DataCacheFactory(configuration); DataCache appFabCache = cacheFactory.GetDefaultCache(); string value = "This string is to be cached locally."; appFabCache.Put("SharedCacheString", value); |
System.Web.Caching.Cache LocalCache = new System.Web.Caching.Cache(); string value = "This string is to be cached locally."; LocalCache.Insert("localCacheString", value); |
圖 1 清楚地演示了即使當您查看 API 的基本元素時,也肯定會看到其中存在差別。創建間接層以調配調用將有助於提高應用程式中代碼的靈活性。很明顯,需要做一些工作才能輕鬆地使用三種緩存類型的高級功能,而獲得的好處將超過為了實現所需功能需要付出的努力。
雖然分散式緩存確實可以解決一些通常難於解決的問題,但不應將其用作解決所有問題的萬能鑰匙,它也不可能具有與萬能鑰匙相同的功效。首先,根據各方面因素達到平衡的方式和進入緩存中的資料,可能需要進行更多獨立于電腦的提取才能使資料進入本地緩存用戶端,而這會對性能產生負面影響。More important is the cost of deployment.到撰寫本文時為止,4GB AppFabric 共用緩存的成本是每月 325 美元。儘管此金額本身並不大,而且 4GB 似乎是一個合適的緩存空間,但在高流量網站中,尤其是使用 AppFabric 緩存備份會話狀態和包含大量豐富的針對性內容的網站中,將很容易填滿多個該大小的緩存。此時可以考慮根據客戶層調整價格差或自訂合約定價的產品目錄。
緩存端間接層
和技術行業中的許多事情一樣(並且我猜想在許多其他行業也一樣),設計過程是指對根據實際財務狀況進行修改的理想技術實現方式的某種融合。因此,即使在只使用 Windows Server 2008 R2 AppFabric 緩存時,也有理由繼續使用 System.Web.Caching 提供的本地緩存。在創建間接層的第一輪操作中,我可能封裝了對每個緩存庫的調用,並為每個緩存庫提供了一個函數,例如 AddtoLocalCache(key, object) 和 AddtoSharedCache(key, object)。但是,這意味著每次需要執行緩存操作時,開發人員都會對應進行緩存的位置做出不透明的個人決策。這種邏輯在進行維護時和在較大團隊中會被迅速摧毀,並將不可避免地造成無法預料的錯誤,因為開發人員可能選擇將物件添加到不適當的緩存中,或添加到某個緩存中然後意外地從另一緩存中提取物件。因此,將需要執行許多額外的資料提取操作,因為在提取資料時,該資料沒有位於相應緩存中,或者位於錯誤的緩存中。這會導致在注意到性能異常差時,經檢查才發現,添加操作是在一個緩存中完成的,但卻莫名其妙地在另一緩存中執行了獲取操作,而這只是因為開發人員忘記了緩存位置或出現了鍵入錯誤。而且,在正確規劃系統時,將提前確定這些資料類型(實體),並且在後續定義中還應考慮每個實體的使用位置、一致性要求(尤其是跨負載平衡伺服器時)以及必需的資料新鮮程度。由此可見,可提前做出有關緩存位置(共用與否)和過期時間的決策並使其成為聲明的一部分。
正如我在上面提到的,應制定緩存計畫。很多時候,人們都是在專案結束時隨意增加緩存計畫,但應像對待應用程式的任何其他方面一樣,仔細考慮和設計緩存計畫。在涉及到雲時,這一點尤其重要,因為考慮不周的決策通常會導致額外的成本和應用程式列為缺陷。在考慮應緩存的資料類型時,一種方法是確定所涉及的實體(資料類型)及其在應用程式和使用者會話中的生命週期。這種方法快速揭示出,如果可以只基於實體類型對實體本身進行智慧緩存,會很不錯。幸運的是,可借助一個自訂屬性輕鬆完成此任務。
我將跳過任一緩存的設置過程,因為上面引用的材料中已詳細介紹這方面的內容。對於我的緩存庫,我僅使用靜態方法創建了一個靜態類作為示例。在其他實現中,有充分的理由使用實例物件執行此操作,但為了簡化本示例,我將其設為靜態。
我將聲明一個指示位置和繼承屬性的類的枚舉,以實現我的自訂屬性,如圖 2 所示。
圖 2 聲明枚舉以實現自訂屬性
public enum CacheLocationEnum
{
None=0,
Local=1,
Shared=2
}
public class CacheLocation:Attribute
{
private CacheLocationEnum _location = CacheLocationEnum.None;
public CacheLocation(CacheLocationEnum location)
{
_location = location;
}
public CacheLocationEnum Location { get { return _location; } }
}
在構造函數中傳遞位置使您稍後可在代碼中輕鬆使用位置,但我還將提供一個唯讀方法來提取值,因為我需要在 Case 語句中使用該值。 在我的 CacheManager 庫中,我已創建一對用於添加到兩個緩存中的私有方法:
private static bool AddToLocalCache(string key, object newItem)
{...}
private static bool AddToSharedCache(string key, object newItem)
{...}
在真正的實現中,我可能需要一些其他資訊(例如緩存名稱、依賴關係、過期時間等等),但目前這些資訊就已足夠。 用於向緩存中添加內容的主要公共函數是一個範本方法,這使我能夠根據類型輕鬆確定緩存,如圖 3 所示。
圖 3 向緩存中添加內容
public static bool AddToCache<T> (string key, T newItem)
{
bool retval = false;
Type curType = newItem.GetType();
CacheLocation cacheLocationAttribute =
(CacheLocation) System.Attribute.GetCustomAttribute(typeof(T),
typeof(CacheLocation));
switch (cacheLocationAttribute.Location)
{
case CacheLocationEnum.None:
break;
case CacheLocationEnum.Local:
retval = AddToLocalCache(key, newItem);
break;
case CacheLocationEnum.Shared:
retval = AddToSharedCache(key, newItem);
break;
}
return retval;
}
我將只使用傳入類型獲取自訂屬性,並通過 GetCustomAttribute(type, type) 方法請求自訂屬性類型。 獲取自訂屬性後,只需調用該唯讀屬性和 Case 語句,並且我已成功將該調用路由到相應的緩存提供程式。 為確保正確執行此操作,我需要適當地修飾類聲明:
[CacheLocation(CacheLocationEnum.Local)]
public class WebSiteData
{
public int IntegerValue { get; set; }
public string StringValue { get; set; }
}
[CacheLocation(CacheLocationEnum.Shared)]
public class WebSiteSharedData
{
public int IntegerValue { get; set; }
public string StringValue { get; set; }
]}
應用程式基礎結構全部設置完畢後,我就可以在應用程式碼內使用該基礎結構。 我打開 default.aspx.cs 檔以創建示例調用並添加代碼以創建類型,分配一些值並將它們添加到緩存中:
WebSiteData data = new WebSiteData();
data.IntegerValue = 10;
data.StringValue = "ten";
WebSiteSharedData sharedData = new WebSiteSharedData();
sharedData.IntegerValue = 50;
sharedData.StringValue = "fifty";
CachingLibrary.CacheManager.AddToCache<WebSiteData>("localData", data);
CachingLibrary.CacheManager.AddToCache<WebSiteSharedData>(
"sharedData", sharedData);
類型名稱清楚地表明瞭將緩存資料的位置。 但是,我可以更改類型名稱,這不會對由自訂屬性檢查控制的緩存產生明顯影響。 使用此模式會向頁面開發者隱藏資料緩存位置的詳細資訊以及與緩存項配置相關的其他詳細資訊。 因此,應由團隊中負責創建資料詞典和設定上述資料的整個生命週期的人員做出這些決策。 請注意傳遞到對 AddToCache<t>(string, t) 的調用的類型。 實現 CacheManager 類的其餘方法(即 GetFromCache)時將採取此處實現 AddToCache 方法時使用的相同模式。
在成本與性能和規模之間取得平衡
Windows Azure 提供了必要的軟體基礎結構以便在實現方式的任何方面為您提供説明,包括緩存以及緩存是針對資源(例如通過 CDN 分發的資源)還是針對可能保存在 AppFabric 緩存中的資料。 獲得出色設計和後續良好實現的關鍵是在成本與性能和規模之間取得平衡。 最後一個注意事項:如果您正在處理新應用程式,並且計畫在其中構建緩存,請繼續並立即創建該間接層。 這是一項額外的工作,但隨著 AppFabric 緩存等新功能的上線,這種做法會讓您能夠更輕鬆地將這些新功能周全、有效地整合到您的應用程式中。
Joseph Fultz 是 Hewlett-Packard Co. 的軟體架構師,參與 HP.com 全球 IT 小組的工作。之前,他是 Microsoft 的軟體架構師,協助 Microsoft 頂層企業和 ISV 客戶定義體系結構和設計解決方案。
衷心感謝以下技術專家對本文的審閱:Wade Wegner